1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* XOOPS Criteria parser for database query |
4
|
|
|
* |
5
|
|
|
* You may not change or alter any portion of this comment or credits |
6
|
|
|
* of supporting developers from this source code or any supporting source code |
7
|
|
|
* which is considered copyrighted (c) material of the original comment or credit authors. |
8
|
|
|
* This program is distributed in the hope that it will be useful, |
9
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
10
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
11
|
|
|
* |
12
|
|
|
* @copyright (c) 2000-2025 XOOPS Project (https://xoops.org) |
13
|
|
|
* @license GNU GPL 2 (https://www.gnu.org/licenses/gpl-2.0.html) |
14
|
|
|
* @package kernel |
15
|
|
|
* @subpackage database |
16
|
|
|
* @since 2.0.0 |
17
|
|
|
* @author Kazumi Ono <[email protected]> |
18
|
|
|
* @author Nathan Dial |
19
|
|
|
* @author Taiwen Jiang <[email protected]> |
20
|
|
|
*/ |
21
|
|
|
defined('XOOPS_ROOT_PATH') || exit('Restricted access'); |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* A criteria (grammar?) for a database query. |
25
|
|
|
* |
26
|
|
|
* Abstract base class should never be instantiated directly. |
27
|
|
|
* |
28
|
|
|
* @abstract |
29
|
|
|
*/ |
30
|
|
|
class CriteriaElement |
31
|
|
|
{ |
32
|
|
|
/** |
33
|
|
|
* Sort order |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
public $order = 'ASC'; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* |
41
|
|
|
* @var string |
42
|
|
|
*/ |
43
|
|
|
public $sort = ''; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Number of records to retrieve |
47
|
|
|
* |
48
|
|
|
* @var int |
49
|
|
|
*/ |
50
|
|
|
public $limit = 0; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Offset of first record |
54
|
|
|
* |
55
|
|
|
* @var int |
56
|
|
|
*/ |
57
|
|
|
public $start = 0; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* |
61
|
|
|
* @var string |
62
|
|
|
*/ |
63
|
|
|
public $groupby = ''; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Constructor |
67
|
|
|
*/ |
68
|
|
|
public function __construct() {} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Render the criteria element |
72
|
|
|
* @return string |
73
|
|
|
*/ |
74
|
|
|
public function render() {} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* |
78
|
|
|
* @param string $sort |
79
|
|
|
*/ |
80
|
|
|
public function setSort($sort) |
81
|
|
|
{ |
82
|
|
|
$this->sort = $sort; |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* |
87
|
|
|
* @return string |
88
|
|
|
*/ |
89
|
|
|
public function getSort() |
90
|
|
|
{ |
91
|
|
|
return $this->sort; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* |
96
|
|
|
* @param string $order |
97
|
|
|
*/ |
98
|
|
|
public function setOrder($order) |
99
|
|
|
{ |
100
|
|
|
if ('DESC' === strtoupper($order)) { |
101
|
|
|
$this->order = 'DESC'; |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* |
107
|
|
|
* @return string |
108
|
|
|
*/ |
109
|
|
|
public function getOrder() |
110
|
|
|
{ |
111
|
|
|
return $this->order; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* |
116
|
|
|
* @param int $limit |
117
|
|
|
*/ |
118
|
|
|
public function setLimit($limit = 0) |
119
|
|
|
{ |
120
|
|
|
$this->limit = (int) $limit; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* |
125
|
|
|
* @return int |
126
|
|
|
*/ |
127
|
|
|
public function getLimit() |
128
|
|
|
{ |
129
|
|
|
return $this->limit; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* |
134
|
|
|
* @param int $start |
135
|
|
|
*/ |
136
|
|
|
public function setStart($start = 0) |
137
|
|
|
{ |
138
|
|
|
$this->start = (int) $start; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* |
143
|
|
|
* @return int |
144
|
|
|
*/ |
145
|
|
|
public function getStart() |
146
|
|
|
{ |
147
|
|
|
return $this->start; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* |
152
|
|
|
* @param string $group |
153
|
|
|
*/ |
154
|
|
|
public function setGroupBy($group) |
155
|
|
|
{ |
156
|
|
|
$this->groupby = $group; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* |
161
|
|
|
* @return string |
162
|
|
|
*/ |
163
|
|
|
public function getGroupby() |
164
|
|
|
{ |
165
|
|
|
return $this->groupby ? " GROUP BY {$this->groupby}" : ''; |
166
|
|
|
} |
167
|
|
|
/** |
168
|
|
|
* *#@- |
169
|
|
|
*/ |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
/** |
173
|
|
|
* Collection of multiple {@link CriteriaElement}s |
174
|
|
|
* |
175
|
|
|
*/ |
176
|
|
|
class CriteriaCompo extends CriteriaElement |
177
|
|
|
{ |
178
|
|
|
/** |
179
|
|
|
* The elements of the collection |
180
|
|
|
* |
181
|
|
|
* @var array Array of {@link CriteriaElement} objects |
182
|
|
|
*/ |
183
|
|
|
public $criteriaElements = []; |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Conditions |
187
|
|
|
* |
188
|
|
|
* @var array |
189
|
|
|
*/ |
190
|
|
|
public $conditions = []; |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* Constructor |
194
|
|
|
* |
195
|
|
|
* @param CriteriaElement|null $ele |
196
|
|
|
* @param string $condition |
197
|
|
|
*/ |
198
|
|
|
public function __construct(?CriteriaElement $ele = null, $condition = 'AND') |
199
|
|
|
{ |
200
|
|
|
if (isset($ele)) { |
201
|
|
|
$this->add($ele, $condition); |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Add an element |
207
|
|
|
* |
208
|
|
|
* @param CriteriaElement|object $criteriaElement |
209
|
|
|
* @param string $condition |
210
|
|
|
* @return object reference to this collection |
211
|
|
|
*/ |
212
|
|
|
public function &add(CriteriaElement $criteriaElement, $condition = 'AND') |
213
|
|
|
{ |
214
|
|
|
if (is_object($criteriaElement)) { |
215
|
|
|
$this->criteriaElements[] = & $criteriaElement; |
216
|
|
|
$this->conditions[] = $condition; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $this; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* Make the criteria into a query string |
224
|
|
|
* |
225
|
|
|
* @return string |
226
|
|
|
*/ |
227
|
|
|
public function render() |
228
|
|
|
{ |
229
|
|
|
$ret = ''; |
230
|
|
|
$count = count($this->criteriaElements); |
231
|
|
|
if ($count > 0) { |
232
|
|
|
$render_string = $this->criteriaElements[0]->render(); |
233
|
|
|
for ($i = 1; $i < $count; ++$i) { |
234
|
|
|
if (!$render = $this->criteriaElements[$i]->render()) { |
235
|
|
|
continue; |
236
|
|
|
} |
237
|
|
|
$render_string .= (empty($render_string) ? '' : ' ' . $this->conditions[$i] . ' ') . $render; |
238
|
|
|
} |
239
|
|
|
$ret = empty($render_string) ? '' : "({$render_string})"; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
return $ret; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Make the criteria into a SQL "WHERE" clause |
247
|
|
|
* |
248
|
|
|
* @return string |
249
|
|
|
*/ |
250
|
|
|
public function renderWhere() |
251
|
|
|
{ |
252
|
|
|
$ret = $this->render(); |
253
|
|
|
$ret = ($ret != '') ? 'WHERE ' . $ret : $ret; |
254
|
|
|
|
255
|
|
|
return $ret; |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Generate an LDAP filter from criteria |
260
|
|
|
* |
261
|
|
|
* @return string |
262
|
|
|
* @author Nathan Dial [email protected] |
263
|
|
|
*/ |
264
|
|
|
public function renderLdap() |
265
|
|
|
{ |
266
|
|
|
$retval = ''; |
267
|
|
|
$count = count($this->criteriaElements); |
268
|
|
|
if ($count > 0) { |
269
|
|
|
$retval = $this->criteriaElements[0]->renderLdap(); |
270
|
|
|
for ($i = 1; $i < $count; ++$i) { |
271
|
|
|
$cond = strtoupper($this->conditions[$i]); |
272
|
|
|
$op = ($cond === 'OR') ? '|' : '&'; |
273
|
|
|
$retval = "({$op}{$retval}" . $this->criteriaElements[$i]->renderLdap() . ')'; |
274
|
|
|
} |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
return $retval; |
278
|
|
|
} |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* A single criteria |
283
|
|
|
* |
284
|
|
|
*/ |
285
|
|
|
class Criteria extends CriteriaElement |
286
|
|
|
{ |
287
|
|
|
/** @var string|null Optional table prefix (alias) like "u" for "u.`uname`" */ |
288
|
|
|
public $prefix; |
289
|
|
|
|
290
|
|
|
/** @var string|null Optional column wrapper function with sprintf format, e.g. 'LOWER(%s)' */ |
291
|
|
|
public $function; |
292
|
|
|
|
293
|
|
|
/** @var string Column name or expression (backticks handled for simple columns) */ |
294
|
|
|
public $column; |
295
|
|
|
|
296
|
|
|
/** @var string SQL operator (=, <, >, LIKE, IN, IS NULL, etc.) */ |
297
|
|
|
public $operator; |
298
|
|
|
|
299
|
|
|
/** @var mixed Value for the operator: scalar for most ops, array or "(a,b)" for IN/NOT IN */ |
300
|
|
|
public $value; |
301
|
|
|
|
302
|
|
|
/** @var bool Allow empty string values to render (default false = skip empty) */ |
303
|
|
|
protected $allowEmptyValue = false; |
304
|
|
|
|
305
|
|
|
/** @var bool Allow inner wildcards in LIKE (default false = escape inner % and _) */ |
306
|
|
|
protected $allowInnerWildcards = false; |
307
|
|
|
|
308
|
|
|
/** @var bool Global default for allowing inner wildcards in LIKE across all instances */ |
309
|
|
|
protected static $defaultAllowInnerWildcards = false; |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Set the global default for allowing inner wildcards in LIKE patterns. |
313
|
|
|
* Useful during migrations of legacy modules that intentionally use inner wildcards. |
314
|
|
|
* |
315
|
|
|
* @param bool $on |
316
|
|
|
* @return void |
317
|
|
|
*/ |
318
|
|
|
public static function setDefaultAllowInnerWildcards(bool $on = true): void |
319
|
|
|
{ |
320
|
|
|
self::$defaultAllowInnerWildcards = $on; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Opt-in per instance for intentional inner wildcards in LIKE patterns. |
325
|
|
|
* Default remains secure (inner %/_ escaped). |
326
|
|
|
* |
327
|
|
|
* @param bool $on |
328
|
|
|
* @return $this |
329
|
|
|
*/ |
330
|
|
|
public function allowInnerWildcards(bool $on = true): self |
331
|
|
|
{ |
332
|
|
|
$this->allowInnerWildcards = $on; |
333
|
|
|
return $this; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Constructor |
338
|
|
|
* |
339
|
|
|
* @param string $column |
340
|
|
|
* @param mixed $value |
341
|
|
|
* @param string $operator |
342
|
|
|
* @param string|null $prefix |
343
|
|
|
* @param string|null $function sprintf format string, e.g. 'LOWER(%s)' |
344
|
|
|
* @param bool $allowEmptyValue |
345
|
|
|
*/ |
346
|
|
|
public function __construct($column, $value = '', $operator = '=', $prefix = '', $function = '', $allowEmptyValue = false) |
347
|
|
|
{ |
348
|
|
|
$this->prefix = $prefix; |
349
|
|
|
$this->function = $function; |
350
|
|
|
$this->column = $column; |
351
|
|
|
$this->value = $value; |
352
|
|
|
$this->operator = $operator; |
353
|
|
|
$this->allowEmptyValue = $allowEmptyValue; |
354
|
|
|
$this->allowInnerWildcards = self::$defaultAllowInnerWildcards; |
355
|
|
|
|
356
|
|
|
// Legacy always-true workaround: new Criteria(1, '1', '=') → no WHERE |
357
|
|
|
if ((int)$column === 1 && (int)$value === 1 && $operator === '=') { |
358
|
|
|
$this->column = ''; |
359
|
|
|
$this->value = ''; |
360
|
|
|
} |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Render the SQL fragment (no leading WHERE) |
365
|
|
|
* |
366
|
|
|
* @return string |
367
|
|
|
*/ |
368
|
|
|
public function render() |
369
|
|
|
{ |
370
|
|
|
/** @var \XoopsDatabase|null $xoopsDB */ |
371
|
|
|
$xoopsDB = isset($GLOBALS['xoopsDB']) ? $GLOBALS['xoopsDB'] : null; |
372
|
|
|
if (!$xoopsDB) { |
373
|
|
|
return ''; |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
$col = (string)($this->column ?? ''); |
377
|
|
|
$backtick = (strpos($col, '.') === false) ? '`' : ''; |
378
|
|
|
if (strpos($col, '(') !== false) { // function/expression like COUNT(col) |
379
|
|
|
$backtick = ''; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
$clause = (empty($this->prefix) ? '' : "{$this->prefix}.") . $backtick . $col . $backtick; |
383
|
|
|
|
384
|
|
|
if (!empty($this->function)) { |
385
|
|
|
// function should be a trusted sprintf pattern, e.g. 'LOWER(%s)' |
386
|
|
|
$clause = sprintf($this->function, $clause); |
387
|
|
|
} |
388
|
|
|
|
389
|
|
|
$op = strtoupper((string)$this->operator); |
390
|
|
|
|
391
|
|
|
// Null checks require no value |
392
|
|
|
if ($op === 'IS NULL' || $op === 'IS NOT NULL') { |
393
|
|
|
return $clause . ' ' . $op; |
394
|
|
|
} |
395
|
|
|
|
396
|
|
|
// Skip empty values unless explicitly allowed |
397
|
|
|
$rawValue = (string)$this->value; |
398
|
|
|
if (trim($rawValue) === '' && !$this->allowEmptyValue) { |
399
|
|
|
return ''; |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
// IN / NOT IN: accept arrays or "(a,b)" string |
403
|
|
|
if ($op === 'IN' || $op === 'NOT IN') { |
404
|
|
|
$vals = is_array($this->value) |
405
|
|
|
? $this->value |
406
|
|
|
: array_map('trim', explode(',', trim((string)$this->value, " ()"))); |
407
|
|
|
|
408
|
|
|
$parts = []; |
409
|
|
|
foreach ($vals as $v) { |
410
|
|
|
if (is_int($v) || (is_string($v) && preg_match('/^-?\d+$/', $v))) { |
411
|
|
|
$parts[] = (string)(int)$v; |
412
|
|
|
} else { |
413
|
|
|
$parts[] = $xoopsDB->quoteString((string)$v); |
|
|
|
|
414
|
|
|
} |
415
|
|
|
} |
416
|
|
|
return $clause . ' ' . $op . ' (' . implode(',', $parts) . ')'; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
// LIKE / NOT LIKE: preserve leading/trailing % runs; escape inner unless opted-in |
420
|
|
|
if ($op === 'LIKE' || $op === 'NOT LIKE') { |
421
|
|
|
$pattern = (string)$this->value; |
422
|
|
|
|
423
|
|
|
// NEW: if pattern is only % signs, it's effectively a no-op for LIKE; |
424
|
|
|
// don't emit a predicate so we don't exclude NULL rows. |
425
|
|
|
if ($op === 'LIKE' && $pattern !== '' && strspn($pattern, '%') === strlen($pattern)) { |
426
|
|
|
return ''; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
$len = strlen($pattern); |
430
|
|
|
$lead = strspn($pattern, '%'); |
431
|
|
|
$trail = strspn(strrev($pattern), '%'); |
432
|
|
|
$coreLen = $len - $lead - $trail; |
433
|
|
|
|
434
|
|
|
if ($coreLen <= 0) { |
435
|
|
|
// With the all-% early-return handled above, reaching here means we have a core. |
436
|
|
|
// If you keep this guard at all, it should not mention LIKE/NOT LIKE reachability. |
437
|
|
|
// Most implementations can simply drop this guard entirely. |
438
|
|
|
$final = $pattern; |
439
|
|
|
} else { |
440
|
|
|
$left = substr($pattern, 0, $lead); |
441
|
|
|
$core = substr($pattern, $lead, $coreLen); |
442
|
|
|
$right = substr($pattern, $len - $trail); |
443
|
|
|
|
444
|
|
|
$core = str_replace('\\', '\\\\', $core); |
445
|
|
|
if (!$this->allowInnerWildcards) { |
446
|
|
|
$core = str_replace(['%', '_'], ['\\%', '\\_'], $core); |
447
|
|
|
} |
448
|
|
|
$final = $left . $core . $right; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
$quoted = $xoopsDB->quoteString($final); |
|
|
|
|
452
|
|
|
// IMPORTANT: no ESCAPE clause for MySQL/MariaDB |
453
|
|
|
return $clause . ' ' . $op . ' ' . $quoted; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
// Equality/comparisons: keep integers numeric; quote strings via DB layer |
457
|
|
|
$v = $this->value; |
458
|
|
|
if (is_int($v) || (is_string($v) && preg_match('/^-?\d+$/', $v))) { |
459
|
|
|
$safe = (string)(int)$v; |
460
|
|
|
} else { |
461
|
|
|
$safe = $xoopsDB->quoteString((string)$v); |
|
|
|
|
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
return $clause . ' ' . $op . ' ' . $safe; |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* Generate an LDAP filter from criteria (unchanged semantics) |
469
|
|
|
* |
470
|
|
|
* @return string |
471
|
|
|
*/ |
472
|
|
|
public function renderLdap() |
473
|
|
|
{ |
474
|
|
|
if ($this->operator === '>') { |
475
|
|
|
$this->operator = '>='; |
476
|
|
|
} |
477
|
|
|
if ($this->operator === '<') { |
478
|
|
|
$this->operator = '<='; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
if ($this->operator === '!=' || $this->operator === '<>') { |
482
|
|
|
$operator = '='; |
483
|
|
|
$clause = '(!(' . $this->column . $operator . $this->value . '))'; |
484
|
|
|
} else { |
485
|
|
|
if ($this->operator === 'IN') { |
486
|
|
|
$newvalue = str_replace(['(', ')'], '', $this->value); |
487
|
|
|
$tab = explode(',', $newvalue); |
488
|
|
|
$clause = ''; |
489
|
|
|
foreach ($tab as $uid) { |
490
|
|
|
$clause .= "({$this->column}={$uid})"; |
491
|
|
|
} |
492
|
|
|
$clause = '(|' . $clause . ')'; |
493
|
|
|
} else { |
494
|
|
|
$clause = '(' . $this->column . $this->operator . $this->value . ')'; |
495
|
|
|
} |
496
|
|
|
} |
497
|
|
|
|
498
|
|
|
return $clause; |
499
|
|
|
} |
500
|
|
|
|
501
|
|
|
/** |
502
|
|
|
* Convenience: render with leading WHERE (or empty if no condition) |
503
|
|
|
* |
504
|
|
|
* @return string |
505
|
|
|
*/ |
506
|
|
|
public function renderWhere() |
507
|
|
|
{ |
508
|
|
|
$cond = $this->render(); |
509
|
|
|
return empty($cond) ? '' : "WHERE {$cond}"; |
510
|
|
|
} |
511
|
|
|
} |
512
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.