Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
65.33% covered (warning)
65.33%
49 / 75
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdapterFactory
65.33% covered (warning)
65.33%
49 / 75
14.29% covered (danger)
14.29%
1 / 7
48.00
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 __invoke
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getAdapter
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 getDriverName
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
7.91
 getDriverOptions
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 getAdapterFromOptions
44.44% covered (danger)
44.44%
8 / 18
0.00% covered (danger)
0.00%
0 / 1
9.29
 getAdapterFromConnectionString
87.50% covered (warning)
87.50%
28 / 32
0.00% covered (danger)
0.00%
0 / 1
6.07
1<?php
2
3/**
4 * Database utility class. May be used as a service or as a standard
5 * Laminas factory.
6 *
7 * PHP version 8
8 *
9 * Copyright (C) Villanova University 2010.
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2,
13 * as published by the Free Software Foundation.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
23 *
24 * @category VuFind
25 * @package  Db
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org Main Site
29 */
30
31namespace VuFind\Db;
32
33use Laminas\Config\Config;
34use Laminas\Db\Adapter\Adapter;
35use Laminas\ServiceManager\Exception\ServiceNotCreatedException;
36use Laminas\ServiceManager\Exception\ServiceNotFoundException;
37use Psr\Container\ContainerExceptionInterface as ContainerException;
38use Psr\Container\ContainerInterface;
39
40/**
41 * Database utility class. May be used as a service or as a standard
42 * Laminas factory.
43 *
44 * @category VuFind
45 * @package  Db
46 * @author   Demian Katz <demian.katz@villanova.edu>
47 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
48 * @link     https://vufind.org Main Site
49 */
50class AdapterFactory implements \Laminas\ServiceManager\Factory\FactoryInterface
51{
52    /**
53     * VuFind configuration
54     *
55     * @var Config
56     */
57    protected $config;
58
59    /**
60     * Constructor
61     *
62     * @param Config $config VuFind configuration (provided when used as service;
63     * omitted when used as factory)
64     */
65    public function __construct(Config $config = null)
66    {
67        $this->config = $config ?: new Config([]);
68    }
69
70    /**
71     * Create an object (glue code for FactoryInterface compliance)
72     *
73     * @param ContainerInterface $container     Service manager
74     * @param string             $requestedName Service being created
75     * @param null|array         $options       Extra options (optional)
76     *
77     * @return object
78     *
79     * @throws ServiceNotFoundException if unable to resolve the service.
80     * @throws ServiceNotCreatedException if an exception is raised when
81     * creating a service.
82     * @throws ContainerException&\Throwable if any other error occurs
83     */
84    public function __invoke(
85        ContainerInterface $container,
86        $requestedName,
87        array $options = null
88    ) {
89        if (!empty($options)) {
90            throw new \Exception('Unexpected options sent to factory!');
91        }
92        $this->config = $container->get(\VuFind\Config\PluginManager::class)
93            ->get('config');
94        return $this->getAdapter();
95    }
96
97    /**
98     * Obtain a Laminas\DB connection using standard VuFind configuration.
99     *
100     * @param string $overrideUser Username override (leave null to use username
101     * from config.ini)
102     * @param string $overridePass Password override (leave null to use password
103     * from config.ini)
104     *
105     * @return Adapter
106     */
107    public function getAdapter($overrideUser = null, $overridePass = null)
108    {
109        // Parse details from connection string:
110        if (!isset($this->config->Database->database)) {
111            throw new \Exception('"database" setting missing');
112        }
113        return $this->getAdapterFromConnectionString(
114            $this->config->Database->database,
115            $overrideUser,
116            $overridePass
117        );
118    }
119
120    /**
121     * Translate the connection string protocol into a driver name.
122     *
123     * @param string $type Database type from connection string
124     *
125     * @return string
126     */
127    public function getDriverName($type)
128    {
129        switch (strtolower($type)) {
130            case 'mysql':
131                return 'mysqli';
132            case 'oci8':
133                return 'Oracle';
134            case 'pgsql':
135                return 'Pdo_Pgsql';
136        }
137        return $type;
138    }
139
140    /**
141     * Get options for the selected driver.
142     *
143     * @param string $driver Driver name
144     *
145     * @return array
146     */
147    protected function getDriverOptions($driver)
148    {
149        switch ($driver) {
150            case 'mysqli':
151                return ($this->config->Database->verify_server_certificate ?? false)
152                    ? [] : [MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT];
153        }
154        return [];
155    }
156
157    /**
158     * Obtain a Laminas\DB connection using an option array.
159     *
160     * @param array $options Options for building adapter
161     *
162     * @return Adapter
163     */
164    public function getAdapterFromOptions($options)
165    {
166        // Set up custom options by database type:
167        $driver = strtolower($options['driver']);
168        switch ($driver) {
169            case 'mysqli':
170                $options['charset'] = $this->config->Database->charset ?? 'utf8mb4';
171                if (strtolower($options['charset']) === 'latin1') {
172                    throw new \Exception(
173                        'The latin1 encoding is no longer supported for MySQL'
174                        . ' databases in VuFind. Please convert your database'
175                        . ' to utf8 using VuFind 7.x or earlier BEFORE'
176                        . ' upgrading to this version.'
177                    );
178                }
179                $options['options'] = ['buffer_results' => true];
180                break;
181        }
182
183        // Set up database connection:
184        $adapter = new Adapter($options);
185
186        // Special-case setup:
187        if ($driver == 'pdo_pgsql' && isset($this->config->Database->schema)) {
188            // Set schema
189            $statement = $adapter->createStatement(
190                'SET search_path TO ' . $this->config->Database->schema
191            );
192            $statement->execute();
193        }
194
195        return $adapter;
196    }
197
198    /**
199     * Obtain a Laminas\DB connection using a connection string.
200     *
201     * @param string $connectionString Connection string of the form
202     * [db_type]://[username]:[password]@[host]/[db_name]
203     * @param string $overrideUser     Username override (leave null to use username
204     * from connection string)
205     * @param string $overridePass     Password override (leave null to use password
206     * from connection string)
207     *
208     * @return Adapter
209     */
210    public function getAdapterFromConnectionString(
211        $connectionString,
212        $overrideUser = null,
213        $overridePass = null
214    ) {
215        [$type, $details] = explode('://', $connectionString);
216        preg_match('/(.+)@([^@]+)\/(.+)/', $details, $matches);
217        $credentials = $matches[1] ?? null;
218        $host = $port = null;
219        if (isset($matches[2])) {
220            if (str_contains($matches[2], ':')) {
221                [$host, $port] = explode(':', $matches[2]);
222            } else {
223                $host = $matches[2];
224            }
225        }
226        $dbName = $matches[3] ?? null;
227        if (strstr($credentials, ':')) {
228            [$username, $password] = explode(':', $credentials, 2);
229        } else {
230            $username = $credentials;
231            $password = null;
232        }
233        $username = $overrideUser ?? $username;
234        $password = $overridePass ?? $password;
235
236        $driverName = $this->getDriverName($type);
237        $driverOptions = $this->getDriverOptions($driverName);
238
239        // Set up default options:
240        $options = [
241            'driver' => $driverName,
242            'hostname' => $host,
243            'username' => $username,
244            'password' => $password,
245            'database' => $dbName,
246            'use_ssl' => $this->config->Database->use_ssl ?? false,
247            'driver_options' => $driverOptions,
248        ];
249        if (!empty($port)) {
250            $options['port'] = $port;
251        }
252        // Get extra custom options from config:
253        $extraOptions = isset($this->config->Database->extra_options)
254            ? $this->config->Database->extra_options->toArray()
255            : [];
256        // Note: $options takes precedence over $extraOptions -- we don't want users
257        // using extended settings to override values from core settings.
258        return $this->getAdapterFromOptions($options + $extraOptions);
259    }
260}