|
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; |
|
|
|
|
|
|
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) |
|
|
|
|
|
|
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); |
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
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) |
|
|
|
|
|
|
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); |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
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
|
|
|
|
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
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. 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.