Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
20.41% covered (danger)
20.41%
30 / 147
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
database
20.41% covered (danger)
20.41%
30 / 147
33.33% covered (danger)
33.33%
2 / 6
1362.43
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_available_dbms
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
240
 remove_comments
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 split_sql_file
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 validate_table_prefix
65.52% covered (warning)
65.52%
19 / 29
0.00% covered (danger)
0.00%
0 / 1
14.10
 check_database_connection
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 1
506
1<?php
2/**
3 *
4 * This file is part of the phpBB Forum Software package.
5 *
6 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7 * @license GNU General Public License, version 2 (GPL-2.0)
8 *
9 * For full copyright and license information, please see
10 * the docs/CREDITS.txt file.
11 *
12 */
13
14namespace phpbb\install\helper;
15
16use phpbb\db\doctrine\connection_factory;
17use phpbb\install\exception\invalid_dbms_exception;
18use phpbb\filesystem\helper as filesystem_helper;
19
20/**
21 * Database related general functionality for installer
22 */
23class database
24{
25    /**
26     * @var \phpbb\filesystem\filesystem_interface
27     */
28    protected $filesystem;
29
30    /**
31     * @var string
32     */
33    protected $phpbb_root_path;
34
35    /**
36     * @var array
37     */
38    protected $supported_dbms = array(
39        // Note: php 5.5 alpha 2 deprecated mysql.
40        // Keep mysqli before mysql in this list.
41        'mysqli'    => array(
42            'LABEL'            => 'MySQL with MySQLi Extension',
43            'SCHEMA'        => 'mysql_41',
44            'MODULE'        => 'mysqli',
45            'DOCTRINE'        => ['pdo_mysql'],
46            'DELIM'            => ';',
47            'DRIVER'        => 'phpbb\db\driver\mysqli',
48            'AVAILABLE'        => true,
49            '2.0.x'            => true,
50        ),
51        'mssql_odbc'    =>    array(
52            'LABEL'            => 'MS SQL Server [ ODBC ]',
53            'SCHEMA'        => 'mssql',
54            'MODULE'        => 'odbc',
55            'DOCTRINE'        => ['pdo_sqlsrv'],
56            'DELIM'            => ';',
57            'DRIVER'        => 'phpbb\db\driver\mssql_odbc',
58            'AVAILABLE'        => true,
59            '2.0.x'            => true,
60        ),
61        'mssqlnative'    => array(
62            'LABEL'            => 'MS SQL Server 2005+ [ Native ]',
63            'SCHEMA'        => 'mssql',
64            'MODULE'        => 'sqlsrv',
65            'DOCTRINE'        => ['pdo_sqlsrv'],
66            'DELIM'            => ';',
67            'DRIVER'        => 'phpbb\db\driver\mssqlnative',
68            'AVAILABLE'        => true,
69            '2.0.x'            => false,
70        ),
71        'oracle'    =>    array(
72            'LABEL'            => 'Oracle',
73            'SCHEMA'        => 'oracle',
74            'MODULE'        => 'oci8',
75            'DELIM'            => ';',
76            'DRIVER'        => 'phpbb\db\driver\oracle',
77            'AVAILABLE'        => true,
78            '2.0.x'            => false,
79        ),
80        'postgres' => array(
81            'LABEL'            => 'PostgreSQL 8.3+',
82            'SCHEMA'        => 'postgres',
83            'MODULE'        => 'pgsql',
84            'DOCTRINE'        => ['pdo_pgsql'],
85            'DELIM'            => ';',
86            'DRIVER'        => 'phpbb\db\driver\postgres',
87            'AVAILABLE'        => true,
88            '2.0.x'            => true,
89        ),
90        'sqlite3'        => array(
91            'LABEL'            => 'SQLite3',
92            'SCHEMA'        => 'sqlite',
93            'MODULE'        => 'sqlite3',
94            'DOCTRINE'        => ['pdo_sqlite'],
95            'DELIM'            => ';',
96            'DRIVER'        => 'phpbb\db\driver\sqlite3',
97            'AVAILABLE'        => true,
98            '2.0.x'            => false,
99        ),
100    );
101
102    /**
103     * Constructor
104     *
105     * @param \phpbb\filesystem\filesystem_interface    $filesystem            Filesystem interface
106     * @param string                                    $phpbb_root_path    Path to phpBB's root
107     */
108    public function __construct(\phpbb\filesystem\filesystem_interface $filesystem, $phpbb_root_path)
109    {
110        $this->filesystem        = $filesystem;
111        $this->phpbb_root_path    = $phpbb_root_path;
112    }
113
114    /**
115     * Returns an array of available DBMS supported by phpBB
116     *
117     * If a DBMS is specified it will only return data for that DBMS
118     * and will load its extension if necessary.
119     *
120     * @param    mixed    $dbms                name of the DBMS that's info is required or false for all DBMS info
121     * @param    bool    $return_unavailable    set it to true if you expect unavailable but supported DBMS
122     *                                         returned as well
123     * @param    bool    $only_20x_options    set it to true if you only want to recover 2.0.x options
124     *
125     * @return    array    Array of available and supported DBMS
126     */
127    public function get_available_dbms($dbms = false, $return_unavailable = false, $only_20x_options = false)
128    {
129        $available_dbms = $this->supported_dbms;
130
131        if ($dbms)
132        {
133            if (isset($this->supported_dbms[$dbms]))
134            {
135                $available_dbms = array($dbms => $this->supported_dbms[$dbms]);
136            }
137            else
138            {
139                return array();
140            }
141        }
142
143        $any_dbms_available = false;
144        foreach ($available_dbms as $db_name => $db_array)
145        {
146            if ($only_20x_options && !$db_array['2.0.x'])
147            {
148                if ($return_unavailable)
149                {
150                    $available_dbms[$db_name]['AVAILABLE'] = false;
151                }
152                else
153                {
154                    unset($available_dbms[$db_name]);
155                }
156
157                continue;
158            }
159
160            $dll = $db_array['MODULE'];
161            if (!@extension_loaded($dll))
162            {
163                if ($return_unavailable)
164                {
165                    $available_dbms[$db_name]['AVAILABLE'] = false;
166                }
167                else
168                {
169                    unset($available_dbms[$db_name]);
170                }
171
172                continue;
173            }
174
175            if (array_key_exists('DOCTRINE', $db_array))
176            {
177                $available = false;
178                foreach ($db_array['DOCTRINE'] as $dll)
179                {
180                    if (@extension_loaded($dll))
181                    {
182                        $available = true;
183                        break;
184                    }
185                }
186
187                if (!$available)
188                {
189                    if ($return_unavailable)
190                    {
191                        $available_dbms[$db_name]['AVAILABLE'] = false;
192                    }
193                    else
194                    {
195                        unset($available_dbms[$db_name]);
196                    }
197
198                    continue;
199                }
200            }
201
202            $any_dbms_available = true;
203        }
204
205        if ($return_unavailable)
206        {
207            $available_dbms['ANY_DB_SUPPORT'] = $any_dbms_available;
208        }
209
210        return $available_dbms;
211    }
212
213    /**
214     * Removes "/* style" as well as "# style" comments from $input.
215     *
216     * @param string $sql_query    Input string
217     *
218     * @return string Input string with comments removed
219     */
220    public function remove_comments($sql_query)
221    {
222        // Remove /* */ comments (http://ostermiller.org/findcomment.html)
223        $sql_query = preg_replace('#/\*(.|[\r\n])*?\*/#', "\n", $sql_query);
224
225        // Remove # style comments
226        $sql_query = preg_replace('/\n{2,}/', "\n", preg_replace('/^#.*$/m', "\n", $sql_query));
227
228        return $sql_query;
229    }
230
231    /**
232     * split_sql_file() will split an uploaded sql file into single sql statements.
233     *
234     * Note: expects trim() to have already been run on $sql.
235     *
236     * @param    string    $sql        SQL statements
237     * @param    string    $delimiter    Delimiter between sql statements
238     *
239     * @return array Array of sql statements
240     */
241    public function split_sql_file($sql, $delimiter)
242    {
243        $sql = str_replace("\r" , '', $sql);
244        $data = preg_split('/' . preg_quote($delimiter, '/') . '$/m', $sql);
245
246        $data = array_map('trim', $data);
247
248        // The empty case
249        $end_data = end($data);
250
251        if (empty($end_data))
252        {
253            unset($data[key($data)]);
254        }
255
256        return $data;
257    }
258
259    /**
260     * Validates table prefix
261     *
262     * @param string    $dbms            The selected dbms
263     * @param string    $table_prefix    The table prefix to validate
264     *
265     * @return bool|array    true if table prefix is valid, array of errors otherwise
266     *
267     * @throws invalid_dbms_exception When $dbms is not a valid
268     */
269    public function validate_table_prefix($dbms, $table_prefix)
270    {
271        $errors = array();
272
273        if (!preg_match('#^[a-zA-Z][a-zA-Z0-9_]*$#', $table_prefix))
274        {
275            $errors[] = array(
276                'title' => 'INST_ERR_DB_INVALID_PREFIX',
277            );
278        }
279
280        // Do dbms specific checks
281        $dbms_info = $this->get_available_dbms($dbms);
282        switch ($dbms_info[$dbms]['SCHEMA'])
283        {
284            case 'mysql_41':
285                $prefix_length = 36;
286            break;
287            case 'mssql':
288                $prefix_length = 90;
289            break;
290            case 'oracle':
291                $prefix_length = 6;
292            break;
293            case 'postgres':
294                $prefix_length = 36;
295            break;
296            case 'sqlite':
297                $prefix_length = 200;
298            break;
299            default:
300                throw new invalid_dbms_exception();
301            break;
302        }
303
304        // Check the prefix length to ensure that index names are not too long
305        if (strlen($table_prefix) > $prefix_length)
306        {
307            $errors[] = array(
308                'title' => array('INST_ERR_PREFIX_TOO_LONG', $prefix_length),
309            );
310        }
311
312        return (empty($errors)) ? true : $errors;
313    }
314
315    /**
316     * Check if the user provided database parameters are correct
317     *
318     * This function checks the database connection data and also checks for
319     * any other problems that could cause an error during the installation
320     * such as if there is any database table names conflicting.
321     *
322     * Note: The function assumes that $table_prefix has been already validated
323     * with validate_table_prefix().
324     *
325     * @param string    $dbms            Selected database type
326     * @param string    $dbhost            Database host address
327     * @param int        $dbport            Database port number
328     * @param string    $dbuser            Database username
329     * @param string    $dbpass            Database password
330     * @param string    $dbname            Database name
331     * @param string    $table_prefix    Database table prefix
332     *
333     * @return array|bool    Returns true if test is successful, array of errors otherwise
334     */
335    public function check_database_connection($dbms, $dbhost, $dbport, $dbuser, $dbpass, $dbname, $table_prefix)
336    {
337        $dbms_info = $this->get_available_dbms($dbms);
338        $dbms_info = $dbms_info[$dbms];
339        $errors = array();
340
341        // Instantiate it and set return on error true
342        /** @var \phpbb\db\driver\driver_interface $db */
343        $db = new $dbms_info['DRIVER'];
344        $db->sql_return_on_error(true);
345
346        // Check that we actually have a database name before going any further
347        if (!in_array($dbms_info['SCHEMA'], array('sqlite', 'oracle'), true) && $dbname === '')
348        {
349            $errors[] = array(
350                'title' => 'INST_ERR_DB_NO_NAME',
351            );
352        }
353
354        // Make sure we don't have a daft user who thinks having the SQLite database in the forum directory is a good idea
355        if ($dbms_info['SCHEMA'] === 'sqlite'
356            && stripos(filesystem_helper::realpath($dbhost), filesystem_helper::realpath($this->phpbb_root_path) === 0))
357        {
358            $errors[] = array(
359                'title' =>'INST_ERR_DB_FORUM_PATH',
360            );
361        }
362
363        // Check if SQLite database is writable
364        if ($dbms_info['SCHEMA'] === 'sqlite'
365            && (($this->filesystem->exists($dbhost) && !$this->filesystem->is_writable($dbhost)) || !$this->filesystem->is_writable(pathinfo($dbhost, PATHINFO_DIRNAME))))
366        {
367            $errors[] = array(
368                'title' =>'INST_ERR_DB_NO_WRITABLE',
369            );
370        }
371
372        // Try to connect to db
373        if (is_array($db->sql_connect($dbhost, $dbuser, $dbpass, $dbname, $dbport, false, true)))
374        {
375            $db_error = $db->sql_error();
376            $errors[] = array(
377                'title' => 'INST_ERR_DB_CONNECT',
378                'description' => ($db_error['message']) ? utf8_convert_message($db_error['message']) : 'INST_ERR_DB_NO_ERROR',
379            );
380        }
381        else
382        {
383            // Check if there is any table name collisions
384            $temp_prefix = strtolower($table_prefix);
385            $table_ary = array(
386                $temp_prefix . 'attachments',
387                $temp_prefix . 'config',
388                $temp_prefix . 'sessions',
389                $temp_prefix . 'topics',
390                $temp_prefix . 'users',
391            );
392
393            $doctrine_db = connection_factory::get_connection_from_params($dbms, $dbhost, $dbuser, $dbpass, $dbname, $dbport);
394            $db_tools_factory = new \phpbb\db\tools\factory();
395            $db_tools = $db_tools_factory->get($doctrine_db);
396            $tables = $db_tools->sql_list_tables();
397            $tables = array_map('strtolower', $tables);
398            $table_intersect = array_intersect($tables, $table_ary);
399
400            if (count($table_intersect))
401            {
402                $errors[] = array(
403                    'title' => 'INST_ERR_PREFIX',
404                );
405            }
406
407            // Check if database version is supported
408            switch ($dbms)
409            {
410                case 'sqlite3':
411                    if (version_compare($db->sql_server_info(true), '3.6.15', '<'))
412                    {
413                        $errors[] = array(
414                            'title' => 'INST_ERR_DB_NO_SQLITE3',
415                        );
416                    }
417                break;
418                case 'oracle':
419                    $sql = "SELECT *
420                        FROM NLS_DATABASE_PARAMETERS
421                        WHERE PARAMETER = 'NLS_RDBMS_VERSION'
422                            OR PARAMETER = 'NLS_CHARACTERSET'";
423                    $result = $db->sql_query($sql);
424
425                    $stats = [];
426                    while ($row = $db->sql_fetchrow($result))
427                    {
428                        $stats[$row['parameter']] = $row['value'];
429                    }
430                    $db->sql_freeresult($result);
431
432                    if (version_compare($stats['NLS_RDBMS_VERSION'], '9.2', '<') && $stats['NLS_CHARACTERSET'] !== 'UTF8')
433                    {
434                        $errors[] = array(
435                            'title' => 'INST_ERR_DB_NO_ORACLE',
436                        );
437                    }
438                break;
439                case 'postgres':
440                    $sql = "SHOW server_encoding;";
441                    $result = $db->sql_query($sql);
442                    $row = $db->sql_fetchrow($result);
443                    $db->sql_freeresult($result);
444
445                    if ($row['server_encoding'] !== 'UNICODE' && $row['server_encoding'] !== 'UTF8')
446                    {
447                        $errors[] = array(
448                            'title' => 'INST_ERR_DB_NO_POSTGRES',
449                        );
450                    }
451                break;
452            }
453        }
454
455        return (empty($errors)) ? true : $errors;
456    }
457}