Passed
Push — main ( 20361b...6d1b9c )
by Thierry
06:44 queued 04:24
created

SelectQuery::getMatchExpression()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 2
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Lagdo\DbAdmin\Db\Page\Dql;
4
5
use Lagdo\DbAdmin\Db\Page\AppPage;
6
use Lagdo\DbAdmin\Driver\DriverInterface;
7
use Lagdo\DbAdmin\Driver\Entity\IndexEntity;
8
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
9
use Lagdo\DbAdmin\Driver\Entity\TableSelectEntity;
10
use Lagdo\DbAdmin\Driver\Utils\Utils;
11
use Exception;
12
13
use function count;
14
use function implode;
15
use function intval;
16
use function in_array;
17
use function preg_match;
18
use function strtoupper;
19
use function str_replace;
20
21
class SelectQuery
22
{
23
    /**
24
     * The constructor
25
     *
26
     * @param AppPage $page
27
     * @param DriverInterface $driver
28
     * @param Utils $utils
29
     */
30
    public function __construct(private AppPage $page,
31
        private DriverInterface $driver, private Utils $utils)
32
    {}
33
34
    /**
35
     * @return SelectOptions
36
     */
37
    private function options(): SelectOptions
38
    {
39
        return new SelectOptions($this->driver, $this->utils);
40
    }
41
42
    /**
43
     * @param SelectEntity $selectEntity
44
     *
45
     * @return void
46
     */
47
    private function setFieldsOptions(SelectEntity $selectEntity): void
48
    {
49
        $selectEntity->rights = []; // privilege => 0
50
        $selectEntity->columns = []; // selectable columns
51
        $selectEntity->textLength = 0;
52
        foreach ($selectEntity->fields as $key => $field) {
53
            $name = $this->page->fieldName($field);
54
            if (isset($field->privileges["select"]) && $name != "") {
55
                $selectEntity->columns[$key] = html_entity_decode(strip_tags($name), ENT_QUOTES);
56
                if ($this->page->isShortable($field)) {
57
                    $this->setSelectTextLength($selectEntity);
58
                }
59
            }
60
            $selectEntity->rights[] = $field->privileges;
61
        }
62
    }
63
64
    /**
65
     * Get required data for select on tables
66
     *
67
     * @param SelectEntity $selectEntity
68
     *
69
     * @return SelectEntity
70
     * @throws Exception
71
     */
72
    public function prepareSelect(SelectEntity $selectEntity): SelectEntity
73
    {
74
        $this->options()->setDefaultOptions($selectEntity);
75
76
        // From select.inc.php
77
        $selectEntity->fields = $this->driver->fields($selectEntity->table);
78
        $this->setFieldsOptions($selectEntity);
79
        if (!$selectEntity->columns && $this->driver->support("table")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $selectEntity->columns of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
80
            throw new Exception($this->utils->trans->lang('Unable to select the table') .
81
                ($selectEntity->fields ? "." : ": " . $this->driver->error()));
82
        }
83
84
        $selectEntity->indexes = $this->driver->indexes($selectEntity->table);
85
        $this->setForeignKeys($selectEntity);
86
        $this->setSelectColumns($selectEntity);
87
88
        $this->setSelectWhere($selectEntity);
89
        $this->setSelectOrder($selectEntity);
90
        $this->setSelectLimit($selectEntity);
91
        $this->setPrimaryKey($selectEntity);
92
93
        // $set = null;
94
        // if(isset($rights["insert"]) || !this->driver->support("table")) {
95
        //     $set = "";
96
        //     foreach((array) $queryOptions["where"] as $val) {
97
        //         if($foreignKeys[$val["col"]] && count($foreignKeys[$val["col"]]) == 1 && ($val["op"] == "="
98
        //             || (!$val["op"] && !preg_match('~[_%]~', $val["val"])) // LIKE in Editor
99
        //         )) {
100
        //             $set .= "&set" . urlencode("[" . $this->driver->bracketEscape($val["col"]) . "]") . "=" . urlencode($val["val"]);
101
        //         }
102
        //     }
103
        // }
104
        // $this->page->selectLinks($tableStatus, $set);
105
106
        // if($page == "last")
107
        // {
108
        //     $isGroup = count($group) < count($select);
109
        //     $found_rows = $this->driver->result($this->driver->getRowCountQuery($table, $where, $isGroup, $group));
110
        //     $page = \floor(\max(0, $found_rows - 1) / $limit);
111
        // }
112
113
        $this->options()->setSelectOptions($selectEntity);
114
        $this->setSelectEntity($selectEntity);
115
        $this->setSelectQuery($selectEntity);
116
117
        return $selectEntity;
118
    }
119
120
    /**
121
     * Find out foreign keys for each column
122
     *
123
     * @param SelectEntity $selectEntity
124
     *
125
     * @return void
126
     */
127
    private function setForeignKeys(SelectEntity $selectEntity): void
128
    {
129
        $selectEntity->foreignKeys = [];
130
        foreach ($this->driver->foreignKeys($selectEntity->table) as $foreignKey) {
131
            foreach ($foreignKey->source as $val) {
132
                $selectEntity->foreignKeys[$val][] = $foreignKey;
133
            }
134
        }
135
    }
136
137
    /**
138
     * @param array $value
139
     *
140
     * @return bool
141
     */
142
    private function colHasValidValue(array $value): bool
143
    {
144
        return $value['fun'] === 'count' ||
145
            ($value['col'] !== '' && (!$value['fun'] ||
146
                in_array($value['fun'], $this->driver->functions()) ||
147
                in_array($value['fun'], $this->driver->grouping())));
148
    }
149
150
    /**
151
     * @param array $where AND conditions
152
     * @param array $foreignKeys
153
     *
154
     * @return bool
155
     */
156
    // private function setSelectEmail(array $where, array $foreignKeys)
157
    // {
158
    //     return false;
159
    // }
160
161
    /**
162
     * Apply SQL function
163
     *
164
     * @param string $function
165
     * @param string $column escaped column identifier
166
     *
167
     * @return string
168
     */
169
    public function applySqlFunction(string $function, string $column): string
170
    {
171
        if (!$function) {
172
            return $column;
173
        }
174
        if ($function === 'unixepoch') {
175
            return "DATETIME($column, '$function')";
176
        }
177
        if ($function === 'count distinct') {
178
            return "COUNT(DISTINCT $column)";
179
        }
180
        return strtoupper($function) . "($column)";
181
    }
182
183
    /**
184
     * @param SelectEntity $selectEntity
185
     *
186
     * @return void
187
     */
188
    private function setSelectColumns(SelectEntity $selectEntity): void
189
    {
190
        $selectEntity->select = []; // select expressions, empty for *
191
        $selectEntity->group = []; // expressions without aggregation - will be used for GROUP BY if an aggregation function is used
192
        $values = $this->utils->input->values;
193
        foreach ($values['columns'] as $key => $value) {
194
            if ($this->colHasValidValue($value)) {
195
                $column = '*';
196
                if ($value['col'] !== '') {
197
                    $column = $this->driver->escapeId($value['col']);
198
                }
199
                $selectEntity->select[$key] = $this->applySqlFunction($value['fun'], $column);
200
                if (!in_array($value['fun'], $this->driver->grouping())) {
201
                    $selectEntity->group[] = $selectEntity->select[$key];
202
                }
203
            }
204
        }
205
    }
206
207
    /**
208
     * @param array $value
209
     * @param array $fields
210
     *
211
     * @return string
212
     */
213
    private function getWhereCondition(array $value, array $fields): string
214
    {
215
        $op = $value['op'];
216
        $val = $value['val'];
217
        $col = $value['col'];
218
        // Todo: use match
219
        if (preg_match('~IN$~', $op)) {
220
            $in = $this->driver->processLength($val);
221
            return " $op " . ($in !== '' ? $in : '(NULL)');
222
        }
223
        if ($op === 'SQL') {
224
            return ' ' . $val; // SQL injection
225
        }
226
        if ($op === 'LIKE %%') {
227
            return ' LIKE ' . $this->page->getUnconvertedFieldValue($fields[$col], "%$val%");
228
        }
229
        if ($op === 'ILIKE %%') {
230
            return ' ILIKE ' . $this->page->getUnconvertedFieldValue($fields[$col], "%$val%");
231
        }
232
        if ($op === 'FIND_IN_SET') {
233
            return ')';
234
        }
235
        if (!preg_match('~NULL$~', $op)) {
236
            return " $op " . $this->page->getUnconvertedFieldValue($fields[$col], $val);
237
        }
238
        return " $op";
239
    }
240
241
    /**
242
     * @param TableFieldEntity $field
243
     * @param array $value
244
     *
245
     * @return bool
246
     */
247
    private function selectFieldIsValid(TableFieldEntity $field, array $value): bool
248
    {
249
        $op = $value['op'];
250
        $val = $value['val'];
251
        $in = preg_match('~IN$~', $op) ? ',' : '';
252
        return (preg_match('~^[-\d.' . $in . ']+$~', $val) ||
253
                !preg_match('~' . $this->driver->numberRegex() . '|bit~', $field->type)) &&
254
            (!preg_match("~[\x80-\xFF]~", $val) ||
255
                preg_match('~char|text|enum|set~', $field->type)) &&
256
            (!preg_match('~date|timestamp~', $field->type) ||
257
                preg_match('~^\d+-\d+-\d+~', $val));
258
    }
259
260
    /**
261
     * @param array $value
262
     * @param array $fields
263
     *
264
     * @return string
265
     */
266
    private function getSelectExpression(array $value, array $fields): string
267
    {
268
        $op = $value['op'];
269
        $col = $value['col'];
270
        $prefix = '';
271
        if ($op === 'FIND_IN_SET') {
272
            $prefix = $op .'(' . $this->driver->quote($value['val']) . ', ';
273
        }
274
        $condition = $this->getWhereCondition($value, $fields);
275
        if ($col !== '') {
276
            return $prefix . $this->driver->convertSearch($this->driver->escapeId($col),
277
                    $value, $fields[$col]) . $condition;
278
        }
279
        // find anywhere
280
        $clauses = [];
281
        foreach ($fields as $name => $field) {
282
            if ($this->selectFieldIsValid($field, $value)) {
283
                $clauses[] = $prefix . $this->driver->convertSearch($this->driver->escapeId($name),
284
                        $value, $field) . $condition;
285
            }
286
        }
287
        if (empty($clauses)) {
288
            return '1 = 0';
289
        }
290
        return '(' . implode(' OR ', $clauses) . ')';
291
    }
292
293
    /**
294
     * @param IndexEntity $index
295
     * @param int $i
296
     *
297
     * @return string
298
     */
299
    private function getMatchExpression(IndexEntity $index, int $i): string
300
    {
301
        $columns = array_map(function ($column) {
302
            return $this->driver->escapeId($column);
303
        }, $index->columns);
304
        $fulltext = $this->utils->input->values['fulltext'][$i] ?? '';
305
        $match = $this->driver->quote($fulltext);
306
        if (isset($this->utils->input->values['boolean'][$i])) {
307
            $match .= ' IN BOOLEAN MODE';
308
        }
309
        return 'MATCH (' . implode(', ', $columns) . ') AGAINST (' . $match . ')';
310
    }
311
312
    /**
313
     * @param SelectEntity $selectEntity
314
     *
315
     * @return void
316
     */
317
    private function setSelectWhere(SelectEntity $selectEntity): void
318
    {
319
        $selectEntity->where = [];
320
        foreach ($selectEntity->indexes as $i => $index) {
321
            $fulltext = $this->utils->input->values['fulltext'][$i] ?? '';
322
            if ($index->type === 'FULLTEXT' && $fulltext !== '') {
323
                $selectEntity->where[] = $this->getMatchExpression($index, $i);
324
            }
325
        }
326
        foreach ((array) $this->utils->input->values['where'] as $value) {
327
            if (($value['col'] !== '' ||  $value['val'] !== '') &&
328
                in_array($value['op'], $this->driver->operators())) {
329
                $selectEntity->where[] = $this
330
                    ->getSelectExpression($value, $selectEntity->fields);
331
            }
332
        }
333
    }
334
335
    /**
336
     * @param SelectEntity $selectEntity
337
     *
338
     * @return void
339
     */
340
    private function setSelectOrder(SelectEntity $selectEntity): void
341
    {
342
        $values = $this->utils->input->values;
343
        $selectEntity->order = [];
344
        foreach ($values['order'] as $key => $value) {
345
            if ($value !== '') {
346
                $regexp = '~^((COUNT\(DISTINCT |[A-Z0-9_]+\()(`(?:[^`]|``)+`|"(?:[^"]|"")+")\)|COUNT\(\*\))$~';
347
                if (preg_match($regexp, $value) !== false) {
348
                    $value = $this->driver->escapeId($value);
349
                }
350
                if (isset($values['desc'][$key]) && intval($values['desc'][$key]) !== 0) {
351
                    $value .= ' DESC';
352
                }
353
                $selectEntity->order[] = $value;
354
            }
355
        }
356
    }
357
358
    /**
359
     * @param SelectEntity $selectEntity
360
     *
361
     * @return void
362
     */
363
    private function setSelectLimit(SelectEntity $selectEntity): void
364
    {
365
        $selectEntity->limit = intval($this->utils->input->values['limit'] ?? 50);
366
    }
367
368
    /**
369
     * @param SelectEntity $selectEntity
370
     *
371
     * @return void
372
     */
373
    private function setSelectTextLength(SelectEntity $selectEntity): void
374
    {
375
        $selectEntity->textLength = intval($this->utils->input->values['text_length'] ?? 100);
376
    }
377
378
    /**
379
     * @param SelectEntity $selectEntity
380
     *
381
     * @return void
382
     */
383
    private function setPrimaryKey(SelectEntity $selectEntity): void
384
    {
385
        $primary = null;
386
        $selectEntity->unselected = [];
387
        foreach ($selectEntity->indexes as $index) {
388
            if ($index->type === "PRIMARY") {
389
                $primary = array_flip($index->columns);
390
                $selectEntity->unselected = ($selectEntity->select ? $primary : []);
391
                foreach ($selectEntity->unselected as $key => $val) {
392
                    if (in_array($this->driver->escapeId($key), $selectEntity->select)) {
393
                        unset($selectEntity->unselected[$key]);
394
                    }
395
                }
396
                break;
397
            }
398
        }
399
400
        $oid = $selectEntity->tableStatus->oid;
401
        if ($oid && !$primary) {
402
            /*$primary = */$selectEntity->unselected = [$oid => 0];
403
            // Make an index for the OID
404
            $index = new IndexEntity();
405
            $index->type = "PRIMARY";
406
            $index->columns = [$oid];
407
            $selectEntity->indexes[] = $index;
408
        }
409
    }
410
411
    /**
412
     * @param SelectEntity $selectEntity
413
     *
414
     * @return void
415
     */
416
    public function setSelectQuery(SelectEntity $selectEntity): void
417
    {
418
        $query = $this->driver->buildSelectQuery($selectEntity->tableSelect);
419
        // From adminer.inc.php
420
        $selectEntity->query = str_replace("\n", " ", $query);
421
    }
422
423
    /**
424
     * @param SelectEntity $selectEntity
425
     *
426
     * @return void
427
     */
428
    private function setSelectEntity(SelectEntity $selectEntity): void
429
    {
430
        $select2 = $selectEntity->select;
431
        $group2 = $selectEntity->group;
432
        if (empty($select2)) {
433
            $select2[] = "*";
434
            $convert_fields = $this->driver->convertFields($selectEntity->columns,
435
                $selectEntity->fields, $selectEntity->select);
436
            if ($convert_fields) {
437
                $select2[] = substr($convert_fields, 2);
438
            }
439
        }
440
        foreach ($selectEntity->select as $key => $val) {
441
            $field = $fields[$this->driver->unescapeId($val)] ?? null;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fields does not exist. Did you maybe mean $convert_fields?
Loading history...
442
            if ($field && ($as = $this->driver->convertField($field))) {
443
                $select2[$key] = "$as AS $val";
444
            }
445
        }
446
        $isGroup = count($selectEntity->group) < count($selectEntity->select);
447
        if (!$isGroup && !empty($unselected)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $unselected seems to never exist and therefore empty should always be true.
Loading history...
448
            foreach ($unselected as $key => $val) {
449
                $select2[] = $this->driver->escapeId($key);
450
                if (!empty($group2)) {
451
                    $group2[] = $this->driver->escapeId($key);
452
                }
453
            }
454
        }
455
456
        // From driver.inc.php
457
        $selectEntity->tableSelect = new TableSelectEntity($selectEntity->table,
458
            $select2, $selectEntity->where, $group2, $selectEntity->order,
459
            $selectEntity->limit, $selectEntity->page);
460
    }
461
}
462