SelectQuery::prepareSelect()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 46
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 17
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 46
rs 9.7
1
<?php
2
3
namespace Lagdo\DbAdmin\Db\Facades\Select;
4
5
use Lagdo\DbAdmin\Admin\Traits\InputFieldTrait;
6
use Lagdo\DbAdmin\Db\Facades\AbstractFacade;
7
use Lagdo\DbAdmin\Driver\Entity\IndexEntity;
8
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
9
use Lagdo\DbAdmin\Driver\Entity\TableSelectEntity;
10
use Lagdo\Facades\Logger;
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
22
class SelectQuery extends AbstractFacade
23
{
24
    use InputFieldTrait;
0 ignored issues
show
Bug introduced by
The trait Lagdo\DbAdmin\Admin\Traits\InputFieldTrait requires the property $name which is not provided by Lagdo\DbAdmin\Db\Facades\Select\SelectQuery.
Loading history...
25
    use SelectTrait;
0 ignored issues
show
Bug introduced by
The trait Lagdo\DbAdmin\Db\Facades\Select\SelectTrait requires the property $str which is not provided by Lagdo\DbAdmin\Db\Facades\Select\SelectQuery.
Loading history...
26
27
    /**
28
     * Get required data for select on tables
29
     *
30
     * @param SelectEntity $selectEntity
31
     *
32
     * @return SelectEntity
33
     * @throws Exception
34
     */
35
    public function prepareSelect(SelectEntity $selectEntity): SelectEntity
36
    {
37
        $this->setDefaultOptions($selectEntity);
38
39
        // From select.inc.php
40
        $selectEntity->fields = $this->driver->fields($selectEntity->table);
0 ignored issues
show
Bug introduced by
The method fields() does not exist on null. ( Ignorable by Annotation )

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

40
        /** @scrutinizer ignore-call */ 
41
        $selectEntity->fields = $this->driver->fields($selectEntity->table);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
41
        $this->setFieldsOptions($selectEntity);
42
        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...
43
            throw new Exception($this->utils->trans->lang('Unable to select the table') .
44
                ($selectEntity->fields ? "." : ": " . $this->driver->error()));
45
        }
46
47
        $selectEntity->indexes = $this->driver->indexes($selectEntity->table);
48
        $this->setForeignKeys($selectEntity);
49
        $this->setSelectColumns($selectEntity);
50
51
        $this->setSelectWhere($selectEntity);
52
        $this->setSelectOrder($selectEntity);
53
        $this->setSelectLimit($selectEntity);
54
        $this->setPrimaryKey($selectEntity);
55
56
        // $set = null;
57
        // if(isset($rights["insert"]) || !this->driver->support("table")) {
58
        //     $set = "";
59
        //     foreach((array) $queryOptions["where"] as $val) {
60
        //         if($foreignKeys[$val["col"]] && count($foreignKeys[$val["col"]]) == 1 && ($val["op"] == "="
61
        //             || (!$val["op"] && !preg_match('~[_%]~', $val["val"])) // LIKE in Editor
62
        //         )) {
63
        //             $set .= "&set" . urlencode("[" . $this->driver->bracketEscape($val["col"]) . "]") . "=" . urlencode($val["val"]);
64
        //         }
65
        //     }
66
        // }
67
        // $this->admin->selectLinks($tableStatus, $set);
68
69
        // if($page == "last")
70
        // {
71
        //     $isGroup = count($group) < count($select);
72
        //     $found_rows = $this->driver->result($this->driver->getRowCountQuery($table, $where, $isGroup, $group));
73
        //     $page = \floor(\max(0, $found_rows - 1) / $limit);
74
        // }
75
76
        $this->setSelectOptions($selectEntity);
77
        $this->setSelectEntity($selectEntity);
78
        $this->setSelectQuery($selectEntity);
79
80
        return $selectEntity;
81
    }
82
83
    /**
84
     * @param SelectEntity $selectEntity
85
     *
86
     * @return void
87
     */
88
    private function setDefaultOptions(SelectEntity $selectEntity): void
89
    {
90
        $defaultOptions = [
91
            'columns' => [],
92
            'where' => [],
93
            'order' => [],
94
            'desc' => [],
95
            'fulltext' => [],
96
            'limit' => '50',
97
            'text_length' => '100',
98
            'page' => '1',
99
        ];
100
        foreach ($defaultOptions as $name => $value) {
101
            if (!isset($this->utils->input->values[$name])) {
102
                $this->utils->input->values[$name] = $value;
103
            }
104
            if (!isset($selectEntity->queryOptions[$name])) {
105
                $selectEntity->queryOptions[$name] = $value;
106
            }
107
        }
108
        $page = intval($selectEntity->queryOptions['page']);
109
        if ($page > 0) {
110
            $page -= 1; // Page numbers start at 0 here, instead of 1.
111
        }
112
        $selectEntity->queryOptions['page'] = $page;
113
        $selectEntity->page = $page;
114
    }
115
116
    /**
117
     * @param SelectEntity $selectEntity
118
     *
119
     * @return void
120
     */
121
    private function setFieldsOptions(SelectEntity $selectEntity): void
122
    {
123
        $selectEntity->rights = []; // privilege => 0
124
        $selectEntity->columns = []; // selectable columns
125
        $selectEntity->textLength = 0;
126
        foreach ($selectEntity->fields as $key => $field) {
127
            $name = $this->admin->fieldName($field);
128
            if (isset($field->privileges["select"]) && $name != "") {
129
                $selectEntity->columns[$key] = html_entity_decode(strip_tags($name), ENT_QUOTES);
130
                if ($this->admin->isShortable($field)) {
131
                    $this->setSelectTextLength($selectEntity);
132
                }
133
            }
134
            $selectEntity->rights[] = $field->privileges;
135
        }
136
    }
137
138
    /**
139
     * Find out foreign keys for each column
140
     *
141
     * @param SelectEntity $selectEntity
142
     *
143
     * @return void
144
     */
145
    private function setForeignKeys(SelectEntity $selectEntity): void
146
    {
147
        $selectEntity->foreignKeys = [];
148
        foreach ($this->driver->foreignKeys($selectEntity->table) as $foreignKey) {
149
            foreach ($foreignKey->source as $val) {
150
                $selectEntity->foreignKeys[$val][] = $foreignKey;
151
            }
152
        }
153
    }
154
155
    /**
156
     * @param array $value
157
     *
158
     * @return bool
159
     */
160
    private function colHasValidValue(array $value): bool
161
    {
162
        return $value['fun'] === 'count' ||
163
            ($value['col'] !== '' && (!$value['fun'] ||
164
                in_array($value['fun'], $this->driver->functions()) ||
165
                in_array($value['fun'], $this->driver->grouping())));
166
    }
167
168
    /**
169
     * @param array $where AND conditions
170
     * @param array $foreignKeys
171
     *
172
     * @return bool
173
     */
174
    // private function setSelectEmail(array $where, array $foreignKeys)
175
    // {
176
    //     return false;
177
    // }
178
179
    /**
180
     * Apply SQL function
181
     *
182
     * @param string $function
183
     * @param string $column escaped column identifier
184
     *
185
     * @return string
186
     */
187
    public function applySqlFunction(string $function, string $column): string
188
    {
189
        if (!$function) {
190
            return $column;
191
        }
192
        if ($function === 'unixepoch') {
193
            return "DATETIME($column, '$function')";
194
        }
195
        if ($function === 'count distinct') {
196
            return "COUNT(DISTINCT $column)";
197
        }
198
        return strtoupper($function) . "($column)";
199
    }
200
201
    /**
202
     * @param SelectEntity $selectEntity
203
     *
204
     * @return void
205
     */
206
    private function setSelectColumns(SelectEntity $selectEntity): void
207
    {
208
        $selectEntity->select = []; // select expressions, empty for *
209
        $selectEntity->group = []; // expressions without aggregation - will be used for GROUP BY if an aggregation function is used
210
        $values = $this->utils->input->values;
211
        foreach ($values['columns'] as $key => $value) {
212
            if ($this->colHasValidValue($value)) {
213
                $column = '*';
214
                if ($value['col'] !== '') {
215
                    $column = $this->driver->escapeId($value['col']);
216
                }
217
                $selectEntity->select[$key] = $this->applySqlFunction($value['fun'], $column);
218
                if (!in_array($value['fun'], $this->driver->grouping())) {
219
                    $selectEntity->group[] = $selectEntity->select[$key];
220
                }
221
            }
222
        }
223
    }
224
225
    /**
226
     * @param array $value
227
     * @param array $fields
228
     *
229
     * @return string
230
     */
231
    private function getWhereCondition(array $value, array $fields): string
232
    {
233
        $op = $value['op'];
234
        $val = $value['val'];
235
        $col = $value['col'];
236
        // Todo: use match
237
        if (preg_match('~IN$~', $op)) {
238
            $in = $this->driver->processLength($val);
239
            return " $op " . ($in !== '' ? $in : '(NULL)');
240
        }
241
        if ($op === 'SQL') {
242
            return ' ' . $val; // SQL injection
243
        }
244
        if ($op === 'LIKE %%') {
245
            return ' LIKE ' . $this->getUnconvertedFieldValue($fields[$col], "%$val%");
246
        }
247
        if ($op === 'ILIKE %%') {
248
            return ' ILIKE ' . $this->getUnconvertedFieldValue($fields[$col], "%$val%");
249
        }
250
        if ($op === 'FIND_IN_SET') {
251
            return ')';
252
        }
253
        if (!preg_match('~NULL$~', $op)) {
254
            return " $op " . $this->getUnconvertedFieldValue($fields[$col], $val);
255
        }
256
        return " $op";
257
    }
258
259
    /**
260
     * @param TableFieldEntity $field
261
     * @param array $value
262
     *
263
     * @return bool
264
     */
265
    private function selectFieldIsValid(TableFieldEntity $field, array $value): bool
266
    {
267
        $op = $value['op'];
268
        $val = $value['val'];
269
        $in = preg_match('~IN$~', $op) ? ',' : '';
270
        return (preg_match('~^[-\d.' . $in . ']+$~', $val) ||
271
                !preg_match('~' . $this->driver->numberRegex() . '|bit~', $field->type)) &&
272
            (!preg_match("~[\x80-\xFF]~", $val) ||
273
                preg_match('~char|text|enum|set~', $field->type)) &&
274
            (!preg_match('~date|timestamp~', $field->type) ||
275
                preg_match('~^\d+-\d+-\d+~', $val));
276
    }
277
278
    /**
279
     * @param array $value
280
     * @param array $fields
281
     *
282
     * @return string
283
     */
284
    private function getSelectExpression(array $value, array $fields): string
285
    {
286
        $op = $value['op'];
287
        $col = $value['col'];
288
        $prefix = '';
289
        if ($op === 'FIND_IN_SET') {
290
            $prefix = $op .'(' . $this->driver->quote($value['val']) . ', ';
291
        }
292
        $condition = $this->getWhereCondition($value, $fields);
293
        if ($col !== '') {
294
            return $prefix . $this->driver->convertSearch($this->driver->escapeId($col),
295
                    $value, $fields[$col]) . $condition;
296
        }
297
        // find anywhere
298
        $clauses = [];
299
        foreach ($fields as $name => $field) {
300
            if ($this->selectFieldIsValid($field, $value)) {
301
                $clauses[] = $prefix . $this->driver->convertSearch($this->driver->escapeId($name),
302
                        $value, $field) . $condition;
303
            }
304
        }
305
        if (empty($clauses)) {
306
            return '1 = 0';
307
        }
308
        return '(' . implode(' OR ', $clauses) . ')';
309
    }
310
311
    /**
312
     * @param IndexEntity $index
313
     * @param int $i
314
     *
315
     * @return string
316
     */
317
    private function getMatchExpression(IndexEntity $index, int $i): string
318
    {
319
        $columns = array_map(function ($column) {
320
            return $this->driver->escapeId($column);
321
        }, $index->columns);
322
        $fulltext = $this->utils->input->values['fulltext'][$i] ?? '';
323
        $match = $this->driver->quote($fulltext);
324
        if (isset($this->utils->input->values['boolean'][$i])) {
325
            $match .= ' IN BOOLEAN MODE';
326
        }
327
        return 'MATCH (' . implode(', ', $columns) . ') AGAINST (' . $match . ')';
328
    }
329
330
    /**
331
     * @param SelectEntity $selectEntity
332
     *
333
     * @return void
334
     */
335
    private function setSelectWhere(SelectEntity $selectEntity): void
336
    {
337
        $selectEntity->where = [];
338
        foreach ($selectEntity->indexes as $i => $index) {
339
            $fulltext = $this->utils->input->values['fulltext'][$i] ?? '';
340
            if ($index->type === 'FULLTEXT' && $fulltext !== '') {
341
                $selectEntity->where[] = $this->getMatchExpression($index, $i);
342
            }
343
        }
344
        foreach ((array) $this->utils->input->values['where'] as $value) {
345
            if (($value['col'] !== '' ||  $value['val'] !== '') &&
346
                in_array($value['op'], $this->driver->operators())) {
347
                $selectEntity->where[] = $this
348
                    ->getSelectExpression($value, $selectEntity->fields);
349
            }
350
        }
351
    }
352
353
    /**
354
     * @param SelectEntity $selectEntity
355
     *
356
     * @return void
357
     */
358
    private function setSelectOrder(SelectEntity $selectEntity): void
359
    {
360
        $values = $this->utils->input->values;
361
        $selectEntity->order = [];
362
        foreach ($values['order'] as $key => $value) {
363
            if ($value !== '') {
364
                $regexp = '~^((COUNT\(DISTINCT |[A-Z0-9_]+\()(`(?:[^`]|``)+`|"(?:[^"]|"")+")\)|COUNT\(\*\))$~';
365
                if (preg_match($regexp, $value) !== false) {
366
                    $value = $this->driver->escapeId($value);
367
                }
368
                if (isset($values['desc'][$key]) && intval($values['desc'][$key]) !== 0) {
369
                    $value .= ' DESC';
370
                }
371
                $selectEntity->order[] = $value;
372
            }
373
        }
374
    }
375
376
    /**
377
     * @param SelectEntity $selectEntity
378
     *
379
     * @return void
380
     */
381
    private function setSelectLimit(SelectEntity $selectEntity): void
382
    {
383
        $selectEntity->limit = intval($this->utils->input->values['limit'] ?? 50);
384
    }
385
386
    /**
387
     * @param SelectEntity $selectEntity
388
     *
389
     * @return void
390
     */
391
    private function setSelectTextLength(SelectEntity $selectEntity): void
392
    {
393
        $selectEntity->textLength = intval($this->utils->input->values['text_length'] ?? 100);
394
    }
395
396
    /**
397
     * @param SelectEntity $selectEntity
398
     *
399
     * @return void
400
     */
401
    private function setPrimaryKey(SelectEntity $selectEntity): void
402
    {
403
        $primary = null;
404
        $selectEntity->unselected = [];
405
        foreach ($selectEntity->indexes as $index) {
406
            if ($index->type === "PRIMARY") {
407
                $primary = array_flip($index->columns);
408
                $selectEntity->unselected = ($selectEntity->select ? $primary : []);
409
                foreach ($selectEntity->unselected as $key => $val) {
410
                    if (in_array($this->driver->escapeId($key), $selectEntity->select)) {
411
                        unset($selectEntity->unselected[$key]);
412
                    }
413
                }
414
                break;
415
            }
416
        }
417
418
        $oid = $selectEntity->tableStatus->oid;
419
        if ($oid && !$primary) {
420
            /*$primary = */$selectEntity->unselected = [$oid => 0];
421
            // Make an index for the OID
422
            $index = new IndexEntity();
423
            $index->type = "PRIMARY";
424
            $index->columns = [$oid];
425
            $selectEntity->indexes[] = $index;
426
        }
427
    }
428
429
    /**
430
     * @param SelectEntity $selectEntity
431
     *
432
     * @return void
433
     */
434
    public function setSelectQuery(SelectEntity $selectEntity): void
435
    {
436
        $query = $this->driver->buildSelectQuery($selectEntity->tableSelect);
437
        // From adminer.inc.php
438
        $selectEntity->query = str_replace("\n", " ", $query);
439
    }
440
441
    /**
442
     * @param SelectEntity $selectEntity
443
     *
444
     * @return void
445
     */
446
    private function setSelectOptions(SelectEntity $selectEntity): void
447
    {
448
        $selectEntity->options = [
449
            'columns' => $this->getColumnsOptions($selectEntity->select,
450
                $selectEntity->columns, $selectEntity->queryOptions),
451
            'filters' => $this->getFiltersOptions($selectEntity->columns,
452
                $selectEntity->indexes, $selectEntity->queryOptions),
453
            'sorting' => $this->getSortingOptions($selectEntity->columns,
454
                $selectEntity->queryOptions),
455
            'limit' => $this->getLimitOptions($selectEntity->limit),
456
            'length' => $this->getLengthOptions($selectEntity->textLength),
457
            // 'action' => $this->getActionOptions($selectEntity->indexes),
458
        ];
459
    }
460
461
    /**
462
     * @param SelectEntity $selectEntity
463
     *
464
     * @return void
465
     */
466
    private function setSelectEntity(SelectEntity $selectEntity): void
467
    {
468
        $select2 = $selectEntity->select;
469
        $group2 = $selectEntity->group;
470
        if (empty($select2)) {
471
            $select2[] = "*";
472
            $convert_fields = $this->driver->convertFields($selectEntity->columns,
473
                $selectEntity->fields, $selectEntity->select);
474
            if ($convert_fields) {
475
                $select2[] = substr($convert_fields, 2);
476
            }
477
        }
478
        foreach ($selectEntity->select as $key => $val) {
479
            $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...
480
            if ($field && ($as = $this->driver->convertField($field))) {
481
                $select2[$key] = "$as AS $val";
482
            }
483
        }
484
        $isGroup = count($selectEntity->group) < count($selectEntity->select);
485
        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...
486
            foreach ($unselected as $key => $val) {
487
                $select2[] = $this->driver->escapeId($key);
488
                if (!empty($group2)) {
489
                    $group2[] = $this->driver->escapeId($key);
490
                }
491
            }
492
        }
493
494
        // From driver.inc.php
495
        $selectEntity->tableSelect = new TableSelectEntity($selectEntity->table,
496
            $select2, $selectEntity->where, $group2, $selectEntity->order,
497
            $selectEntity->limit, $selectEntity->page);
498
    }
499
500
    /**
501
     * Print action box in select
502
     *
503
     * @param array $indexes
504
     *
505
     * @return array
506
     */
507
    // private function getActionOptions(array $indexes)
508
    // {
509
    //     $columns = [];
510
    //     foreach ($indexes as $index) {
511
    //         $current_key = \reset($index->columns);
512
    //         if ($index->type != "FULLTEXT" && $current_key) {
513
    //             $columns[$current_key] = 1;
514
    //         }
515
    //     }
516
    //     $columns[""] = 1;
517
    //     return ['columns' => $columns];
518
    // }
519
520
    /**
521
     * Print command box in select
522
     *
523
     * @return bool whether to print default commands
524
     */
525
    // private function getCommandOptions()
526
    // {
527
    //     return !$this->driver->isInformationSchema($this->driver->database());
528
    // }
529
530
    /**
531
     * Print import box in select
532
     *
533
     * @return bool whether to print default import
534
     */
535
    // private function getImportOptions()
536
    // {
537
    //     return !$this->driver->isInformationSchema($this->driver->database());
538
    // }
539
540
    /**
541
     * Print extra text in the end of a select form
542
     *
543
     * @param array $emailFields Fields holding e-mails
544
     * @param array $columns Selectable columns
545
     *
546
     * @return array
547
     */
548
    // private function getEmailOptions(array $emailFields, array $columns)
549
    // {
550
    // }
551
}
552