AbstractQuery   F
last analyzed

Complexity

Total Complexity 128

Size/Duplication

Total Lines 1092
Duplicated Lines 0 %

Test Coverage

Coverage 66.29%

Importance

Changes 0
Metric Value
eloc 282
dl 0
loc 1092
ccs 175
cts 264
cp 0.6629
rs 2
c 0
b 0
f 0
wmc 128

43 Methods

Rating   Name   Duplication   Size   Complexity  
A _replaceStringCaseInsensitive() 0 3 1
A _preQueryDebug() 0 30 4
A setUnbuffered() 0 3 1
A query() 0 35 5
A connection() 0 4 1
A _replaceArrayStringCaseInsensitive() 0 18 4
A fetch_row() 0 9 2
A _postQueryDebug() 0 9 2
A affected_rows() 0 4 1
A free_result() 0 9 2
A _replaceArrayInt() 0 23 6
A _replaceStringCaseSensitive() 0 3 1
A _replaceString() 0 3 1
C replacement__callback() 0 54 12
A validConnection() 0 3 1
A fetch_assoc() 0 9 2
A error_backtrace() 0 19 2
A insert_id() 0 4 1
A _replaceInt() 0 8 3
C _doSanityCheck() 0 64 17
A quote() 0 17 2
A truncate() 0 5 1
A num_rows() 0 9 2
A _replaceArrayString() 0 18 4
A prepareInsert() 0 46 6
A supportMediumtext() 0 3 1
A data_seek() 0 4 1
A num_fields() 0 9 2
A setWannaSeeBoard() 0 3 1
A __construct() 0 8 2
A throwError() 0 28 5
A _replaceColumnCaseInsensitive() 0 3 1
A fetchQuery() 0 3 1
A _array_combine() 0 20 4
A _replaceDate() 0 8 2
A escape_wildcard_string() 0 16 2
A backtrace_message() 0 22 5
A _replaceIdentifier() 0 8 2
A skip_next_error() 0 3 1
A _replaceFloat() 0 8 2
B _prepareQuery() 0 27 10
A num_queries() 0 3 1
A setSeeBoard() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractQuery often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractQuery, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * This file provides an implementation of the most common functions needed
5
 * for the database drivers to work.
6
 *
7
 * @package   ElkArte Forum
8
 * @copyright ElkArte Forum contributors
9
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
10
 *
11
 * This file contains code covered by:
12
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
namespace ElkArte\Database;
19
20
use ElkArte\Debug;
21
use ElkArte\Errors\Errors;
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use ElkArte\Exceptions\Exception;
23
24
/**
25
 * Abstract database class, implements database to control functions
26
 */
27
abstract class AbstractQuery implements QueryInterface
28
{
29
	/** @var string Of course the character used to escape characters that have to be escaped */
30
	public const ESCAPE_CHAR = '\\';
31
32
	/** @var int Number of queries run (may include queries from $_SESSION if is a redirect) */
33
	protected $_query_count = 0;
34
35
	/** @var bool The way to skip a database error */
36
	protected $_skip_error = false;
37
38
	/** @var string String to match visible boards. Default is false, so that unless it is set, nothing is returned. */
39
	protected $query_see_board = '1!=1';
40
41
	/** @var String to match boards the user want to see. Default is false, so that unless it is set, nothing is returned. */
42
	protected $query_wanna_see_board = '1!=1';
43
44
	/** @var string String to match boards the user want to see.  Default is false, so that unless it is set, nothing is returned. */
45
	protected $ilike = '';
46
47
	/** @var string String that defines case insensitive not-like query operator */
48
	protected $not_ilike = '';
49
50
	/** @var string String that defines regular-expression-like query operator */
51
	protected $rlike = '';
52
53
	/** @var string String that defines regular-expression-not-like query operator */
54
	protected $not_rlike = '';
55
56
	/** @var bool MySQL supports unbuffered queries, this remembers if we are running an unbuffered or not */
57
	protected $_unbuffered = false;
58
59
	/** @var array This holds the "values" used in the replacement__callback method */
60
	protected $_db_callback_values = [];
61
62
	/** @var resource Holds the resource from the dBMS of the last query run */
63
	protected $_db_last_result;
64
65
	/** @var array Holds some values (time, file, line, delta) to debug performance of the queries. */
66
	protected $db_cache = [];
67
68
	/** @var Debug The debug object. */
69
	protected $_debug;
70
71
	/** @var AbstractResult Temporary variable to support the migration to the new db-layer.
72
	 * Ideally to be removed before 2.0 shipment */
73
	protected $result;
74
75
	/** @var string[] Comments that are allowed in a query are preg_removed. These replacements happen in the query checks. */
76
	protected $allowed_comments = [
77
		'from' => [
78
			'~\s+~s',
79
			'~/\*!40001 SQL_NO_CACHE \*/~',
80
			'~/\*!40000 USE INDEX \([A-Za-z\_]+?\) \*/~',
81
			'~/\*!40100 ON DUPLICATE KEY UPDATE id_msg = \d+ \*/~',
82
		],
83
		'to' => [
84
			' ',
85
			'',
86
			'',
87
			'',
88
		]
89
	];
90
91
	/**
92
	 * Constructor.
93
	 *
94
	 * @param string $_db_prefix Guess what? The tables prefix
95
	 * @param resource|object $connection Obviously the database connection
96
	 */
97
	public function __construct(protected $_db_prefix, protected $connection)
98
	{
99
		global $db_show_debug;
100
101
		// Debugging.
102
		if ($db_show_debug === true)
103
		{
104
			$this->_debug = Debug::instance();
105
		}
106
	}
107
108
	/**
109
	 * {@inheritDoc}
110
	 */
111
	abstract public function transaction($type = 'commit');
112
113
	/**
114
	 * {@inheritDoc}
115
	 */
116
	abstract public function last_error();
117
118
	/**
119
	 * Public setter for the string that defines which boards the user can see.
120
	 *
121
	 * @param string $string
122
	 */
123
	public function setSeeBoard($string)
124
	{
125
		$this->query_see_board = $string;
126
	}
127
128
	/**
129
	 * Public setter for the string that defines which boards the user want to see.
130
	 *
131
	 * @param string $string
132
	 */
133
	public function setWannaSeeBoard($string)
134
	{
135
		$this->query_wanna_see_board = $string;
136
	}
137 1
138
	/**
139 1
	 * {@inheritDoc}
140
	 */
141 1
	public function quote($db_string, $db_values)
142 1
	{
143
		// Only bother if there's something to replace.
144
		if (strpos($db_string, '{') !== false)
145 1
		{
146
			// This is needed by the callback function.
147
			$this->_db_callback_values = $db_values;
148
149 1
			// Do the quoting and escaping
150
			$db_string = preg_replace_callback('~{([a-z_]+)(?::([.a-zA-Z0-9_-]+))?}~',
151
				fn($matches) => $this->replacement__callback($matches), $db_string);
152
153
			// Clear this variables.
154
			$this->_db_callback_values = array();
155
		}
156
157
		return $db_string;
158
	}
159
160
	/**
161
	 * Callback for preg_replace_callback on the query.
162
	 *
163
	 * It allows to replace on the fly a few pre-defined strings, for convenience
164
	 * ('query_see_board', 'query_wanna_see_board'), with their current values from User::$info.
165
	 *
166 5
	 * In addition, it performs checks and sanitation on the values sent to the database.
167
	 *
168 5
	 * @param array $matches
169 5
	 *
170
	 * @return mixed|string
171
	 */
172
	public function replacement__callback($matches)
173
	{
174
		// Connection gone???  This should *never* happen at this point, yet it does :'(
175
		if (!$this->validConnection())
176 5
		{
177
			Errors::instance()->display_db_error('ElkArte\\Database\\AbstractQuery::replacement__callback');
178 5
		}
179 5
180
		switch ($matches[1])
181
		{
182
			case 'db_prefix':
183
				return $this->_db_prefix;
184 81
			case 'query_see_board':
185
				return $this->query_see_board;
186
			case 'query_wanna_see_board':
187 81
				return $this->query_wanna_see_board;
188
			case 'ilike':
189
				return $this->ilike;
190 81
			case 'not_ilike':
191
				return $this->not_ilike;
192
			case 'rlike':
193 81
				return $this->rlike;
194
			case 'not_rlike':
195 81
				return $this->not_rlike;
196 81
			case 'column_case_insensitive':
197
				return $this->_replaceColumnCaseInsensitive($matches[2]);
198
		}
199 81
200
		if (!isset($matches[2]))
201
		{
202 81
			$this->error_backtrace('Invalid value inserted or no type specified.', '', E_USER_ERROR, __FILE__, __LINE__);
203
		}
204
205
		if (!isset($this->_db_callback_values[$matches[2]]))
206
		{
207
			$this->error_backtrace("The database value you're trying to insert does not exist: " . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8'), '', E_USER_ERROR, __FILE__, __LINE__);
208
		}
209
210
		$replacement = $this->_db_callback_values[$matches[2]];
211
212
		return match ($matches[1])
213
		{
214
			'int' => $this->_replaceInt($matches[2], $replacement),
215
			'string', 'text' => $this->_replaceString($replacement),
216
			'string_case_sensitive' => $this->_replaceStringCaseSensitive($replacement),
217
			'string_case_insensitive' => $this->_replaceStringCaseInsensitive($replacement),
218 303
			'array_int' => $this->_replaceArrayInt($matches[2], $replacement),
219
			'array_string' => $this->_replaceArrayString($matches[2], $replacement),
220
			'array_string_case_insensitive' => $this->_replaceArrayStringCaseInsensitive($matches[2], $replacement),
221 303
			'date' => $this->_replaceDate($matches[2], $replacement),
222
			'float' => $this->_replaceFloat($matches[2], $replacement),
223
			'identifier' => $this->_replaceIdentifier($replacement),
224
			'raw' => $replacement,
225
			default => $this->error_backtrace('Undefined type used in the database query. (' . $matches[1] . ':' . $matches[2] . ')', '', false, __FILE__, __LINE__),
226 303
		};
227
	}
228 303
229 297
	/**
230 301
	 * Finds out if the connection is still valid.
231 23
	 *
232 301
	 * @return bool
233 2
	 */
234 301
	public function validConnection()
235 12
	{
236
		return (bool) $this->connection;
237
	}
238 301
239
	/**
240
	 * Casts the column to LOWER(column_name) for replacement__callback.
241
	 *
242
	 * @param mixed $replacement
243 301
	 * @return string
244
	 */
245
	protected function _replaceColumnCaseInsensitive($replacement)
246
	{
247
		return 'LOWER(' . $replacement . ')';
248 301
	}
249
250 301
	/**
251
	 * Scans the debug_backtrace output looking for the place where the
252 301
	 * actual error happened
253 275
	 *
254 137
	 * @return array
255 106
	 */
256 110
	protected function backtrace_message()
257 106
	{
258 2
		$log_message = '';
259 106
		$file = null;
260 10
		$line = null;
261 102
		foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $step)
262 69
		{
263 86
			// Found it?
264 43
			if (!method_exists($this, $step['function']) && !in_array(substr($step['function'], 0, 7), array('elk_db_', 'preg_re', 'db_erro', 'call_us')))
265 65
			{
266 2
				$log_message .= '<br />Function: ' . $step['function'];
267 63
				break;
268 3
			}
269 60
270 1
			if (isset($step['line']))
271 60
			{
272
				$file = $step['file'];
273 60
				$line = $step['line'];
274 60
			}
275
		}
276
277
		return [$file, $line, $log_message];
278
	}
279
280
	/**
281
	 * This function tries to work out additional error information from a back trace.
282
	 *
283
	 * @param string $error_message
284
	 * @param string $log_message
285
	 * @param string|bool $error_type
286
	 * @param string|null $file_fallback
287
	 * @param int|null $line_fallback
288
	 *
289
	 * @throws Exception
290
	 */
291
	protected function error_backtrace($error_message, $log_message = '', $error_type = false, $file_fallback = null, $line_fallback = null)
0 ignored issues
show
Unused Code introduced by
The parameter $error_type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

291
	protected function error_backtrace($error_message, $log_message = '', /** @scrutinizer ignore-unused */ $error_type = false, $file_fallback = null, $line_fallback = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
292
	{
293
		if (empty($log_message))
294
		{
295
			$log_message = $error_message;
296
		}
297
298
		// We'll try recovering the file and line number the original db query was called from.
299 6
		[$file, $line, $backtrace_message] = $this->backtrace_message();
300
301 6
		// Just in case nothing can be found from debug_backtrace
302
		$file = $file ?? $file_fallback;
303
		$line = $line ?? $line_fallback;
304
		$log_message .= $backtrace_message;
305
306
		// Is always a critical error.
307 37
		Errors::instance()->log_error($log_message, 'critical', $file, $line);
308
309 37
		throw new Exception([false, $error_message], false);
0 ignored issues
show
Bug introduced by
array(false, $error_message) of type array<integer,false|string> is incompatible with the type string|string[] expected by parameter $message of ElkArte\Exceptions\Exception::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
		throw new Exception(/** @scrutinizer ignore-type */ [false, $error_message], false);
Loading history...
310
	}
311
312
	/**
313
	 * Tests and casts integers for replacement__callback.
314 37
	 *
315
	 * @param mixed $identifier
316
	 * @param mixed $replacement
317 37
	 * @return string
318
	 * @throws Exception
319 37
	 */
320 37
	protected function _replaceInt($identifier, $replacement)
321
	{
322
		if (!is_numeric($replacement) || (string) $replacement !== (string) (int) $replacement)
323 37
		{
324
			$this->error_backtrace('Wrong value type sent to the database. Integer expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
325 37
		}
326 37
327
		return (string) (int) $replacement;
328
	}
329
330
	/**
331 37
	 * Casts values to string for replacement__callback.
332
	 *
333 37
	 * @param mixed $replacement
334
	 * @return string
335
	 */
336
	protected function _replaceString($replacement)
337
	{
338
		return sprintf('\'%1$s\'', $this->escape_string($replacement));
339
	}
340
341
	/**
342
	 * Escape string for the database input
343
	 *
344
	 * @param string $string
345
	 *
346
	 * @return string
347
	 */
348
	abstract public function escape_string($string);
349
350
	/**
351
	 * Casts values to string for replacement__callback and in the DBMS that
352
	 * require this solution makes it so that the comparison will be case sensitive.
353
	 *
354
	 * @param mixed $replacement
355
	 * @return string
356
	 */
357
	protected function _replaceStringCaseSensitive($replacement)
358
	{
359
		return $this->_replaceString($replacement);
360
	}
361
362
	/**
363
	 * Casts values to LOWER(string) for replacement__callback.
364
	 *
365
	 * @param mixed $replacement
366 275
	 * @return string
367
	 */
368 275
	protected function _replaceStringCaseInsensitive($replacement)
369
	{
370
		return 'LOWER(' . $this->_replaceString($replacement) . ')';
371
	}
372
373 275
	/**
374
	 * Tests and casts arrays of integers for replacement__callback.
375
	 *
376
	 * @param string $identifier
377
	 * @param mixed[] $replacement
378
	 * @return string
379
	 */
380
	protected function _replaceArrayInt($identifier, $replacement)
381
	{
382 114
		if (is_array($replacement))
0 ignored issues
show
introduced by
The condition is_array($replacement) is always true.
Loading history...
383
		{
384 114
			if (empty($replacement))
385
			{
386
				$this->error_backtrace('Database error, given array of integer values is empty. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
387
			}
388
389
			foreach ($replacement as $key => $value)
390
			{
391
				if (!is_numeric($value) || (string) $value !== (string) (int) $value)
392
				{
393
					$this->error_backtrace('Wrong value type sent to the database. Array of integers expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
394
				}
395
396
				$replacement[$key] = (string) (int) $value;
397
			}
398
399
			return implode(', ', $replacement);
400
		}
401
402
		$this->error_backtrace('Wrong value type sent to the database. Array of integers expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
403 1
	}
404
405 1
	/**
406
	 * Tests and casts arrays of strings for replacement__callback.
407
	 *
408
	 * @param string $identifier
409
	 * @param array $replacement
410
	 * @return string
411
	 */
412
	protected function _replaceArrayString($identifier, $replacement)
413
	{
414 5
		if (is_array($replacement))
0 ignored issues
show
introduced by
The condition is_array($replacement) is always true.
Loading history...
415
		{
416 5
			if (empty($replacement))
417
			{
418
				$this->error_backtrace('Database error, given array of string values is empty. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
419
			}
420
421
			foreach ($replacement as $key => $value)
422
			{
423
				$replacement[$key] = sprintf('\'%1$s\'', $this->escape_string($value));
424
			}
425
426
			return implode(', ', $replacement);
427 69
		}
428
429 69
		$this->error_backtrace('Wrong value type sent to the database. Array of strings expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
430
	}
431 69
432
	/**
433
	 * Tests and casts to LOWER(column_name) (if needed) arrays of strings
434
	 * for replacement__callback.
435
	 *
436 69
	 * @param string $identifier
437
	 * @param mixed[] $replacement
438 69
	 * @return string
439
	 */
440
	protected function _replaceArrayStringCaseInsensitive($identifier, $replacement)
441
	{
442
		if (is_array($replacement))
0 ignored issues
show
introduced by
The condition is_array($replacement) is always true.
Loading history...
443 69
		{
444
			if (empty($replacement))
445
			{
446 69
				$this->error_backtrace('Database error, given array of string values is empty. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
447
			}
448
449
			foreach ($replacement as $key => $value)
450
			{
451
				$replacement[$key] = $this->_replaceStringCaseInsensitive($value);
452
			}
453
454
			return implode(', ', $replacement);
455
		}
456
457
		$this->error_backtrace('Wrong value type sent to the database. Array of strings expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
458
	}
459
460
	/**
461
	 * Tests and casts date for replacement__callback.
462 43
	 *
463
	 * @param mixed $identifier
464 43
	 * @param mixed $replacement
465
	 * @return string
466 43
	 */
467
	protected function _replaceDate($identifier, $replacement)
468
	{
469
		if (preg_match('~^(\d{4})-([0-1]?\d)-([0-3]?\d)$~', $replacement, $date_matches) === 1)
470
		{
471 43
			return sprintf("'%04d-%02d-%02d'", $date_matches[1], $date_matches[2], $date_matches[3]);
472
		}
473 43
474
		$this->error_backtrace('Wrong value type sent to the database. Date expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
475
	}
476 43
477
	/**
478
	 * Tests and casts floating numbers for replacement__callback.
479
	 *
480
	 * @param mixed $identifier
481
	 * @param mixed $replacement
482
	 * @return string
483
	 */
484
	protected function _replaceFloat($identifier, $replacement)
485
	{
486
		if (!is_numeric($replacement))
487
		{
488
			$this->error_backtrace('Wrong value type sent to the database. Floating point number expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
489
		}
490
491
		return (string) (float) $replacement;
492
	}
493 2
494
	/**
495 2
	 * Quotes identifiers for replacement__callback.
496
	 *
497 2
	 * @param mixed $replacement
498
	 * @return string
499
	 */
500
	protected function _replaceIdentifier($replacement)
501
	{
502 2
		if (preg_match('~[a-z_][0-9a-zA-Z$,_]{0,60}~', $replacement) !== 1)
503
		{
504 2
			$this->error_backtrace('Wrong value type sent to the database. Invalid identifier used. (' . $replacement . ')', '', E_USER_ERROR, __FILE__, __LINE__);
505
		}
506
507 2
		return '`' . $replacement . '`';
508
	}
509
510
	/**
511
	 * {@inheritDoc}
512
	 */
513
	public function fetchQuery($db_string, $db_values = array())
514
	{
515
		return $this->query('', $db_string, $db_values);
516
	}
517
518
	/**
519
	 * {@inheritDoc}
520
	 */
521
	public function query($identifier, $db_string, $db_values = array())
522
	{
523 3
		// One more query....
524
		$this->_query_count++;
525 3
526
		$db_string = $this->initialChecks($db_string, $db_values, $identifier);
527 3
528
		if (trim($db_string) === '')
529
		{
530
			throw new \Exception('Query string empty');
531
		}
532
533
		$db_string = $this->_prepareQuery($db_string, $db_values);
534
535
		$this->_preQueryDebug($db_string);
536
537
		$this->_doSanityCheck($db_string);
538
539
		$this->executeQuery($db_string);
540
541
		if ($this->_db_last_result === false && !$this->_skip_error)
0 ignored issues
show
introduced by
The condition $this->_db_last_result === false is always false.
Loading history...
542
		{
543 1
			$this->_db_last_result = $this->error($db_string);
544
		}
545 1
546
		// Revert not to skip errors
547
		if ($this->_skip_error)
548
		{
549
			$this->_skip_error = false;
550 1
		}
551
552
		// Debugging.
553
		$this->_postQueryDebug();
554
555
		return $this->result;
556
	}
557
558
	/**
559
	 * Actually execute the DBMS-specific code to run the query
560
	 *
561
	 * @param string $db_string
562
	 */
563
	abstract protected function executeQuery($db_string);
564
565
	/**
566
	 * {@inheritDoc}
567
	 */
568
	abstract public function error($db_string);
569
570
	/**
571
	 * Prepares the strings to show the error to the user/admin and stop
572
	 * the code execution
573 277
	 *
574
	 * @param string $db_string
575 277
	 * @param string $query_error
576
	 * @param string $file
577
	 * @param int $line
578
	 * @throws Exception
579
	 */
580
	protected function throwError($db_string, $query_error, $file, $line)
581
	{
582
		global $context, $txt, $modSettings, $db_show_debug;
583
584
		// Nothing's defined yet... just die with it.
585
		if (empty($context) || empty($txt))
586
		{
587
			die($query_error);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
588
		}
589
590
		// Show an error message, if possible.
591
		$context['error_title'] = $txt['database_error'];
592
		$message = $txt['try_again'];
593
594
		// Add database version that we know of, for the admin to know. (and ask for support)
595
		if (allowedTo('admin_forum'))
596
		{
597
			$message = nl2br($query_error) . '<br />' . $txt['file'] . ': ' . $file . '<br />' . $txt['line'] . ': ' . $line .
598
				'<br /><br />' . sprintf($txt['database_error_versions'], $modSettings['elkVersion']);
599
600
			if ($db_show_debug === true)
601
			{
602
				$message .= '<br /><br />' . nl2br($db_string);
603
			}
604
		}
605
606
		// It's already been logged... don't log it again.
607
		throw new Exception($message, false);
608
	}
609
610
	/**
611
	 * {@inheritDoc}
612
	 */
613
	abstract public function insert($method, $table, $columns, $data, $keys, $disable_trans = false);
614
615
	/**
616
	 * Prepares the data that will be later implode'd into the actual query string
617
	 *
618
	 * @param string $table
619
	 * @param mixed[] $columns
620
	 * @param mixed[] $data
621
	 * @return mixed[]
622
	 * @throws \Exception
623
	 */
624
	protected function prepareInsert($table, $columns, $data)
625
	{
626
		// With nothing to insert, simply return.
627
		if (empty($data))
628
		{
629
			throw new \Exception('No data to insert');
630
		}
631
632
		// Inserting data as a single row can be done as a single array.
633
		if (!is_array($data[array_rand($data)]))
634 38
		{
635
			$data = [$data];
636 38
		}
637 38
638
		// Replace the prefix holder with the actual prefix.
639
		$table = str_replace('{db_prefix}', $this->_db_prefix, $table);
640
		$this->_skip_error = $table === $this->_db_prefix . 'log_errors';
641
642
		// Create the mold for a single row insert.
643
		$insertData = '(';
644
		foreach ($columns as $columnName => $type)
645
		{
646
			// Are we restricting the length?
647
			if (strpos($type, 'string-') !== false)
648
			{
649
				$insertData .= sprintf('SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $columnName);
650
			}
651
			else
652
			{
653
				$insertData .= sprintf('{%1$s:%2$s}, ', $type, $columnName);
654
			}
655
		}
656
657
		$insertData = substr($insertData, 0, -2) . ')';
658
659
		// Create an array consisting of only the columns.
660
		$indexed_columns = array_keys($columns);
661
662
		// Here's where the variables are injected to the query.
663
		$insertRows = [];
664
		foreach ($data as $dataRow)
665
		{
666
			$insertRows[] = $this->quote($insertData, $this->_array_combine($indexed_columns, $dataRow));
667
		}
668
669
		return [$table, $indexed_columns, $insertRows];
670
	}
671
672
	/**
673
	 * {@inheritDoc}
674
	 */
675
	abstract public function replace($table, $columns, $data, $keys, $disable_trans = false);
676
677
	/**
678
	 * {@inheritDoc}
679
	 */
680
	public function escape_wildcard_string($string, $translate_human_wildcards = false)
681
	{
682
		$replacements = [
683
			'%' => '\%',
684
			'_' => '\_',
685
			'\\' => '\\\\',
686
		];
687
688
		if ($translate_human_wildcards)
689
		{
690
			$replacements += [
691
				'*' => '%',
692
			];
693
		}
694
695
		return strtr($string, $replacements);
696
	}
697
698 6
	/**
699
	 * {@inheritDoc}
700
	 */
701 6
	public function connection()
702
	{
703
		// find it, find it
704
		return $this->connection;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->connection also could return the type object which is incompatible with the return type mandated by ElkArte\Database\QueryInterface::connection() of resource.
Loading history...
705
	}
706
707 6
	/**
708
	 * {@inheritDoc}
709
	 */
710
	public function num_queries()
711
	{
712
		return $this->_query_count;
713
	}
714
715
	/**
716
	 * {@inheritDoc}
717
	 */
718
	public function skip_next_error()
719
	{
720
		$this->_skip_error = true;
721
	}
722
723
	/**
724
	 * {@inheritDoc}
725
	 */
726
	public function truncate($table)
727
	{
728
		return $this->fetchQuery('
729
			TRUNCATE ' . $table,
730
			[]
731
		);
732
	}
733
734 6
	/**
735
	 * Set the unbuffered state for the connection
736
	 *
737 6
	 * @param bool $state
738
	 */
739
	public function setUnbuffered($state)
740
	{
741
		$this->_unbuffered = (bool) $state;
742
	}
743 6
744
	/**
745
	 *  Get the version number.
746
	 *
747
	 * @return string - the version
748
	 * @throws Exception
749
	 */
750
	abstract public function client_version();
751
752
	/**
753
	 * Return server info.
754
	 *
755
	 * @return string
756
	 */
757
	abstract public function server_info();
758
759
	/**
760
	 * Whether the database system is case sensitive.
761
	 *
762
	 * @return bool
763
	 */
764
	abstract public function case_sensitive();
765
766
	/**
767
	 * Get the name (title) of the database system.
768
	 *
769
	 * @return string
770
	 */
771
	abstract public function title();
772
773
	/**
774
	 * Returns whether the database system supports ignore.
775
	 *
776
	 * @return false
777
	 */
778
	abstract public function support_ignore();
779
780
	/**
781
	 * Get the version number.
782
	 *
783
	 * @return string - the version
784
	 * @throws Exception
785
	 */
786
	abstract public function server_version();
787
788
	/**
789
	 * Temporary function to support migration to the new schema of the db layer
790
	 *
791
	 * @deprecated since 2.0
792
	 */
793
	public function fetch_row($result)
794
	{
795
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::fetch_row()', 'Result::fetch_row()');
796
		if ($result === false)
797
		{
798
			return false;
799 19
		}
800
801
		return $result->fetch_row();
802 19
	}
803
804
	/**
805
	 * Temporary function to support migration to the new schema of the db layer
806
	 *
807
	 * @deprecated since 2.0
808
	 */
809
	public function fetch_assoc($result)
810
	{
811
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::fetch_assoc()', 'Result::fetch_assoc()');
812
		if ($result === false)
813
		{
814
			return false;
815
		}
816
817
		return $result->fetch_assoc();
818
	}
819
820
	/**
821
	 * Temporary function to support migration to the new schema of the db layer
822
	 *
823
	 * @deprecated since 2.0
824
	 */
825
	public function free_result($result)
826
	{
827
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::free_result()', 'Result::free_result()');
828
		if ($result === false)
829
		{
830
			return;
831
		}
832
833
		return $result->free_result();
834
	}
835
836
	/**
837
	 * Temporary function to support migration to the new schema of the db layer
838
	 *
839
	 * @deprecated since 2.0
840
	 */
841 79
	public function affected_rows()
842
	{
843 79
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::affected_rows()', 'Result::affected_rows()');
844
		return $this->result->affected_rows();
845 79
	}
846
847 79
	/**
848
	 * Temporary function to support migration to the new schema of the db layer
849
	 *
850
	 * @deprecated since 2.0
851 30
	 */
852 30
	public function num_rows($result)
853
	{
854 30
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::num_rows()', 'Result::num_rows()');
855
		if ($result === false)
856 30
		{
857
			return 0;
858
		}
859
860
		return (int) $result->num_rows();
861 30
	}
862
863
	/**
864
	 * Temporary function to support migration to the new schema of the db layer
865
	 *
866
	 * @deprecated since 2.0
867
	 */
868
	public function num_fields($result)
869
	{
870
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::num_fields()', 'Result::num_fields()');
871
		if ($result === false)
872
		{
873
			return 0;
874
		}
875 301
876
		return $result->num_fields();
877 301
	}
878
879 301
	/**
880
	 * Temporary function to support migration to the new schema of the db layer
881 37
	 *
882
	 * @deprecated since 2.0
883
	 */
884 301
	public function insert_id($table)
885
	{
886
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::insert_id()', 'Result::insert_id()');
887 299
		return $this->result->insert_id();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->result->insert_id() also could return the type string which is incompatible with the return type mandated by ElkArte\Database\QueryInterface::insert_id() of boolean|integer.
Loading history...
888
	}
889
890 299
	/**
891 299
	 * Temporary function to support migration to the new schema of the db layer
892
	 *
893 299
	 * @deprecated since 2.0
894
	 */
895 299
	public function data_seek($result, $counter)
896 299
	{
897
		// \ElkArte\Errors\Errors::instance()->log_deprecated('Query::data_seek()', 'Result::data_seek()');
898
		return $result->data_seek($counter);
899
	}
900 299
901
	/**
902
	 * Temporary function: I'm not sure this is the best place to have it, though it was
903 301
	 * convenient while fixing other issues.
904
	 *
905
	 * @deprecated since 2.0
906
	 */
907
	public function supportMediumtext()
908
	{
909
		return false;
910
	}
911
912 301
	/**
913
	 * Temporary function to support migration to the new schema of the db layer
914 301
	 *
915
	 * @deprecated since 2.0
916
	 */
917 301
	abstract public function list_tables($db_name_str = false, $filter = false);
918
919
	/**
920
	 * This function combines the keys and values of the data passed to db::insert.
921
	 *
922
	 * @param int[] $keys
923
	 * @param array $values
924
	 * @return array
925
	 */
926
	protected function _array_combine($keys, $values)
927
	{
928
		$is_numeric = array_filter(array_keys($values), 'is_numeric');
929
930
		if (!empty($is_numeric))
931
		{
932
			return array_combine($keys, $values);
933
		}
934
935
		$combined = [];
936
		foreach ($keys as $key)
937
		{
938
			if (isset($values[$key]))
939 301
			{
940
				$combined[$key] = $values[$key];
941
			}
942
		}
943
944 301
		// @todo should throw an E_WARNING if count($combined) != count($keys)
945
		return $combined;
946 301
	}
947
948 301
	/**
949
	 * Checks for "illegal characters" and runs replacement__callback if not
950
	 * overridden.
951
	 * In case of problems, the method can ends up dying.
952
	 *
953
	 * @param string $db_string
954 301
	 * @param mixed $db_values
955
	 * @return string
956
	 */
957
	protected function _prepareQuery($db_string, $db_values)
958
	{
959
		global $modSettings;
960
961
		if (empty($modSettings['disableQueryCheck']) && empty($db_values['security_override']) && strpos($db_string, "'") !== false)
962
		{
963
			$this->error_backtrace('Hacking attempt...', "Illegal character (') used in query...", true, __FILE__, __LINE__);
964 301
		}
965
966 301
		if (empty($db_values['security_override']) && (!empty($db_values) || strpos($db_string, '{db_prefix}') !== false))
967
		{
968
			// Store these values for use in the callback function.
969 301
			$this->_db_callback_values = $db_values;
970 301
971
			// Inject the values passed to this function.
972 301
			$count = -1;
973 301
			while (($count > 0 && isset($db_values['recursive'])) || $count === -1)
974 301
			{
975
				$db_string = preg_replace_callback('~{([a-z_]+)(?::([.a-zA-Z0-9_-]+))?}~',
976 301
					fn($matches) => $this->replacement__callback($matches), $db_string, -1, $count);
977 301
			}
978
979 301
			// No need for them any longer.
980
			$this->_db_callback_values = array();
981 122
		}
982
983 122
		return $db_string;
984
	}
985 122
986 122
	/**
987
	 * Some initial checks and replacement of text insside the query string
988 122
	 *
989
	 * @param string $db_string
990
	 * @param mixed $db_values
991
	 * @param string $identifier The old (now mostly unused) query identifier
992 122
	 * @return string
993
	 */
994 122
	abstract protected function initialChecks($db_string, $db_values, $identifier = '');
995 122
996
	/**
997
	 * Tracks the initial status (time, file/line, query) for performance evaluation.
998 10
	 *
999
	 * @param string $db_string
1000
	 */
1001 122
	protected function _preQueryDebug($db_string)
1002 122
	{
1003
		global $db_show_debug, $time_start;
1004
1005 301
		// Debugging.
1006 301
		if ($db_show_debug === true)
1007
		{
1008
			// We'll try recovering the file and line number the original db query was called from.
1009 301
			[$file, $line] = $this->backtrace_message();
1010
1011
			// Just in case nothing can be found from debug_backtrace
1012
			$file = $file ?? __FILE__;
1013
			$line = $line ?? __LINE__;
1014 301
1015
			if (!empty($_SESSION['debug_redirect']))
1016
			{
1017
				$this->_debug->merge_db($_SESSION['debug_redirect']);
1018 301
				// @todo this may be off by 1
1019
				$this->_query_count += count($_SESSION['debug_redirect']);
1020
				$_SESSION['debug_redirect'] = array();
1021
			}
1022
1023 301
			// Don't overload it.
1024
			$st = microtime(true);
1025
			$this->db_cache = [];
1026
			$this->db_cache['q'] = $this->_query_count < 50 ? $db_string : '...';
1027
			$this->db_cache['f'] = $file;
1028 301
			$this->db_cache['l'] = $line;
1029
			$this->db_cache['s'] = $st - $time_start;
1030
			$this->db_cache['st'] = $st;
1031
		}
1032
	}
1033
1034
	/**
1035
	 * Closes up the tracking and stores everything in the debug class.
1036
	 */
1037
	protected function _postQueryDebug()
1038
	{
1039
		global $db_show_debug;
1040
1041
		if ($db_show_debug === true)
1042
		{
1043
			$this->db_cache['t'] = microtime(true) - $this->db_cache['st'];
1044
			$this->_debug->db_query($this->db_cache);
1045
			$this->db_cache = [];
1046
		}
1047
	}
1048
1049
	/**
1050
	 * Checks the query doesn't have nasty stuff in it.
1051
	 * In case of problems, the method can ends up dying.
1052
	 *
1053
	 * @param string $db_string
1054
	 */
1055
	protected function _doSanityCheck($db_string)
1056
	{
1057
		global $modSettings;
1058
1059
		// First, we clean strings out of the query, reduce whitespace, lowercase, and trim - so we can check it over.
1060
		$clean = '';
1061
		if (empty($modSettings['disableQueryCheck']))
1062
		{
1063
			$old_pos = 0;
1064
			$pos = -1;
1065
			while (true)
1066
			{
1067
				$pos = strpos($db_string, "'", $pos + 1);
1068
				if ($pos === false)
1069
				{
1070
					break;
1071
				}
1072
1073
				$clean .= substr($db_string, $old_pos, $pos - $old_pos);
1074
1075
				while (true)
1076
				{
1077
					$pos1 = strpos($db_string, "'", $pos + 1);
1078
					$pos2 = strpos($db_string, static::ESCAPE_CHAR, $pos + 1);
1079
1080
					if ($pos1 === false)
1081
					{
1082
						break;
1083
					}
1084
1085
					if ($pos2 === false || $pos2 > $pos1)
1086
					{
1087
						$pos = $pos1;
1088
						break;
1089
					}
1090
1091
					$pos = $pos2 + 1;
1092
				}
1093
1094
				$clean .= ' %s ';
1095
				$old_pos = $pos + 1;
1096
			}
1097
1098
			$clean .= substr($db_string, $old_pos);
1099
			$clean = strtolower(trim(preg_replace($this->allowed_comments['from'], $this->allowed_comments['to'], $clean)));
1100
1101
			// Comments?  We don't use comments in our queries, we leave 'em outside!
1102
			if (strpos($clean, '/*') > 2 || strpos($clean, '--') !== false || strpos($clean, ';') !== false)
1103
			{
1104
				$fail = true;
1105
			}
1106
			// Trying to change passwords, slow us down, or something?
1107
			elseif (strpos($clean, 'sleep') !== false && preg_match('~(^|[^a-z])sleep($|[^[_a-z])~', $clean) != 0)
1108
			{
1109
				$fail = true;
1110
			}
1111
			elseif (strpos($clean, 'benchmark') !== false && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~', $clean) != 0)
1112
			{
1113
				$fail = true;
1114
			}
1115
1116
			if (!empty($fail) && class_exists(\ElkArte\Errors\Errors::class))
1117
			{
1118
				$this->error_backtrace('Hacking attempt...', 'Hacking attempt...' . "\n" . $db_string, E_USER_ERROR, __FILE__, __LINE__);
1119
			}
1120
		}
1121
	}
1122
}
1123