Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.33% covered (warning)
59.33%
229 / 386
44.00% covered (danger)
44.00%
22 / 50
CRAP
0.00% covered (danger)
0.00%
0 / 1
driver
59.33% covered (warning)
59.33%
229 / 386
44.00% covered (danger)
44.00%
22 / 50
2808.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 set_debug_load_time
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_debug_sql_explain
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_sql_layer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_db_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_any_char
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_one_char
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_db_connect_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_sql_error_triggered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_sql_error_sql
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_transaction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_sql_time
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_sql_error_returned
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_multi_insert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_multi_insert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_return_on_error
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 sql_num_queries
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 sql_add_num_queries
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 sql_close
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
5.03
 _sql_close
n/a
0 / 0
n/a
0 / 0
0
 sql_query_limit
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 _sql_query_limit
n/a
0 / 0
n/a
0 / 0
0
 sql_fetchrowset
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 sql_rowseek
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
10.37
 sql_fetchfield
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
8.38
 sql_like_expression
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 _sql_like_expression
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sql_not_like_expression
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 _sql_not_like_expression
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 sql_case
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 sql_concatenate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_buffer_nested_transactions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_transaction
82.76% covered (warning)
82.76%
24 / 29
0.00% covered (danger)
0.00%
0 / 1
11.62
 _sql_transaction
n/a
0 / 0
n/a
0 / 0
0
 sql_build_array
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
15.31
 sql_in_set
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
7.03
 sql_bit_and
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 sql_bit_or
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 cast_expr_to_bigint
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_nextid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cast_expr_to_string
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_lower_text
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sql_multi_insert
58.82% covered (warning)
58.82%
10 / 17
0.00% covered (danger)
0.00%
0 / 1
12.47
 _sql_validate_value
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 sql_build_query
61.90% covered (warning)
61.90%
26 / 42
0.00% covered (danger)
0.00%
0 / 1
45.38
 _sql_custom_build
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 _process_boolean_tree_first
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 _process_boolean_tree
82.61% covered (warning)
82.61%
38 / 46
0.00% covered (danger)
0.00%
0 / 1
19.70
 sql_error
60.87% covered (warning)
60.87%
14 / 23
0.00% covered (danger)
0.00%
0 / 1
20.63
 _sql_error
n/a
0 / 0
n/a
0 / 0
0
 sql_report
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
552
 _sql_report
n/a
0 / 0
n/a
0 / 0
0
 get_estimated_row_count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_row_count
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 clean_query_id
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
4.59
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* Database Abstraction Layer
18*/
19abstract class driver implements driver_interface
20{
21    var $db_connect_id;
22    var $query_result;
23    var $return_on_error = false;
24    var $transaction = false;
25    var $sql_time = 0;
26    var $num_queries = array();
27    var $open_queries = array();
28
29    var $curtime = 0;
30    var $query_hold = '';
31    var $html_hold = '';
32    var $sql_report = '';
33
34    /** @var string Last query text */
35    protected $last_query_text = '';
36
37    var $persistency = false;
38    var $user = '';
39    var $server = '';
40    var $dbname = '';
41
42    // Set to true if error triggered
43    var $sql_error_triggered = false;
44
45    // Holding the last sql query on sql error
46    var $sql_error_sql = '';
47    // Holding the error information - only populated if sql_error_triggered is set
48    var $sql_error_returned = array();
49
50    // Holding transaction count
51    var $transactions = 0;
52
53    // Supports multi inserts?
54    var $multi_insert = false;
55
56    /**
57    * Current sql layer
58    */
59    var $sql_layer = '';
60
61    /**
62    * Wildcards for matching any (%) or exactly one (_) character within LIKE expressions
63    */
64    var $any_char;
65    var $one_char;
66
67    /**
68    * Exact version of the DBAL, directly queried
69    */
70    var $sql_server_version = false;
71
72    const LOGICAL_OP = 0;
73    const STATEMENTS = 1;
74    const LEFT_STMT = 0;
75    const COMPARE_OP = 1;
76    const RIGHT_STMT = 2;
77    const SUBQUERY_OP = 3;
78    const SUBQUERY_SELECT_TYPE = 4;
79    const SUBQUERY_BUILD = 5;
80
81    /**
82    * @var bool
83    */
84    protected $debug_load_time = false;
85
86    /**
87    * @var bool
88    */
89    protected $debug_sql_explain = false;
90
91    /**
92    * Constructor
93    */
94    function __construct()
95    {
96        $this->num_queries = array(
97            'cached'    => 0,
98            'normal'    => 0,
99            'total'        => 0,
100        );
101
102        // Fill default sql layer based on the class being called.
103        // This can be changed by the specified layer itself later if needed.
104        $this->sql_layer = substr(get_class($this), strlen('phpbb\db\driver\\'));
105
106        // Do not change this please! This variable is used to easy the use of it - and is hardcoded.
107        $this->any_char = chr(0) . '%';
108        $this->one_char = chr(0) . '_';
109    }
110
111    /**
112    * {@inheritdoc}
113    */
114    public function set_debug_load_time($value)
115    {
116        $this->debug_load_time = $value;
117    }
118
119    /**
120    * {@inheritdoc}
121    */
122    public function set_debug_sql_explain($value)
123    {
124        $this->debug_sql_explain = $value;
125    }
126
127    /**
128    * {@inheritdoc}
129    */
130    public function get_sql_layer()
131    {
132        return $this->sql_layer;
133    }
134
135    /**
136    * {@inheritdoc}
137    */
138    public function get_db_name()
139    {
140        return $this->dbname;
141    }
142
143    /**
144    * {@inheritdoc}
145    */
146    public function get_any_char()
147    {
148        return $this->any_char;
149    }
150
151    /**
152    * {@inheritdoc}
153    */
154    public function get_one_char()
155    {
156        return $this->one_char;
157    }
158
159    /**
160    * {@inheritdoc}
161    */
162    public function get_db_connect_id()
163    {
164        return $this->db_connect_id;
165    }
166
167    /**
168    * {@inheritdoc}
169    */
170    public function get_sql_error_triggered()
171    {
172        return $this->sql_error_triggered;
173    }
174
175    /**
176    * {@inheritdoc}
177    */
178    public function get_sql_error_sql()
179    {
180        return $this->sql_error_sql;
181    }
182
183    /**
184    * {@inheritdoc}
185    */
186    public function get_transaction()
187    {
188        return $this->transaction;
189    }
190
191    /**
192    * {@inheritdoc}
193    */
194    public function get_sql_time()
195    {
196        return $this->sql_time;
197    }
198
199    /**
200    * {@inheritdoc}
201    */
202    public function get_sql_error_returned()
203    {
204        return $this->sql_error_returned;
205    }
206
207    /**
208    * {@inheritdoc}
209    */
210    public function get_multi_insert()
211    {
212        return $this->multi_insert;
213    }
214
215    /**
216    * {@inheritdoc}
217    */
218    public function set_multi_insert($multi_insert)
219    {
220        $this->multi_insert = $multi_insert;
221    }
222
223    /**
224    * {@inheritDoc}
225    */
226    function sql_return_on_error($fail = false)
227    {
228        $this->sql_error_triggered = false;
229        $this->sql_error_sql = '';
230
231        $this->return_on_error = $fail;
232    }
233
234    /**
235    * {@inheritDoc}
236    */
237    function sql_num_queries($cached = false)
238    {
239        return ($cached) ? $this->num_queries['cached'] : $this->num_queries['normal'];
240    }
241
242    /**
243    * {@inheritDoc}
244    */
245    function sql_add_num_queries($cached = false)
246    {
247        $this->num_queries['cached'] += ($cached !== false) ? 1 : 0;
248        $this->num_queries['normal'] += ($cached !== false) ? 0 : 1;
249        $this->num_queries['total'] += 1;
250    }
251
252    /**
253    * {@inheritDoc}
254    */
255    function sql_close()
256    {
257        if (!$this->db_connect_id)
258        {
259            return false;
260        }
261
262        if ($this->transaction)
263        {
264            do
265            {
266                $this->sql_transaction('commit');
267            }
268            while ($this->transaction);
269        }
270
271        foreach ($this->open_queries as $query_id)
272        {
273            $this->sql_freeresult($query_id);
274        }
275
276        // Connection closed correctly. Set db_connect_id to false to prevent errors
277        if ($result = $this->_sql_close())
278        {
279            $this->db_connect_id = false;
280        }
281
282        return $result;
283    }
284
285    /**
286     * Close sql connection
287     *
288     * @return    bool        False if failure
289     */
290    abstract protected function _sql_close(): bool;
291
292    /**
293    * {@inheritDoc}
294    */
295    function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0)
296    {
297        if (empty($query))
298        {
299            return false;
300        }
301
302        // Never use a negative total or offset
303        $total = ($total < 0) ? 0 : $total;
304        $offset = ($offset < 0) ? 0 : $offset;
305
306        return $this->_sql_query_limit($query, $total, $offset, $cache_ttl);
307    }
308
309    /**
310     * Build LIMIT query
311     *
312     * @param    string    $query        The SQL query to execute
313     * @param    int        $total        The number of rows to select
314     * @param    int        $offset
315     * @param    int        $cache_ttl    Either 0 to avoid caching or
316     *                the time in seconds which the result shall be kept in cache
317     * @return    mixed    Buffered, seekable result handle, false on error
318     */
319    abstract protected function _sql_query_limit(string $query, int $total, int $offset = 0, int $cache_ttl = 0);
320
321    /**
322    * {@inheritDoc}
323    */
324    function sql_fetchrowset($query_id = false)
325    {
326        if ($query_id === false)
327        {
328            $query_id = $this->query_result;
329        }
330
331        if ($query_id)
332        {
333            $result = array();
334            while ($row = $this->sql_fetchrow($query_id))
335            {
336                $result[] = $row;
337            }
338
339            return $result;
340        }
341
342        return false;
343    }
344
345    /**
346    * {@inheritDoc}
347    */
348    function sql_rowseek($rownum, &$query_id)
349    {
350        global $cache;
351
352        if ($query_id === false)
353        {
354            $query_id = $this->query_result;
355        }
356
357        if ($cache && $cache->sql_exists($query_id))
358        {
359            return $cache->sql_rowseek($rownum, $query_id);
360        }
361
362        if (!$query_id)
363        {
364            return false;
365        }
366
367        $this->sql_freeresult($query_id);
368        $query_id = $this->sql_query($this->last_query_text);
369
370        if (!$query_id)
371        {
372            return false;
373        }
374
375        // We do not fetch the row for rownum == 0 because then the next resultset would be the second row
376        for ($i = 0; $i < $rownum; $i++)
377        {
378            if (!$this->sql_fetchrow($query_id))
379            {
380                return false;
381            }
382        }
383
384        return true;
385    }
386
387    /**
388    * {@inheritDoc}
389    */
390    function sql_fetchfield($field, $rownum = false, &$query_id = false)
391    {
392        global $cache;
393
394        if ($query_id === false)
395        {
396            $query_id = $this->query_result;
397        }
398
399        if ($query_id)
400        {
401            if ($rownum !== false)
402            {
403                $this->sql_rowseek($rownum, $query_id);
404            }
405
406            if ($cache && !is_object($query_id) && $cache->sql_exists($query_id))
407            {
408                return $cache->sql_fetchfield($query_id, $field);
409            }
410
411            $row = $this->sql_fetchrow($query_id);
412            return (isset($row[$field])) ? $row[$field] : false;
413        }
414
415        return false;
416    }
417
418    /**
419    * {@inheritDoc}
420    */
421    function sql_like_expression($expression)
422    {
423        $expression = str_replace(array('_', '%'), array("\_", "\%"), $expression);
424        $expression = str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression);
425
426        return $this->_sql_like_expression('LIKE \'' . $this->sql_escape($expression) . '\'');
427    }
428
429    /**
430     * Build LIKE expression
431     *
432     * @param string $expression Base expression
433     *
434     * @return string LIKE expression
435     */
436    protected function _sql_like_expression(string $expression): string
437    {
438        return $expression;
439    }
440
441    /**
442    * {@inheritDoc}
443    */
444    function sql_not_like_expression($expression)
445    {
446        $expression = str_replace(array('_', '%'), array("\_", "\%"), $expression);
447        $expression = str_replace(array(chr(0) . "\_", chr(0) . "\%"), array('_', '%'), $expression);
448
449        return $this->_sql_not_like_expression('NOT LIKE \'' . $this->sql_escape($expression) . '\'');
450    }
451
452    /**
453     * Build NOT LIKE expression
454     *
455     * @param string $expression Base expression
456     *
457     * @return string NOT LIKE expression
458     */
459    protected function _sql_not_like_expression(string $expression): string
460    {
461        return $expression;
462    }
463
464    /**
465    * {@inheritDoc}
466    */
467    public function sql_case($condition, $action_true, $action_false = false)
468    {
469        $sql_case = 'CASE WHEN ' . $condition;
470        $sql_case .= ' THEN ' . $action_true;
471        $sql_case .= ($action_false !== false) ? ' ELSE ' . $action_false : '';
472        $sql_case .= ' END';
473        return $sql_case;
474    }
475
476    /**
477    * {@inheritDoc}
478    */
479    public function sql_concatenate($expr1, $expr2)
480    {
481        return $expr1 . ' || ' . $expr2;
482    }
483
484    /**
485    * {@inheritDoc}
486    */
487    function sql_buffer_nested_transactions()
488    {
489        return false;
490    }
491
492    /**
493    * {@inheritDoc}
494    */
495    function sql_transaction($status = 'begin')
496    {
497        switch ($status)
498        {
499            case 'begin':
500                // If we are within a transaction we will not open another one, but enclose the current one to not loose data (preventing auto commit)
501                if ($this->transaction)
502                {
503                    $this->transactions++;
504                    return true;
505                }
506
507                $result = $this->_sql_transaction('begin');
508
509                if (!$result)
510                {
511                    $this->sql_error();
512                }
513
514                $this->transaction = true;
515            break;
516
517            case 'commit':
518                // If there was a previously opened transaction we do not commit yet...
519                // but count back the number of inner transactions
520                if ($this->transaction && $this->transactions)
521                {
522                    $this->transactions--;
523                    return true;
524                }
525
526                // Check if there is a transaction (no transaction can happen if
527                // there was an error, with a combined rollback and error returning enabled)
528                // This implies we have transaction always set for autocommit db's
529                if (!$this->transaction)
530                {
531                    return false;
532                }
533
534                $result = $this->_sql_transaction('commit');
535
536                if (!$result)
537                {
538                    $this->sql_error();
539                }
540
541                $this->transaction = false;
542                $this->transactions = 0;
543            break;
544
545            case 'rollback':
546                $result = $this->_sql_transaction('rollback');
547                $this->transaction = false;
548                $this->transactions = 0;
549            break;
550
551            default:
552                $result = $this->_sql_transaction($status);
553            break;
554        }
555
556        return $result;
557    }
558
559    /**
560     * SQL Transaction
561     *
562     * @param string $status        Should be one of the following strings:
563     *                                begin, commit, rollback
564     *
565     * @return    bool    Success/failure of the transaction query
566     */
567    abstract protected function _sql_transaction(string $status = 'begin'): bool;
568
569    /**
570    * {@inheritDoc}
571    */
572    function sql_build_array($query, $assoc_ary = [])
573    {
574        if (!is_array($assoc_ary) || !count($assoc_ary))
575        {
576            return false;
577        }
578
579        $fields = $values = array();
580
581        if ($query == 'INSERT' || $query == 'INSERT_SELECT')
582        {
583            foreach ($assoc_ary as $key => $var)
584            {
585                $fields[] = $key;
586
587                if (is_array($var) && is_string($var[0]))
588                {
589                    // This is used for INSERT_SELECT(s)
590                    $values[] = $var[0];
591                }
592                else
593                {
594                    $values[] = $this->_sql_validate_value($var);
595                }
596            }
597
598            $query = ($query == 'INSERT') ? ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')' : ' (' . implode(', ', $fields) . ') SELECT ' . implode(', ', $values) . ' ';
599        }
600        else if ($query == 'MULTI_INSERT')
601        {
602            trigger_error('The MULTI_INSERT query value is no longer supported. Please use sql_multi_insert() instead.', E_USER_ERROR);
603        }
604        else if ($query == 'UPDATE' || $query == 'SELECT' || $query == 'DELETE')
605        {
606            $values = array();
607            foreach ($assoc_ary as $key => $var)
608            {
609                $values[] = "$key = " . $this->_sql_validate_value($var);
610            }
611            $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values);
612        }
613
614        return $query;
615    }
616
617    /**
618    * {@inheritDoc}
619    */
620    function sql_in_set($field, $array, $negate = false, $allow_empty_set = false)
621    {
622        $array = (array) $array;
623
624        if (!count($array))
625        {
626            if (!$allow_empty_set)
627            {
628                // Print the backtrace to help identifying the location of the problematic code
629                $this->sql_error('No values specified for SQL IN comparison');
630            }
631            else
632            {
633                // NOT IN () actually means everything so use a tautology
634                if ($negate)
635                {
636                    return '1=1';
637                }
638                // IN () actually means nothing so use a contradiction
639                else
640                {
641                    return '1=0';
642                }
643            }
644        }
645
646        if (count($array) == 1)
647        {
648            @reset($array);
649            $var = current($array);
650
651            return $field . ($negate ? ' <> ' : ' = ') . $this->_sql_validate_value($var);
652        }
653        else
654        {
655            return $field . ($negate ? ' NOT IN ' : ' IN ') . '(' . implode(', ', array_map(array($this, '_sql_validate_value'), $array)) . ')';
656        }
657    }
658
659    /**
660    * {@inheritDoc}
661    */
662    function sql_bit_and($column_name, $bit, $compare = '')
663    {
664        if (method_exists($this, '_sql_bit_and'))
665        {
666            return $this->_sql_bit_and($column_name, $bit, $compare);
667        }
668
669        return $column_name . ' & ' . (1 << $bit) . (($compare) ? ' ' . $compare : '');
670    }
671
672    /**
673    * {@inheritDoc}
674    */
675    function sql_bit_or($column_name, $bit, $compare = '')
676    {
677        if (method_exists($this, '_sql_bit_or'))
678        {
679            return $this->_sql_bit_or($column_name, $bit, $compare);
680        }
681
682        return $column_name . ' | ' . (1 << $bit) . (($compare) ? ' ' . $compare : '');
683    }
684
685    /**
686    * {@inheritDoc}
687    */
688    function cast_expr_to_bigint($expression)
689    {
690        return $expression;
691    }
692
693    /**
694     * {@inheritDoc}
695     */
696    public function sql_nextid()
697    {
698        return $this->sql_last_inserted_id();
699    }
700
701    /**
702    * {@inheritDoc}
703    */
704    function cast_expr_to_string($expression)
705    {
706        return $expression;
707    }
708
709    /**
710    * {@inheritDoc}
711    */
712    function sql_lower_text($column_name)
713    {
714        return "LOWER($column_name)";
715    }
716
717    /**
718    * {@inheritDoc}
719    */
720    function sql_multi_insert($table, $sql_ary)
721    {
722        if (!count($sql_ary))
723        {
724            return false;
725        }
726
727        $ary = array();
728        foreach ($sql_ary as $_sql_ary)
729        {
730            // If by accident the sql array is only one-dimensional we build a normal insert statement
731            if (!is_array($_sql_ary))
732            {
733                return $this->sql_query('INSERT INTO ' . $table . ' ' . $this->sql_build_array('INSERT', $sql_ary));
734            }
735
736            // Add values to set to $ary if multi insert is supported, otherwise run every insert query separately
737            if ($this->multi_insert)
738            {
739                $values = array();
740                foreach ($_sql_ary as $var)
741                {
742                    $values[] = $this->_sql_validate_value($var);
743                }
744                $ary[] = '(' . implode(', ', $values) . ')';
745            }
746            else
747            {
748                $result = $this->sql_query('INSERT INTO ' . $table . ' ' . $this->sql_build_array('INSERT', $_sql_ary));
749                if (!$result)
750                {
751                    return false;
752                }
753            }
754        }
755
756        if ($this->multi_insert)
757        {
758            return $this->sql_query('INSERT INTO ' . $table . ' ' . ' (' . implode(', ', array_keys($sql_ary[0])) . ') VALUES ' . implode(', ', $ary));
759        }
760
761        return true;
762    }
763
764    /**
765    * Function for validating values
766    * @access private
767    */
768    function _sql_validate_value($var)
769    {
770        if (is_null($var))
771        {
772            return 'NULL';
773        }
774        else if (is_string($var))
775        {
776            return "'" . $this->sql_escape($var) . "'";
777        }
778        else
779        {
780            return (is_bool($var)) ? intval($var) : $var;
781        }
782    }
783
784    /**
785    * {@inheritDoc}
786    */
787    function sql_build_query($query, $array)
788    {
789        $sql = '';
790        switch ($query)
791        {
792            case 'SELECT':
793            case 'SELECT_DISTINCT';
794
795                $sql = str_replace('_', ' ', $query) . ' ' . $array['SELECT'] . ' FROM ';
796
797                // Build table array. We also build an alias array for later checks.
798                $table_array = $aliases = array();
799                $used_multi_alias = false;
800
801                foreach ($array['FROM'] as $table_name => $alias)
802                {
803                    if (is_array($alias))
804                    {
805                        $used_multi_alias = true;
806
807                        foreach ($alias as $multi_alias)
808                        {
809                            $table_array[] = $table_name . ' ' . $multi_alias;
810                            $aliases[] = $multi_alias;
811                        }
812                    }
813                    else
814                    {
815                        $table_array[] = $table_name . ' ' . $alias;
816                        $aliases[] = $alias;
817                    }
818                }
819
820                // We run the following code to determine if we need to re-order the table array. ;)
821                // The reason for this is that for multi-aliased tables (two equal tables) in the FROM statement the last table need to match the first comparison.
822                // DBMS who rely on this: Oracle, PostgreSQL and MSSQL. For all other DBMS it makes absolutely no difference in which order the table is.
823                if (!empty($array['LEFT_JOIN']) && count($array['FROM']) > 1 && $used_multi_alias !== false)
824                {
825                    // Take first LEFT JOIN
826                    $join = current($array['LEFT_JOIN']);
827
828                    // Determine the table used there (even if there are more than one used, we only want to have one
829                    preg_match('/(' . implode('|', $aliases) . ')\.[^\s]+/U', str_replace(array('(', ')', 'AND', 'OR', ' '), '', $join['ON']), $matches);
830
831                    // If there is a first join match, we need to make sure the table order is correct
832                    if (!empty($matches[1]))
833                    {
834                        $first_join_match = trim($matches[1]);
835                        $table_array = $last = array();
836
837                        foreach ($array['FROM'] as $table_name => $alias)
838                        {
839                            if (is_array($alias))
840                            {
841                                foreach ($alias as $multi_alias)
842                                {
843                                    ($multi_alias === $first_join_match) ? $last[] = $table_name . ' ' . $multi_alias : $table_array[] = $table_name . ' ' . $multi_alias;
844                                }
845                            }
846                            else
847                            {
848                                ($alias === $first_join_match) ? $last[] = $table_name . ' ' . $alias : $table_array[] = $table_name . ' ' . $alias;
849                            }
850                        }
851
852                        $table_array = array_merge($table_array, $last);
853                    }
854                }
855
856                $sql .= $this->_sql_custom_build('FROM', implode(' CROSS JOIN ', $table_array));
857
858                if (!empty($array['LEFT_JOIN']))
859                {
860                    foreach ($array['LEFT_JOIN'] as $join)
861                    {
862                        $sql .= ' LEFT JOIN ' . key($join['FROM']) . ' ' . current($join['FROM']) . ' ON (' . $join['ON'] . ')';
863                    }
864                }
865
866                if (!empty($array['WHERE']))
867                {
868                    $sql .= ' WHERE ';
869
870                    if (is_array($array['WHERE']))
871                    {
872                        $sql_where = $this->_process_boolean_tree_first($array['WHERE']);
873                    }
874                    else
875                    {
876                        $sql_where = $array['WHERE'];
877                    }
878
879                    $sql .= $this->_sql_custom_build('WHERE', $sql_where);
880                }
881
882                if (!empty($array['GROUP_BY']))
883                {
884                    $sql .= ' GROUP BY ' . $array['GROUP_BY'];
885                }
886
887                if (!empty($array['ORDER_BY']))
888                {
889                    $sql .= ' ORDER BY ' . $array['ORDER_BY'];
890                }
891
892            break;
893        }
894
895        return $sql;
896    }
897
898    /**
899     * Build db-specific query data
900     *
901     * @param string $stage Query stage, can be 'FROM' or 'WHERE'
902     * @param string|array $data A string containing the CROSS JOIN query or an array of WHERE clauses
903     *
904     * @return string|array The db-specific query fragment
905     */
906    protected function _sql_custom_build(string $stage, $data)
907    {
908        return $data;
909    }
910
911    protected function _process_boolean_tree_first($operations_ary)
912    {
913        // In cases where an array exists but there is no head condition,
914        // it should be because there's only 1 WHERE clause. This seems the best way to deal with it.
915        if ($operations_ary[self::LOGICAL_OP] !== 'AND' &&
916            $operations_ary[self::LOGICAL_OP] !== 'OR')
917        {
918            $operations_ary = array('AND', array($operations_ary));
919        }
920        return $this->_process_boolean_tree($operations_ary) . "\n";
921    }
922
923    protected function _process_boolean_tree($operations_ary)
924    {
925        $operation = $operations_ary[self::LOGICAL_OP];
926
927        foreach ($operations_ary[self::STATEMENTS] as &$condition)
928        {
929            switch ($condition[self::LOGICAL_OP])
930            {
931                case 'AND':
932                case 'OR':
933
934                    $condition = ' ( ' . $this->_process_boolean_tree($condition) . ') ';
935
936                break;
937                case 'NOT':
938
939                    $condition = ' NOT (' . $this->_process_boolean_tree($condition) . ') ';
940
941                break;
942
943                default:
944
945                    switch (count($condition))
946                    {
947                        case 3:
948
949                            // Typical 3 element clause with {left hand} {operator} {right hand}
950                            switch ($condition[self::COMPARE_OP])
951                            {
952                                case 'IN':
953                                case 'NOT_IN':
954
955                                    // As this is used with an IN, assume it is a set of elements for sql_in_set()
956                                    $condition = $this->sql_in_set($condition[self::LEFT_STMT], $condition[self::RIGHT_STMT], $condition[self::COMPARE_OP] === 'NOT_IN', true);
957
958                                break;
959
960                                case 'LIKE':
961
962                                    $condition = $condition[self::LEFT_STMT] . ' ' . $this->sql_like_expression($condition[self::RIGHT_STMT]) . ' ';
963
964                                break;
965
966                                case 'NOT_LIKE':
967
968                                    $condition = $condition[self::LEFT_STMT] . ' ' . $this->sql_not_like_expression($condition[self::RIGHT_STMT]) . ' ';
969
970                                break;
971
972                                case 'IS_NOT':
973
974                                    $condition[self::COMPARE_OP] = 'IS NOT';
975
976                                // no break
977                                case 'IS':
978
979                                    // If the value is NULL, the string of it is the empty string ('') which is not the intended result.
980                                    // this should solve that
981                                    if ($condition[self::RIGHT_STMT] === null)
982                                    {
983                                        $condition[self::RIGHT_STMT] = 'NULL';
984                                    }
985
986                                    $condition = implode(' ', $condition);
987
988                                break;
989
990                                default:
991
992                                    $condition = implode(' ', $condition);
993
994                                break;
995                            }
996
997                        break;
998
999                        case 5:
1000
1001                            // Subquery with {left hand} {operator} {compare kind} {SELECT Kind } {Sub Query}
1002
1003                            $result = $condition[self::LEFT_STMT] . ' ' . $condition[self::COMPARE_OP] . ' ' . $condition[self::SUBQUERY_OP] . ' ( ';
1004                            $result .= $this->sql_build_query($condition[self::SUBQUERY_SELECT_TYPE], $condition[self::SUBQUERY_BUILD]);
1005                            $result .= ' )';
1006                            $condition = $result;
1007
1008                        break;
1009
1010                        default:
1011                            // This is an unpredicted clause setup. Just join all elements.
1012                            $condition = implode(' ', $condition);
1013
1014                        break;
1015                    }
1016
1017                break;
1018            }
1019
1020        }
1021
1022        if ($operation === 'NOT')
1023        {
1024            $operations_ary =  implode("", $operations_ary[self::STATEMENTS]);
1025        }
1026        else
1027        {
1028            $operations_ary = implode(" \n    $operation ", $operations_ary[self::STATEMENTS]);
1029        }
1030
1031        return $operations_ary;
1032    }
1033
1034
1035    /**
1036    * {@inheritDoc}
1037    */
1038    function sql_error($sql = '')
1039    {
1040        global $auth, $user, $config;
1041
1042        // Set var to retrieve errored status
1043        $this->sql_error_triggered = true;
1044        $this->sql_error_sql = $sql;
1045
1046        $this->sql_error_returned = $this->_sql_error();
1047
1048        if (!$this->return_on_error)
1049        {
1050            $message = 'SQL ERROR [ ' . $this->sql_layer . ' ]<br /><br />' . $this->sql_error_returned['message'] . ' [' . $this->sql_error_returned['code'] . ']';
1051
1052            // Show complete SQL error and path to administrators only
1053            // Additionally show complete error on installation or if extended debug mode is enabled
1054            // The DEBUG constant is for development only!
1055            if ((isset($auth) && $auth->acl_get('a_')) || defined('IN_INSTALL') || $this->debug_sql_explain)
1056            {
1057                $message .= ($sql) ? '<br /><br />SQL<br /><br />' . htmlspecialchars($sql, ENT_COMPAT) : '';
1058            }
1059            else
1060            {
1061                // If error occurs in initiating the session we need to use a pre-defined language string
1062                // This could happen if the connection could not be established for example (then we are not able to grab the default language)
1063                if (!isset($user->lang['SQL_ERROR_OCCURRED']))
1064                {
1065                    $message .= '<br /><br />An sql error occurred while fetching this page. Please contact an administrator if this problem persists.';
1066                }
1067                else
1068                {
1069                    if (!empty($config['board_contact']))
1070                    {
1071                        $message .= '<br /><br />' . sprintf($user->lang['SQL_ERROR_OCCURRED'], '<a href="mailto:' . htmlspecialchars($config['board_contact'], ENT_COMPAT) . '">', '</a>');
1072                    }
1073                    else
1074                    {
1075                        $message .= '<br /><br />' . sprintf($user->lang['SQL_ERROR_OCCURRED'], '', '');
1076                    }
1077                }
1078            }
1079
1080            if ($this->transaction)
1081            {
1082                $this->sql_transaction('rollback');
1083            }
1084
1085            if (strlen($message) > 1024)
1086            {
1087                // We need to define $msg_long_text here to circumvent text stripping.
1088                global $msg_long_text;
1089                $msg_long_text = $message;
1090
1091                trigger_error('', E_USER_ERROR);
1092            }
1093
1094            trigger_error($message, E_USER_ERROR);
1095        }
1096
1097        if ($this->transaction)
1098        {
1099            $this->sql_transaction('rollback');
1100        }
1101
1102        return $this->sql_error_returned;
1103    }
1104
1105    /**
1106     * Return sql error array
1107     *
1108     * @return array SQL error array with message and error code
1109     * @psalm-return array{message: string, code: int|string}
1110     */
1111    abstract protected function _sql_error(): array;
1112
1113    /**
1114    * {@inheritDoc}
1115    */
1116    function sql_report($mode, $query = '')
1117    {
1118        global $cache, $starttime, $phpbb_root_path, $phpbb_path_helper;
1119
1120        if (!$query && $this->query_hold != '')
1121        {
1122            $query = $this->query_hold;
1123        }
1124
1125        switch ($mode)
1126        {
1127            case 'display':
1128                if (!empty($cache))
1129                {
1130                    $cache->unload();
1131                }
1132                $this->sql_close();
1133
1134                $mtime = explode(' ', microtime());
1135                $totaltime = $mtime[0] + $mtime[1] - $starttime;
1136
1137                echo '<!DOCTYPE html>
1138                    <html dir="ltr">
1139                    <head>
1140                        <meta charset="utf-8">
1141                        <meta http-equiv="X-UA-Compatible" content="IE=edge">
1142                        <title>SQL Report</title>
1143                        <link href="' . htmlspecialchars($phpbb_path_helper->update_web_root_path($phpbb_root_path) . $phpbb_path_helper->get_adm_relative_path(), ENT_COMPAT) . 'style/admin.css" rel="stylesheet" type="text/css" media="screen" />
1144                    </head>
1145                    <body id="errorpage">
1146                    <div id="wrap">
1147                        <div id="page-header">
1148                            <a href="' . build_url('explain') . '">Return to previous page</a>
1149                        </div>
1150                        <div id="page-body">
1151                            <div id="acp">
1152                            <div class="panel">
1153                                <span class="corners-top"><span></span></span>
1154                                <div id="content">
1155                                    <h1>SQL Report</h1>
1156                                    <br />
1157                                    <p><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries['normal']} queries" . (($this->num_queries['cached']) ? " + {$this->num_queries['cached']} " . (($this->num_queries['cached'] == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></p>
1158
1159                                    <p>Time spent on ' . $this->sql_layer . ' queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></p>
1160
1161                                    <br /><br />
1162                                    ' . $this->sql_report . '
1163                                </div>
1164                                <span class="corners-bottom"><span></span></span>
1165                            </div>
1166                            </div>
1167                        </div>
1168                        <div id="page-footer">
1169                            Powered by <a href="https://www.phpbb.com/">phpBB</a>&reg; Forum Software &copy; phpBB Limited
1170                        </div>
1171                    </div>
1172                    </body>
1173                    </html>';
1174
1175                exit_handler();
1176
1177            break;
1178
1179            case 'stop':
1180                $endtime = explode(' ', microtime());
1181                $endtime = $endtime[0] + $endtime[1];
1182
1183                $this->sql_report .= '
1184
1185                    <table cellspacing="1">
1186                    <thead>
1187                    <tr>
1188                        <th>Query #' . $this->num_queries['total'] . '</th>
1189                    </tr>
1190                    </thead>
1191                    <tbody>
1192                    <tr>
1193                        <td class="row3"><textarea style="font-family:\'Courier New\',monospace;width:99%" rows="5" cols="10">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query), ENT_COMPAT)) . '</textarea></td>
1194                    </tr>
1195                    </tbody>
1196                    </table>
1197
1198                    ' . $this->html_hold . '
1199
1200                    <p style="text-align: center;">
1201                ';
1202
1203                if ($this->query_result)
1204                {
1205                    if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query))
1206                    {
1207                        $this->sql_report .= 'Affected rows: <b>' . $this->sql_affectedrows() . '</b> | ';
1208                    }
1209                    $this->sql_report .= 'Before: ' . sprintf('%.5f', $this->curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $this->curtime) . 's</b>';
1210                }
1211                else
1212                {
1213                    $error = $this->sql_error();
1214                    $this->sql_report .= '<b style="color: red">FAILED</b> - ' . $this->sql_layer . ' Error ' . $error['code'] . ': ' . htmlspecialchars($error['message'], ENT_COMPAT);
1215                }
1216
1217                $this->sql_report .= '</p><br /><br />';
1218
1219                $this->sql_time += $endtime - $this->curtime;
1220            break;
1221
1222            case 'start':
1223                $this->query_hold = $query;
1224                $this->html_hold = '';
1225
1226                $this->_sql_report($mode, $query);
1227
1228                $this->curtime = explode(' ', microtime());
1229                $this->curtime = $this->curtime[0] + $this->curtime[1];
1230
1231            break;
1232
1233            case 'add_select_row':
1234
1235                $html_table = func_get_arg(2);
1236                $row = func_get_arg(3);
1237
1238                if (!$html_table && count($row))
1239                {
1240                    $html_table = true;
1241                    $this->html_hold .= '<table cellspacing="1"><tr>';
1242
1243                    foreach (array_keys($row) as $val)
1244                    {
1245                        $this->html_hold .= '<th>' . (($val) ? ucwords(str_replace('_', ' ', $val)) : '&nbsp;') . '</th>';
1246                    }
1247                    $this->html_hold .= '</tr>';
1248                }
1249                $this->html_hold .= '<tr>';
1250
1251                $class = 'row1';
1252                foreach ($row as $val)
1253                {
1254                    $class = ($class == 'row1') ? 'row2' : 'row1';
1255                    $this->html_hold .= '<td class="' . $class . '">' . (($val) ? $val : '&nbsp;') . '</td>';
1256                }
1257                $this->html_hold .= '</tr>';
1258
1259                return $html_table;
1260
1261            break;
1262
1263            case 'fromcache':
1264
1265                $this->_sql_report($mode, $query);
1266
1267            break;
1268
1269            case 'record_fromcache':
1270
1271                $endtime = func_get_arg(2);
1272                $splittime = func_get_arg(3);
1273
1274                $time_cache = $endtime - $this->curtime;
1275                $time_db = $splittime - $endtime;
1276                $color = ($time_db > $time_cache) ? 'green' : 'red';
1277
1278                $this->sql_report .= '<table cellspacing="1"><thead><tr><th>Query results obtained from the cache</th></tr></thead><tbody><tr>';
1279                $this->sql_report .= '<td class="row3"><textarea style="font-family:\'Courier New\',monospace;width:99%" rows="5" cols="10">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query), ENT_COMPAT)) . '</textarea></td></tr></tbody></table>';
1280                $this->sql_report .= '<p style="text-align: center;">';
1281                $this->sql_report .= 'Before: ' . sprintf('%.5f', $this->curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p><br /><br />';
1282
1283                // Pad the start time to not interfere with page timing
1284                $starttime += $time_db;
1285
1286            break;
1287
1288            default:
1289
1290                $this->_sql_report($mode, $query);
1291
1292            break;
1293        }
1294
1295        return true;
1296    }
1297
1298    /**
1299     * Build db-specific report
1300     *
1301     * @param string $mode 'start' to add to report, 'fromcache' to output it
1302     * @param string $query Query to add to sql report
1303     *
1304     * @return void
1305     */
1306    abstract protected function _sql_report(string $mode, string $query = ''): void;
1307
1308    /**
1309    * {@inheritDoc}
1310    */
1311    function get_estimated_row_count($table_name)
1312    {
1313        return $this->get_row_count($table_name);
1314    }
1315
1316    /**
1317    * {@inheritDoc}
1318    */
1319    function get_row_count($table_name)
1320    {
1321        $sql = 'SELECT COUNT(*) AS rows_total
1322            FROM ' . $this->sql_escape($table_name);
1323        $result = $this->sql_query($sql);
1324        $rows_total = $this->sql_fetchfield('rows_total');
1325        $this->sql_freeresult($result);
1326
1327        return $rows_total;
1328    }
1329
1330    /**
1331     * {@inheritDoc}
1332     */
1333    public function clean_query_id(mixed $query_id): int|string|null
1334    {
1335        // Some DBMS functions accept/return objects and/or resources instead if identifiers
1336        // Attempting to use objects/resources as array keys will throw error, hence correctly handle all cases
1337        if (is_resource($query_id))
1338        {
1339            return function_exists('get_resource_id') ? get_resource_id($query_id) : (int) $query_id;
1340        }
1341        else
1342        {
1343            return is_object($query_id) ? spl_object_id($query_id) : $query_id;
1344        }
1345    }
1346}