Passed
Pull Request — development (#3445)
by Emanuele
06:45
created

AbstractQuery::query()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 35
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 5
nop 3
dl 0
loc 35
ccs 0
cts 3
cp 0
crap 30
rs 9.4888
c 0
b 0
f 0
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;
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
	/**
30
	 * Of course the character used to escape characters that have to be escaped
31
	 *
32
	 * @var string
33
	 */
34
	const ESCAPE_CHAR = '\\';
35
36
	/**
37
	 * Current connection to the database
38
	 *
39
	 * @var \ElkArte\Database\ConnectionInterface
40
	 */
41
	protected $connection = null;
42
43
	/**
44
	 * Number of queries run (may include queries from $_SESSION if is a redirect)
45
	 *
46
	 * @var int
47
	 */
48
	protected $_query_count = 0;
49
50
	/**
51
	 * The way to skip a database error
52
	 *
53
	 * @var bool
54
	 */
55
	protected $_skip_error = false;
56
57
	/**
58
	 * The tables prefix
59
	 *
60
	 * @var string
61
	 */
62
	protected $_db_prefix = '';
63
64
	/**
65
	 * String to match visible boards.
66
	 * By default set to a false, so that unless it is set, nothing is returned.
67
	 *
68
	 * @var string
69
	 */
70
	protected $query_see_board = '1!=1';
71
72
	/**
73
	 * String to match boards the user want to see.
74
	 * By default set to a false, so that unless it is set, nothing is returned.
75
	 *
76
	 * @var string
77
	 */
78
	protected $query_wanna_see_board = '1!=1';
79
80
	/**
81
	 * String that defines case insensitive like query operator
82
	 *
83
	 * @var string
84
	 */
85
	protected $ilike = '';
86
87
	/**
88
	 * String that defines case insensitive not-like query operator
89
	 *
90
	 * @var string
91
	 */
92
	protected $not_ilike = '';
93
94
	/**
95
	 * String that defines regular-expression-like query operator
96
	 *
97
	 * @var string
98
	 */
99
	protected $rlike = '';
100
101
	/**
102
	 * String that defines regular-expression-not-like query operator
103
	 *
104
	 * @var string
105
	 */
106
	protected $not_rlike = '';
107
108
	/**
109
	 * MySQL supports unbuffered queries, this remembers if we are running an
110
	 * unbuffered or not
111
	 *
112
	 * @var bool
113
	 */
114
	protected $_unbuffered = false;
115
116
	/**
117
	 * This holds the "values" used in the replacement__callback method
118
	 *
119
	 * @var array
120
	 */
121
	protected $_db_callback_values = array();
122
123
	/**
124
	 * Temporary variable to support the migration to the new db-layer
125
	 * Ideally to be removed before 2.0 shipment
126
	 *
127
	 * @var \ElkArte\Database\AbstractResult
128
	 */
129
	protected $result = null;
130
131
	/**
132
	 * Holds the resource from the dBMS of the last query run
133
	 *
134
	 * @var resource
135
	 */
136
	protected $_db_last_result = null;
137 1
138
	/**
139 1
	 * Comments that are allowed in a query are preg_removed.
140
	 * These replacements happen in the query checks.
141 1
	 *
142 1
	 * @var string[]
143
	 */
144
	protected $allowed_comments = [
145 1
		'from' => [
146
			'~\s+~s',
147
			'~/\*!40001 SQL_NO_CACHE \*/~',
148
			'~/\*!40000 USE INDEX \([A-Za-z\_]+?\) \*/~',
149 1
			'~/\*!40100 ON DUPLICATE KEY UPDATE id_msg = \d+ \*/~',
150
		],
151
		'to' => [
152
			' ',
153
			'',
154
			'',
155
			'',
156
		]
157
	];
158
159
	/**
160
	 * Holds some values (time, file, line, delta) to debug performance of the queries.
161
	 *
162
	 * @var mixed[]
163
	 */
164
	protected $db_cache = [];
165
166 5
	/**
167
	 * The debug object.
168 5
	 *
169 5
	 * @var \ElkArte\Debug
170
	 */
171
	protected $_debug = null;
172
173
	/**
174
	 * Constructor.
175
	 *
176 5
	 * @param string $db_prefix Guess what? The tables prefix
177
	 * @param resource|object $connection Obviously the database connection
178 5
	 */
179 5
	public function __construct($db_prefix, $connection)
180
	{
181
		global $db_show_debug;
182
183
		$this->_db_prefix = $db_prefix;
184 81
		$this->connection = $connection;
0 ignored issues
show
Documentation Bug introduced by
It seems like $connection can also be of type resource. However, the property $connection is declared as type ElkArte\Database\ConnectionInterface. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
185
186
		// Debugging.
187 81
		if ($db_show_debug === true)
188
		{
189
			$this->_debug = Debug::instance();
190 81
		}
191
	}
192
193 81
	/**
194
	 * {@inheritDoc}
195 81
	 */
196 81
	abstract public function transaction($type = 'commit');
197
198
	/**
199 81
	 * {@inheritDoc}
200
	 */
201
	abstract public function last_error();
202 81
203
	/**
204
	 * Public setter for the string that defines which boards the user can see.
205
	 *
206
	 * @param string $string
207
	 */
208
	public function setSeeBoard($string)
209
	{
210
		$this->query_see_board = $string;
211
	}
212
213
	/**
214
	 * Public setter for the string that defines which boards the user want to see.
215
	 *
216
	 * @param string $string
217
	 */
218 303
	public function setWannaSeeBoard($string)
219
	{
220
		$this->query_wanna_see_board = $string;
221 303
	}
222
223
	/**
224
	 * {@inheritDoc}
225
	 */
226 303
	public function quote($db_string, $db_values)
227
	{
228 303
		// Only bother if there's something to replace.
229 297
		if (strpos($db_string, '{') !== false)
230 301
		{
231 23
			// This is needed by the callback function.
232 301
			$this->_db_callback_values = $db_values;
233 2
234 301
			// Do the quoting and escaping
235 12
			$db_string = preg_replace_callback('~{([a-z_]+)(?::([\.a-zA-Z0-9_-]+))?}~',
236
				function ($matches) {
237
					return $this->replacement__callback($matches);
238 301
				}, $db_string);
239
240
			// Clear this variables.
241
			$this->_db_callback_values = array();
242
		}
243 301
244
		return $db_string;
245
	}
246
247
	/**
248 301
	 * Callback for preg_replace_callback on the query.
249
	 * It allows to replace on the fly a few pre-defined strings, for
250 301
	 * convenience ('query_see_board', 'query_wanna_see_board'), with
251
	 * their current values from User::$info.
252 301
	 * In addition, it performs checks and sanitation on the values
253 275
	 * sent to the database.
254 137
	 *
255 106
	 * @param mixed[] $matches
256 110
	 *
257 106
	 * @return mixed|string
258 2
	 * @throws \ElkArte\Exceptions\Exception
259 106
	 */
260 10
	public function replacement__callback($matches)
261 102
	{
262 69
		// Connection gone???  This should *never* happen at this point, yet it does :'(
263 86
		if (!$this->validConnection())
264 43
		{
265 65
			Errors::instance()->display_db_error('ElkArte\\Database\\AbstractQuery::replacement__callback');
266 2
		}
267 63
268 3
		switch ($matches[1])
269 60
		{
270 1
			case 'db_prefix':
271 60
				return $this->_db_prefix;
272
			case 'query_see_board':
273 60
				return $this->query_see_board;
274 60
			case 'query_wanna_see_board':
275
				return $this->query_wanna_see_board;
276
			case 'ilike':
277
				return $this->ilike;
278
			case 'not_ilike':
279
				return $this->not_ilike;
280
			case 'rlike':
281
				return $this->rlike;
282
			case 'not_rlike':
283
				return $this->not_rlike;
284
			case 'column_case_insensitive':
285
				return $this->_replaceColumnCaseInsensitive($matches[2]);
286
		}
287
288
		if (!isset($matches[2]))
289
		{
290
			$this->error_backtrace('Invalid value inserted or no type specified.', '', E_USER_ERROR, __FILE__, __LINE__);
291
		}
292
293
		if (!isset($this->_db_callback_values[$matches[2]]))
294
		{
295
			$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__);
296
		}
297
298
		$replacement = $this->_db_callback_values[$matches[2]];
299 6
300
		switch ($matches[1])
301 6
		{
302
			case 'int':
303
				return $this->_replaceInt($matches[2], $replacement);
304
			case 'string':
305
			case 'text':
306
				return $this->_replaceString($replacement);
307 37
			case 'string_case_sensitive':
308
				return $this->_replaceStringCaseSensitive($replacement);
309 37
			case 'string_case_insensitive':
310
				return $this->_replaceStringCaseInsensitive($replacement);
311
			case 'array_int':
312
				return $this->_replaceArrayInt($matches[2], $replacement);
313
			case 'array_string':
314 37
				return $this->_replaceArrayString($matches[2], $replacement);
315
			case 'array_string_case_insensitive':
316
				return $this->_replaceArrayStringCaseInsensitive($matches[2], $replacement);
317 37
			case 'date':
318
				return $this->_replaceDate($matches[2], $replacement);
319 37
			case 'float':
320 37
				return $this->_replaceFloat($matches[2], $replacement);
321
			case 'identifier':
322
				return $this->_replaceIdentifier($replacement);
323 37
			case 'raw':
324
				return $replacement;
325 37
			default:
326 37
				$this->error_backtrace('Undefined type used in the database query. (' . $matches[1] . ':' . $matches[2] . ')', '', false, __FILE__, __LINE__);
327
				break;
328
		}
329
330
		return '';
331 37
	}
332
333 37
	/**
334
	 * Finds out if the connection is still valid.
335
	 *
336
	 * @return bool
337
	 */
338
	public function validConnection()
339
	{
340
		return (bool) $this->connection;
341
	}
342
343
	/**
344
	 * Casts the column to LOWER(column_name) for replacement__callback.
345
	 *
346
	 * @param mixed $replacement
347
	 * @return string
348
	 */
349
	protected function _replaceColumnCaseInsensitive($replacement)
350
	{
351
		return 'LOWER(' . $replacement . ')';
352
	}
353
354
	/**
355
	 * Scans the debug_backtrace output looking for the place where the
356
	 * actual error happened
357
	 *
358
	 * @return mixed[]
359
	 */
360
	protected function backtrace_message()
361
	{
362
		$log_message = '';
363
		$file = null;
364
		$line = null;
365
		foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $step)
366 275
		{
367
			// Found it?
368 275
			if (!method_exists($this, $step['function']) && !in_array(substr($step['function'], 0, 7), array('elk_db_', 'preg_re', 'db_erro', 'call_us')))
369
			{
370
				$log_message .= '<br />Function: ' . $step['function'];
371
				break;
372
			}
373 275
374
			if (isset($step['line']))
375
			{
376
				$file = $step['file'];
377
				$line = $step['line'];
378
			}
379
		}
380
		return [$file, $line, $log_message];
381
	}
382 114
383
	/**
384 114
	 * This function tries to work out additional error information from a back trace.
385
	 *
386
	 * @param string $error_message
387
	 * @param string $log_message
388
	 * @param string|bool $error_type
389
	 * @param string|null $file
390
	 * @param int|null $line
391
	 *
392
	 * @return array
393
	 * @throws \ElkArte\Exceptions\Exception
394
	 */
395
	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

395
	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...
396
	{
397
		if (empty($log_message))
398
		{
399
			$log_message = $error_message;
400
		}
401
402
		// We'll try recovering the file and line number the original db query was called from.
403 1
		list ($file, $line, $backtrace_message) = $this->backtrace_message();
404
405 1
		// Just in case nothing can be found from debug_backtrace
406
		$file = $file ?? $file_fallback;
407
		$line = $line ?? $line_fallback;
408
		$log_message .= $backtrace_message;
409
410
		// Is always a critical error.
411
		Errors::instance()->log_error($log_message, 'critical', $file, $line);
412
413
		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

413
		throw new Exception(/** @scrutinizer ignore-type */ [false, $error_message], false);
Loading history...
414 5
	}
415
416 5
	/**
417
	 * Tests and casts integers for replacement__callback.
418
	 *
419
	 * @param mixed $identifier
420
	 * @param mixed $replacement
421
	 * @return string
422
	 * @throws \ElkArte\Exceptions\Exception
423
	 */
424
	protected function _replaceInt($identifier, $replacement)
425
	{
426
		if (!is_numeric($replacement) || (string) $replacement !== (string) (int) $replacement)
427 69
		{
428
			$this->error_backtrace('Wrong value type sent to the database. Integer expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
429 69
		}
430
431 69
		return (string) (int) $replacement;
432
	}
433
434
	/**
435
	 * Casts values to string for replacement__callback.
436 69
	 *
437
	 * @param mixed $replacement
438 69
	 * @return string
439
	 */
440
	protected function _replaceString($replacement)
441
	{
442
		return sprintf('\'%1$s\'', $this->escape_string($replacement));
443 69
	}
444
445
	/**
446 69
	 * Escape string for the database input
447
	 *
448
	 * @param string $string
449
	 *
450
	 * @return string
451
	 */
452
	abstract public function escape_string($string);
453
454
	/**
455
	 * Casts values to string for replacement__callback and in the DBMS that
456
	 * require this solution makes it so that the comparison will be case sensitive.
457
	 *
458
	 * @param mixed $replacement
459
	 * @return string
460
	 */
461
	protected function _replaceStringCaseSensitive($replacement)
462 43
	{
463
		return $this->_replaceString($replacement);
464 43
	}
465
466 43
	/**
467
	 * Casts values to LOWER(string) for replacement__callback.
468
	 *
469
	 * @param mixed $replacement
470
	 * @return string
471 43
	 */
472
	protected function _replaceStringCaseInsensitive($replacement)
473 43
	{
474
		return 'LOWER(' . $this->_replaceString($replacement) . ')';
475
	}
476 43
477
	/**
478
	 * Tests and casts arrays of integers for replacement__callback.
479
	 *
480
	 * @param string $identifier
481
	 * @param mixed[] $replacement
482
	 * @return string
483
	 * @throws \ElkArte\Exceptions\Exception
484
	 */
485
	protected function _replaceArrayInt($identifier, $replacement)
486
	{
487
		if (is_array($replacement))
0 ignored issues
show
introduced by
The condition is_array($replacement) is always true.
Loading history...
488
		{
489
			if (empty($replacement))
490
			{
491
				$this->error_backtrace('Database error, given array of integer values is empty. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
492
			}
493 2
494
			foreach ($replacement as $key => $value)
495 2
			{
496
				if (!is_numeric($value) || (string) $value !== (string) (int) $value)
497 2
				{
498
					$this->error_backtrace('Wrong value type sent to the database. Array of integers expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
499
				}
500
501
				$replacement[$key] = (string) (int) $value;
502 2
			}
503
504 2
			return implode(', ', $replacement);
505
		}
506
		else
507 2
		{
508
			$this->error_backtrace('Wrong value type sent to the database. Array of integers expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
509
		}
510
	}
511
512
	/**
513
	 * Tests and casts arrays of strings for replacement__callback.
514
	 *
515
	 * @param string $identifier
516
	 * @param mixed[] $replacement
517
	 * @return string
518
	 * @throws \ElkArte\Exceptions\Exception
519
	 */
520
	protected function _replaceArrayString($identifier, $replacement)
521
	{
522
		if (is_array($replacement))
0 ignored issues
show
introduced by
The condition is_array($replacement) is always true.
Loading history...
523 3
		{
524
			if (empty($replacement))
525 3
			{
526
				$this->error_backtrace('Database error, given array of string values is empty. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
527 3
			}
528
529
			foreach ($replacement as $key => $value)
530
			{
531
				$replacement[$key] = sprintf('\'%1$s\'', $this->escape_string($value));
532
			}
533
534
			return implode(', ', $replacement);
535
		}
536
		else
537
		{
538
			$this->error_backtrace('Wrong value type sent to the database. Array of strings expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
539
		}
540
	}
541
542
	/**
543 1
	 * Tests and casts to LOWER(column_name) (if needed) arrays of strings
544
	 * for replacement__callback.
545 1
	 *
546
	 * @param string $identifier
547
	 * @param mixed[] $replacement
548
	 * @return string
549
	 * @throws \ElkArte\Exceptions\Exception
550 1
	 */
551
	protected function _replaceArrayStringCaseInsensitive($identifier, $replacement)
552
	{
553
		if (is_array($replacement))
0 ignored issues
show
introduced by
The condition is_array($replacement) is always true.
Loading history...
554
		{
555
			if (empty($replacement))
556
			{
557
				$this->error_backtrace('Database error, given array of string values is empty. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
558
			}
559
560
			foreach ($replacement as $key => $value)
561
			{
562
				$replacement[$key] = $this->_replaceStringCaseInsensitive($value);
563
			}
564
565
			return implode(', ', $replacement);
566
		}
567
		else
568
		{
569
			$this->error_backtrace('Wrong value type sent to the database. Array of strings expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
570
		}
571
	}
572
573 277
	/**
574
	 * Tests and casts date for replacement__callback.
575 277
	 *
576
	 * @param mixed $identifier
577
	 * @param mixed $replacement
578
	 * @return string
579
	 * @throws \ElkArte\Exceptions\Exception
580
	 */
581
	protected function _replaceDate($identifier, $replacement)
582
	{
583
		if (preg_match('~^(\d{4})-([0-1]?\d)-([0-3]?\d)$~', $replacement, $date_matches) === 1)
584
		{
585
			return sprintf('\'%04d-%02d-%02d\'', $date_matches[1], $date_matches[2], $date_matches[3]);
586
		}
587
		else
588
		{
589
			$this->error_backtrace('Wrong value type sent to the database. Date expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
590
		}
591
	}
592
593
	/**
594
	 * Tests and casts floating numbers for replacement__callback.
595
	 *
596
	 * @param mixed $identifier
597
	 * @param mixed $replacement
598
	 * @return string
599
	 * @throws \ElkArte\Exceptions\Exception
600
	 */
601
	protected function _replaceFloat($identifier, $replacement)
602
	{
603
		if (!is_numeric($replacement))
604
		{
605
			$this->error_backtrace('Wrong value type sent to the database. Floating point number expected. (' . $identifier . ')', '', E_USER_ERROR, __FILE__, __LINE__);
606
		}
607
608
		return (string) (float) $replacement;
609
	}
610
611
	/**
612
	 * Quotes identifiers for replacement__callback.
613
	 *
614
	 * @param mixed $replacement
615
	 * @return string
616
	 * @throws \ElkArte\Exceptions\Exception
617
	 */
618
	protected function _replaceIdentifier($replacement)
619
	{
620
		if (preg_match('~[a-z_][0-9a-zA-Z$,_]{0,60}~', $replacement) !== 1)
621
		{
622
			$this->error_backtrace('Wrong value type sent to the database. Invalid identifier used. (' . $replacement . ')', '', E_USER_ERROR, __FILE__, __LINE__);
623
		}
624
625
		return '`' . $replacement . '`';
626
	}
627
628
	/**
629
	 * {@inheritDoc}
630
	 */
631
	public function fetchQuery($db_string, $db_values = array())
632
	{
633
		return $this->query('', $db_string, $db_values);
634 38
	}
635
636 38
	/**
637 38
	 * {@inheritDoc}
638
	 */
639
	public function query($identifier, $db_string, $db_values = array())
640
	{
641
		// One more query....
642
		$this->_query_count++;
643
644
		$db_string = $this->initialChecks($db_string, $db_values, $identifier);
645
646
		if (trim($db_string) === '')
647
		{
648
			throw new \Exception('Query string empty');
649
		}
650
651
		$db_string = $this->_prepareQuery($db_string, $db_values);
652
653
		$this->_preQueryDebug($db_string);
654
655
		$this->_doSanityCheck($db_string);
656
657
		$this->executeQuery($db_string);
658
659
		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...
660
		{
661
			$this->_db_last_result = $this->error($db_string);
662
		}
663
664
		// Revert not to skip errors
665
		if ($this->_skip_error)
666
		{
667
			$this->_skip_error = false;
668
		}
669
670
		// Debugging.
671
		$this->_postQueryDebug();
672
673
		return $this->result;
674
	}
675
676
	/**
677
	 * Actually execute the DBMS-specific code to run the query
678
	 *
679
	 * @param string $db_string
680
	 */
681
	abstract protected function executeQuery($db_string);
682
683
	/**
684
	 * {@inheritDoc}
685
	 */
686
	abstract public function error($db_string);
687
688
	/**
689
	 * Prepares the strings to show the error to the user/admin and stop
690
	 * the code execution
691
	 *
692
	 * @param string $db_string
693
	 * @param string $query_error
694
	 * @param string $file
695
	 * @param int $line
696
	 */
697
	protected function throwError($db_string, $query_error, $file, $line)
698 6
	{
699
		global $context, $txt, $modSettings, $db_show_debug;
700
701 6
		// Nothing's defined yet... just die with it.
702
		if (empty($context) || empty($txt))
703
		{
704
			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...
705
		}
706
707 6
		// Show an error message, if possible.
708
		$context['error_title'] = $txt['database_error'];
709
		$message = $txt['try_again'];
710
711
		// Add database version that we know of, for the admin to know. (and ask for support)
712
		if (allowedTo('admin_forum'))
713
		{
714
			$message = nl2br($query_error) . '<br />' . $txt['file'] . ': ' . $file . '<br />' . $txt['line'] . ': ' . $line .
715
				'<br /><br />' . sprintf($txt['database_error_versions'], $modSettings['elkVersion']);
716
717
			if ($db_show_debug === true)
718
			{
719
				$message .= '<br /><br />' . nl2br($db_string);
720
			}
721
		}
722
723
		// It's already been logged... don't log it again.
724
		throw new Exception($message, false);
725
	}
726
727
	/**
728
	 * {@inheritDoc}
729
	 */
730
	abstract public function insert($method, $table, $columns, $data, $keys, $disable_trans = false);
731
732
	/**
733
	 * Prepares the data that will be later implode'd into the actual query string
734 6
	 *
735
	 * @param string $table
736
	 * @param mixed[] $columns
737 6
	 * @param mixed[] $data
738
	 * @return mixed[]
739
	 */
740
	protected function prepareInsert($table, $columns, $data)
741
	{
742
		// With nothing to insert, simply return.
743 6
		if (empty($data))
744
		{
745
			throw new \Exception('No data to insert');
746
		}
747
748
		// Inserting data as a single row can be done as a single array.
749
		if (!is_array($data[array_rand($data)]))
750
		{
751
			$data = [$data];
752
		}
753
754
		// Replace the prefix holder with the actual prefix.
755
		$table = str_replace('{db_prefix}', $this->_db_prefix, $table);
756
		$this->_skip_error = $table === $this->_db_prefix . 'log_errors';
757
758
		// Create the mold for a single row insert.
759
		$insertData = '(';
760
		foreach ($columns as $columnName => $type)
761
		{
762
			// Are we restricting the length?
763
			if (strpos($type, 'string-') !== false)
764
			{
765
				$insertData .= sprintf('SUBSTRING({string:%1$s}, 1, ' . substr($type, 7) . '), ', $columnName);
766
			}
767
			else
768
			{
769
				$insertData .= sprintf('{%1$s:%2$s}, ', $type, $columnName);
770
			}
771
		}
772
		$insertData = substr($insertData, 0, -2) . ')';
773
774
		// Create an array consisting of only the columns.
775
		$indexed_columns = array_keys($columns);
776
777
		// Here's where the variables are injected to the query.
778
		$insertRows = [];
779
		foreach ($data as $dataRow)
780
		{
781
			$insertRows[] = $this->quote($insertData, $this->_array_combine($indexed_columns, $dataRow));
782
		}
783
		return [$table, $indexed_columns, $insertRows];
784
	}
785
786
	/**
787
	 * {@inheritDoc}
788
	 */
789
	abstract public function replace($table, $columns, $data, $keys, $disable_trans = false);
790
791
	/**
792
	 * {@inheritDoc}
793
	 */
794
	public function escape_wildcard_string($string, $translate_human_wildcards = false)
795
	{
796
		$replacements = array(
797
			'%' => '\%',
798
			'_' => '\_',
799 19
			'\\' => '\\\\',
800
		);
801
802 19
		if ($translate_human_wildcards)
803
		{
804
			$replacements += array(
805
				'*' => '%',
806
			);
807
		}
808
809
		return strtr($string, $replacements);
810
	}
811
812
	/**
813
	 * {@inheritDoc}
814
	 */
815
	public function connection()
816
	{
817
		// find it, find it
818
		return $this->connection;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->connection returns the type ElkArte\Database\ConnectionInterface which is incompatible with the return type mandated by ElkArte\Database\QueryInterface::connection() of resource.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
819
	}
820
821
	/**
822
	 * {@inheritDoc}
823
	 */
824
	public function num_queries()
825
	{
826
		return $this->_query_count;
827
	}
828
829
	/**
830
	 * {@inheritDoc}
831
	 */
832
	public function skip_next_error()
833
	{
834
		$this->_skip_error = true;
835
	}
836
837
	/**
838
	 * {@inheritDoc}
839
	 */
840
	public function truncate($table)
841 79
	{
842
		return $this->fetchQuery('
843 79
			TRUNCATE ' . $table,
844
			[]
845 79
		);
846
	}
847 79
848
	/**
849
	 * Set the unbuffered state for the connection
850
	 *
851 30
	 * @param bool $state
852 30
	 */
853
	public function setUnbuffered($state)
854 30
	{
855
		$this->_unbuffered = (bool) $state;
856 30
	}
857
858
	/**
859
	 *  Get the version number.
860
	 *
861 30
	 * @return string - the version
862
	 * @throws \ElkArte\Exceptions\Exception
863
	 */
864
	abstract public function client_version();
865
866
	/**
867
	 * Return server info.
868
	 *
869
	 * @return string
870
	 */
871
	abstract public function server_info();
872
873
	/**
874
	 * Whether the database system is case sensitive.
875 301
	 *
876
	 * @return bool
877 301
	 */
878
	abstract public function case_sensitive();
879 301
880
	/**
881 37
	 * Get the name (title) of the database system.
882
	 *
883
	 * @return string
884 301
	 */
885
	abstract public function title();
886
887 299
	/**
888
	 * Returns whether the database system supports ignore.
889
	 *
890 299
	 * @return false
891 299
	 */
892
	abstract public function support_ignore();
893 299
894
	/**
895 299
	 * Get the version number.
896 299
	 *
897
	 * @return string - the version
898
	 * @throws \ElkArte\Exceptions\Exception
899
	 */
900 299
	abstract public function server_version();
901
902
	/**
903 301
	 * Temporary function to support migration to the new schema of the db layer
904
	 *
905
	 * @deprecated since 2.0
906
	 */
907
	public function fetch_row($result)
908
	{
909
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::fetch_row()', 'Result::fetch_row()');
910
		if ($result === false)
911
		{
912 301
			return false;
913
		}
914 301
		else
915
		{
916
			return $result->fetch_row();
917 301
		}
918
	}
919
920
	/**
921
	 * Temporary function to support migration to the new schema of the db layer
922
	 *
923
	 * @deprecated since 2.0
924
	 */
925
	public function fetch_assoc($result)
926
	{
927
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::fetch_assoc()', 'Result::fetch_assoc()');
928
		if ($result === false)
929
		{
930
			return false;
931
		}
932
		else
933
		{
934
			return $result->fetch_assoc();
935
		}
936
	}
937
938
	/**
939 301
	 * Temporary function to support migration to the new schema of the db layer
940
	 *
941
	 * @deprecated since 2.0
942
	 */
943
	public function free_result($result)
944 301
	{
945
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::free_result()', 'Result::free_result()');
946 301
		if ($result === false)
947
		{
948 301
			return;
949
		}
950
		else
951
		{
952
			return $result->free_result();
953
		}
954 301
	}
955
956
	/**
957
	 * Temporary function to support migration to the new schema of the db layer
958
	 *
959
	 * @deprecated since 2.0
960
	 */
961
	public function affected_rows()
962
	{
963
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::affected_rows()', 'Result::affected_rows()');
964 301
		return $this->result->affected_rows();
965
	}
966 301
967
	/**
968
	 * Temporary function to support migration to the new schema of the db layer
969 301
	 *
970 301
	 * @deprecated since 2.0
971
	 */
972 301
	public function num_rows($result)
973 301
	{
974 301
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::num_rows()', 'Result::num_rows()');
975
		if ($result === false)
976 301
		{
977 301
			return 0;
978
		}
979 301
		else
980
		{
981 122
			return $result->num_rows();
982
		}
983 122
	}
984
985 122
	/**
986 122
	 * Temporary function to support migration to the new schema of the db layer
987
	 *
988 122
	 * @deprecated since 2.0
989
	 */
990
	public function num_fields($result)
991
	{
992 122
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::num_fields()', 'Result::num_fields()');
993
		if ($result === false)
994 122
		{
995 122
			return 0;
996
		}
997
		else
998 10
		{
999
			return $result->num_fields();
1000
		}
1001 122
	}
1002 122
1003
	/**
1004
	 * Temporary function to support migration to the new schema of the db layer
1005 301
	 *
1006 301
	 * @deprecated since 2.0
1007
	 */
1008
	public function insert_id($table)
1009 301
	{
1010
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::insert_id()', 'Result::insert_id()');
1011
		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...
1012
	}
1013
1014 301
	/**
1015
	 * Temporary function to support migration to the new schema of the db layer
1016
	 *
1017
	 * @deprecated since 2.0
1018 301
	 */
1019
	public function data_seek($result, $counter)
1020
	{
1021
// 		\ElkArte\Errors\Errors::instance()->log_deprecated('Query::data_seek()', 'Result::data_seek()');
1022
		return $result->data_seek($counter);
1023 301
	}
1024
1025
	/**
1026
	 * Temporary function: I'm not sure this is the best place to have it, though it was
1027
	 * convenient while fixing other issues.
1028 301
	 *
1029
	 * @deprecated since 2.0
1030
	 */
1031
	public function supportMediumtext()
1032
	{
1033
		return false;
1034
	}
1035
1036
	/**
1037
	 * Temporary function to support migration to the new schema of the db layer
1038
	 *
1039
	 * @deprecated since 2.0
1040
	 */
1041
	abstract public function list_tables($db_name_str = false, $filter = false);
1042
1043
	/**
1044
	 * This function combines the keys and values of the data passed to db::insert.
1045
	 *
1046
	 * @param int[] $keys
1047
	 * @param mixed[] $values
1048
	 * @return mixed[]
1049
	 */
1050
	protected function _array_combine($keys, $values)
1051
	{
1052
		$is_numeric = array_filter(array_keys($values), 'is_numeric');
1053
1054
		if (!empty($is_numeric))
1055
		{
1056
			return array_combine($keys, $values);
1057
		}
1058
		else
1059
		{
1060
			$combined = array();
1061
			foreach ($keys as $key)
1062
			{
1063
				if (isset($values[$key]))
1064
				{
1065
					$combined[$key] = $values[$key];
1066
				}
1067
			}
1068
1069
			// @todo should throw an E_WARNING if count($combined) != count($keys)
1070
			return $combined;
1071
		}
1072
	}
1073
1074
	/**
1075
	 * Checks for "illegal characters" and runs replacement__callback if not
1076
	 * overridden.
1077
	 * In case of problems, the method can ends up dying.
1078
	 *
1079
	 * @param string $db_string
1080
	 * @param mixed $db_values
1081
	 * @return string
1082
	 * @throws \ElkArte\Exceptions\Exception
1083
	 */
1084
	protected function _prepareQuery($db_string, $db_values)
1085
	{
1086
		global $modSettings;
1087
1088
		if (empty($modSettings['disableQueryCheck']) && strpos($db_string, '\'') !== false && empty($db_values['security_override']))
1089
		{
1090
			$this->error_backtrace('Hacking attempt...', 'Illegal character (\') used in query...', true, __FILE__, __LINE__);
1091
		}
1092
1093
		if (empty($db_values['security_override']) && (!empty($db_values) || strpos($db_string, '{db_prefix}') !== false))
1094
		{
1095
			// Store these values for use in the callback function.
1096
			$this->_db_callback_values = $db_values;
1097
1098
			// Inject the values passed to this function.
1099
			$count = -1;
1100
			while (($count > 0 && isset($db_values['recursive'])) || $count === -1)
1101
			{
1102
				$db_string = preg_replace_callback('~{([a-z_]+)(?::([\.a-zA-Z0-9_-]+))?}~',
1103
					function ($matches) {
1104
						return $this->replacement__callback($matches);
1105
					}, $db_string, -1, $count);
1106
			}
1107
1108
			// No need for them any longer.
1109
			$this->_db_callback_values = array();
1110
		}
1111
1112
		return $db_string;
1113
	}
1114
1115
	/**
1116
	 * Some initial checks and replacement of text insside the query string
1117
	 *
1118
	 * @param string $db_string
1119
	 * @param mixed $db_values
1120
	 * @param string $identifier The old (now mostly unused) query identifier
1121
	 * @return string
1122
	 */
1123
	abstract protected function initialChecks($db_string, $db_values, $identifier = '');
1124
1125
	/**
1126
	 * Tracks the initial status (time, file/line, query) for performance evaluation.
1127
	 *
1128
	 * @param string $db_string
1129
	 * @throws \ElkArte\Exceptions\Exception
1130
	 */
1131
	protected function _preQueryDebug($db_string)
1132
	{
1133
		global $db_show_debug, $time_start;
1134
1135
		// Debugging.
1136
		if ($db_show_debug === true)
1137
		{
1138
			// We'll try recovering the file and line number the original db query was called from.
1139
			list ($file, $line) = $this->backtrace_message();
1140
1141
			// Just in case nothing can be found from debug_backtrace
1142
			$file = $file ?? __FILE__;
1143
			$line = $line ?? __LINE__;
1144
1145
			if (!empty($_SESSION['debug_redirect']))
1146
			{
1147
				$this->_debug->merge_db($_SESSION['debug_redirect']);
1148
				// @todo this may be off by 1
1149
				$this->_query_count += count($_SESSION['debug_redirect']);
1150
				$_SESSION['debug_redirect'] = array();
1151
			}
1152
1153
			// Don't overload it.
1154
			$st = microtime(true);
1155
			$this->db_cache = [];
1156
			$this->db_cache['q'] = $this->_query_count < 50 ? $db_string : '...';
1157
			$this->db_cache['f'] = $file;
1158
			$this->db_cache['l'] = $line;
1159
			$this->db_cache['s'] = $st - $time_start;
1160
			$this->db_cache['st'] = $st;
1161
		}
1162
	}
1163
1164
	/**
1165
	 * Closes up the tracking and stores everything in the debug class.
1166
	 */
1167
	protected function _postQueryDebug()
1168
	{
1169
		global $db_show_debug;
1170
1171
		if ($db_show_debug === true)
1172
		{
1173
			$this->db_cache['t'] = microtime(true) - $this->db_cache['st'];
1174
			$this->_debug->db_query($this->db_cache);
1175
			$this->db_cache = [];
1176
		}
1177
	}
1178
1179
	/**
1180
	 * Checks the query doesn't have nasty stuff in it.
1181
	 * In case of problems, the method can ends up dying.
1182
	 *
1183
	 * @param string $db_string
1184
	 * @throws \ElkArte\Exceptions\Exception
1185
	 */
1186
	protected function _doSanityCheck($db_string)
1187
	{
1188
		global $modSettings;
1189
1190
		// First, we clean strings out of the query, reduce whitespace, lowercase, and trim - so we can check it over.
1191
		$clean = '';
1192
		if (empty($modSettings['disableQueryCheck']))
1193
		{
1194
			$old_pos = 0;
1195
			$pos = -1;
1196
			while (true)
1197
			{
1198
				$pos = strpos($db_string, '\'', $pos + 1);
1199
				if ($pos === false)
1200
				{
1201
					break;
1202
				}
1203
				$clean .= substr($db_string, $old_pos, $pos - $old_pos);
1204
1205
				while (true)
1206
				{
1207
					$pos1 = strpos($db_string, '\'', $pos + 1);
1208
					$pos2 = strpos($db_string, static::ESCAPE_CHAR, $pos + 1);
1209
1210
					if ($pos1 === false)
1211
					{
1212
						break;
1213
					}
1214
					elseif ($pos2 === false || $pos2 > $pos1)
1215
					{
1216
						$pos = $pos1;
1217
						break;
1218
					}
1219
1220
					$pos = $pos2 + 1;
1221
				}
1222
1223
				$clean .= ' %s ';
1224
				$old_pos = $pos + 1;
1225
			}
1226
1227
			$clean .= substr($db_string, $old_pos);
1228
			$clean = trim(strtolower(preg_replace($this->allowed_comments['from'], $this->allowed_comments['to'], $clean)));
1229
1230
			// Comments?  We don't use comments in our queries, we leave 'em outside!
1231
			if (strpos($clean, '/*') > 2 || strpos($clean, '--') !== false || strpos($clean, ';') !== false)
1232
			{
1233
				$fail = true;
1234
			}
1235
			// Trying to change passwords, slow us down, or something?
1236
			elseif (strpos($clean, 'sleep') !== false && preg_match('~(^|[^a-z])sleep($|[^[_a-z])~s', $clean) != 0)
1237
			{
1238
				$fail = true;
1239
			}
1240
			elseif (strpos($clean, 'benchmark') !== false && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
1241
			{
1242
				$fail = true;
1243
			}
1244
1245
			if (!empty($fail) && class_exists('\\ElkArte\\Errors\\Errors'))
1246
			{
1247
				$this->error_backtrace('Hacking attempt...', 'Hacking attempt...' . "\n" . $db_string, E_USER_ERROR, __FILE__, __LINE__);
1248
			}
1249
		}
1250
	}
1251
}
1252