Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.11% covered (danger)
5.11%
9 / 176
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
mysqli
5.11% covered (danger)
5.11%
9 / 176
0.00% covered (danger)
0.00%
0 / 14
7805.08
0.00% covered (danger)
0.00%
0 / 1
 sql_connect
7.89% covered (danger)
7.89%
3 / 38
0.00% covered (danger)
0.00%
0 / 1
242.81
 sql_server_info
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
 _sql_transaction
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 sql_query
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
240
 sql_affectedrows
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 sql_fetchrow
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 sql_rowseek
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 sql_last_inserted_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 sql_freeresult
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 sql_escape
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 _sql_error
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
5.02
 _sql_close
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 _sql_report
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
552
 sql_quote
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
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\db\driver;
15
16/**
17* MySQLi Database Abstraction Layer
18* mysqli-extension has to be compiled with:
19* MySQL 4.1+ or MySQL 5.0+
20*/
21class mysqli extends \phpbb\db\driver\mysql_base
22{
23    var $multi_insert = true;
24    var $connect_error = '';
25
26    /**
27    * {@inheritDoc}
28    */
29    function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false, $new_link = false)
30    {
31        if (!function_exists('mysqli_connect'))
32        {
33            $this->connect_error = 'mysqli_connect function does not exist, is mysqli extension installed?';
34            return $this->sql_error('');
35        }
36
37        $this->persistency = $persistency;
38        $this->user = $sqluser;
39
40        // If persistent connection, set dbhost to localhost when empty and prepend it with 'p:' prefix
41        $this->server = ($this->persistency) ? 'p:' . (($sqlserver) ? $sqlserver : 'localhost') : $sqlserver;
42
43        $this->dbname = $database;
44        $port = (!$port) ? null : $port;
45
46        // If port is set and it is not numeric, most likely mysqli socket is set.
47        // Try to map it to the $socket parameter.
48        $socket = null;
49        if ($port)
50        {
51            if (is_numeric($port))
52            {
53                $port = (int) $port;
54            }
55            else
56            {
57                $socket = $port;
58                $port = null;
59            }
60        }
61
62        if (!$this->db_connect_id = mysqli_init())
63        {
64            $this->connect_error = 'Failed to initialize MySQLi object.';
65
66        }
67        else if (!@mysqli_real_connect($this->db_connect_id, $this->server, $this->user, $sqlpassword, $this->dbname, $port, $socket, MYSQLI_CLIENT_FOUND_ROWS))
68        {
69            $this->connect_error = 'Failed to establish a connection to the MySQL database engine. Please ensure MySQL server is running and the database configuration parameters are correct.';
70        }
71
72        if (!$this->connect_error && $this->db_connect_id && $this->dbname != '')
73        {
74            // Disable loading local files on client side
75            @mysqli_options($this->db_connect_id, MYSQLI_OPT_LOCAL_INFILE, 0);
76
77            /*
78             * As of PHP 8.1 MySQLi default error mode is set to MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT
79             * See https://wiki.php.net/rfc/mysqli_default_errmode
80             * Since phpBB implements own SQL errors handling, explicitly set it back to MYSQLI_REPORT_OFF
81             */
82            mysqli_report(MYSQLI_REPORT_OFF);
83
84            @mysqli_query($this->db_connect_id, "SET NAMES 'utf8'");
85
86            // enforce strict mode on databases that support it
87            if (version_compare($this->sql_server_info(true), '5.0.2', '>='))
88            {
89                $result = @mysqli_query($this->db_connect_id, 'SELECT @@session.sql_mode AS sql_mode');
90                if ($result)
91                {
92                    $row = mysqli_fetch_assoc($result);
93                    mysqli_free_result($result);
94
95                    $modes = array_map('trim', explode(',', $row['sql_mode']));
96                }
97                else
98                {
99                    $modes = array();
100                }
101
102                // TRADITIONAL includes STRICT_ALL_TABLES and STRICT_TRANS_TABLES
103                if (!in_array('TRADITIONAL', $modes))
104                {
105                    if (!in_array('STRICT_ALL_TABLES', $modes))
106                    {
107                        $modes[] = 'STRICT_ALL_TABLES';
108                    }
109
110                    if (!in_array('STRICT_TRANS_TABLES', $modes))
111                    {
112                        $modes[] = 'STRICT_TRANS_TABLES';
113                    }
114                }
115
116                $mode = implode(',', $modes);
117                @mysqli_query($this->db_connect_id, "SET SESSION sql_mode='{$mode}'");
118            }
119            return $this->db_connect_id;
120        }
121
122        return $this->sql_error('');
123    }
124
125    /**
126    * {@inheritDoc}
127    */
128    function sql_server_info($raw = false, $use_cache = true)
129    {
130        global $cache;
131
132        if (!$use_cache || empty($cache) || ($this->sql_server_version = $cache->get('mysqli_version')) === false)
133        {
134            $result = @mysqli_query($this->db_connect_id, 'SELECT VERSION() AS version');
135            if ($result)
136            {
137                $row = mysqli_fetch_assoc($result);
138                mysqli_free_result($result);
139
140                $this->sql_server_version = $row['version'];
141
142                if (!empty($cache) && $use_cache)
143                {
144                    $cache->put('mysqli_version', $this->sql_server_version);
145                }
146            }
147        }
148
149        return ($raw) ? (string) $this->sql_server_version : 'MySQL(i) ' . $this->sql_server_version;
150    }
151
152    /**
153    * {@inheritDoc}
154    */
155    protected function _sql_transaction(string $status = 'begin'): bool
156    {
157        switch ($status)
158        {
159            case 'begin':
160                @mysqli_autocommit($this->db_connect_id, false);
161                $result = @mysqli_begin_transaction($this->db_connect_id);
162                return $result;
163
164            case 'commit':
165                $result = @mysqli_commit($this->db_connect_id);
166                @mysqli_autocommit($this->db_connect_id, true);
167                return $result;
168
169            case 'rollback':
170                $result = @mysqli_rollback($this->db_connect_id);
171                @mysqli_autocommit($this->db_connect_id, true);
172                return $result;
173        }
174
175        return true;
176    }
177
178    /**
179    * {@inheritDoc}
180    */
181    function sql_query($query = '', $cache_ttl = 0)
182    {
183        if ($query != '')
184        {
185            global $cache;
186
187            if ($this->debug_sql_explain)
188            {
189                $this->sql_report('start', $query);
190            }
191            else if ($this->debug_load_time)
192            {
193                $this->curtime = microtime(true);
194            }
195
196            $this->query_result = ($cache && $cache_ttl) ? $cache->sql_load($query) : false;
197            $this->sql_add_num_queries($this->query_result);
198
199            if ($this->query_result === false)
200            {
201                try
202                {
203                    $this->query_result = @mysqli_query($this->db_connect_id, $query);
204                }
205                catch (\Error $e)
206                {
207                    // Do nothing as SQL driver will report the error
208                }
209
210                if ($this->query_result === false)
211                {
212                    $this->sql_error($query);
213                }
214
215                if ($this->debug_sql_explain)
216                {
217                    $this->sql_report('stop', $query);
218                }
219                else if ($this->debug_load_time)
220                {
221                    $this->sql_time += microtime(true) - $this->curtime;
222                }
223
224                if (!$this->query_result)
225                {
226                    return false;
227                }
228
229                if ($cache && $cache_ttl)
230                {
231                    $this->query_result = $cache->sql_save($this, $query, $this->query_result, $cache_ttl);
232                }
233            }
234            else if ($this->debug_sql_explain)
235            {
236                $this->sql_report('fromcache', $query);
237            }
238        }
239        else
240        {
241            return false;
242        }
243
244        return $this->query_result;
245    }
246
247    /**
248    * {@inheritDoc}
249    */
250    function sql_affectedrows()
251    {
252        return ($this->db_connect_id) ? @mysqli_affected_rows($this->db_connect_id) : false;
253    }
254
255    /**
256    * {@inheritDoc}
257    */
258    function sql_fetchrow($query_id = false)
259    {
260        global $cache;
261
262        if ($query_id === false)
263        {
264            $query_id = $this->query_result;
265        }
266
267        $safe_query_id = $this->clean_query_id($query_id);
268        if ($cache && $cache->sql_exists($safe_query_id))
269        {
270            return $cache->sql_fetchrow($safe_query_id);
271        }
272
273        if ($query_id)
274        {
275            $result = mysqli_fetch_assoc($query_id);
276            return $result !== null ? $result : false;
277        }
278
279        return false;
280    }
281
282    /**
283    * {@inheritDoc}
284    */
285    function sql_rowseek($rownum, &$query_id)
286    {
287        global $cache;
288
289        if ($query_id === false)
290        {
291            $query_id = $this->query_result;
292        }
293
294        $safe_query_id = $this->clean_query_id($query_id);
295        if ($cache && $cache->sql_exists($safe_query_id))
296        {
297            return $cache->sql_rowseek($rownum, $safe_query_id);
298        }
299
300        return ($query_id) ? @mysqli_data_seek($query_id, $rownum) : false;
301    }
302
303    /**
304     * {@inheritdoc}
305     */
306    public function sql_last_inserted_id()
307    {
308        return ($this->db_connect_id) ? (int) @mysqli_insert_id($this->db_connect_id) : false;
309    }
310
311    /**
312    * {@inheritDoc}
313    */
314    function sql_freeresult($query_id = false)
315    {
316        global $cache;
317
318        if ($query_id === false)
319        {
320            $query_id = $this->query_result;
321        }
322
323        $safe_query_id = $this->clean_query_id($query_id);
324        if ($cache && $cache->sql_exists($safe_query_id))
325        {
326            $cache->sql_freeresult($safe_query_id);
327        }
328        else if ($query_id && $query_id !== true)
329        {
330            mysqli_free_result($query_id);
331        }
332    }
333
334    /**
335    * {@inheritDoc}
336    */
337    function sql_escape($msg)
338    {
339        return @mysqli_real_escape_string($this->db_connect_id, $msg);
340    }
341
342    /**
343    * {@inheritDoc}
344    */
345    protected function _sql_error(): array
346    {
347        if ($this->db_connect_id)
348        {
349            $error = [
350                'message'    => $this->db_connect_id->connect_error ?: $this->db_connect_id->error,
351                'code'        => $this->db_connect_id->connect_errno ?: $this->db_connect_id->errno,
352            ];
353        }
354        else
355        {
356            $error = [
357                'message'    => $this->connect_error,
358                'code'        => '',
359            ];
360        }
361
362        return $error;
363    }
364
365    /**
366     * {@inheritDoc}
367     */
368    protected function _sql_close(): bool
369    {
370        return @mysqli_close($this->db_connect_id);
371    }
372
373    /**
374    * {@inheritDoc}
375    */
376    protected function _sql_report(string $mode, string $query = ''): void
377    {
378        static $test_prof;
379
380        // current detection method, might just switch to see the existence of INFORMATION_SCHEMA.PROFILING
381        if ($test_prof === null)
382        {
383            $test_prof = false;
384            if (strpos(mysqli_get_server_info($this->db_connect_id), 'community') !== false)
385            {
386                $ver = mysqli_get_server_version($this->db_connect_id);
387                if ($ver >= 50037 && $ver < 50100)
388                {
389                    $test_prof = true;
390                }
391            }
392        }
393
394        switch ($mode)
395        {
396            case 'start':
397
398                $explain_query = $query;
399                if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m))
400                {
401                    $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2];
402                }
403                else if (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m))
404                {
405                    $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2];
406                }
407
408                if (preg_match('/^SELECT/', $explain_query))
409                {
410                    $html_table = false;
411
412                    // begin profiling
413                    if ($test_prof)
414                    {
415                        @mysqli_query($this->db_connect_id, 'SET profiling = 1;');
416                    }
417
418                    if ($result = @mysqli_query($this->db_connect_id, "EXPLAIN $explain_query"))
419                    {
420                        while ($row = mysqli_fetch_assoc($result))
421                        {
422                            $html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
423                        }
424                        mysqli_free_result($result);
425                    }
426
427                    if ($html_table)
428                    {
429                        $this->html_hold .= '</table>';
430                    }
431
432                    if ($test_prof)
433                    {
434                        $html_table = false;
435
436                        // get the last profile
437                        if ($result = @mysqli_query($this->db_connect_id, 'SHOW PROFILE ALL;'))
438                        {
439                            $this->html_hold .= '<br />';
440                            while ($row = mysqli_fetch_assoc($result))
441                            {
442                                // make <unknown> HTML safe
443                                if (!empty($row['Source_function']))
444                                {
445                                    $row['Source_function'] = str_replace(array('<', '>'), array('&lt;', '&gt;'), $row['Source_function']);
446                                }
447
448                                // remove unsupported features
449                                foreach ($row as $key => $val)
450                                {
451                                    if ($val === null)
452                                    {
453                                        unset($row[$key]);
454                                    }
455                                }
456                                $html_table = $this->sql_report('add_select_row', $query, $html_table, $row);
457                            }
458                            mysqli_free_result($result);
459                        }
460
461                        if ($html_table)
462                        {
463                            $this->html_hold .= '</table>';
464                        }
465
466                        @mysqli_query($this->db_connect_id, 'SET profiling = 0;');
467                    }
468                }
469
470            break;
471
472            case 'fromcache':
473                $endtime = explode(' ', microtime());
474                $endtime = $endtime[0] + $endtime[1];
475
476                $result = @mysqli_query($this->db_connect_id, $query);
477                if ($result)
478                {
479                    while ($void = mysqli_fetch_assoc($result))
480                    {
481                        // Take the time spent on parsing rows into account
482                    }
483                    mysqli_free_result($result);
484                }
485
486                $splittime = explode(' ', microtime());
487                $splittime = $splittime[0] + $splittime[1];
488
489                $this->sql_report('record_fromcache', $query, $endtime, $splittime);
490
491            break;
492        }
493    }
494
495    /**
496    * {@inheritDoc}
497    */
498    function sql_quote($msg)
499    {
500        return '`' . $msg . '`';
501    }
502}