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