SelectQuery::setForeignKeys()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 6
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 str_replace;
19
20
/**
21
 * Prepare a select query using the user provided options.
22
 */
23
class SelectQuery
24
{
25
    /**
26
     * @var SelectOptions
27
     */
28
    private $selectOptions;
29
30
    /**
31
     * The constructor
32
     *
33
     * @param AppPage $page
34
     * @param DriverInterface $driver
35
     * @param Utils $utils
36
     */
37
    public function __construct(private AppPage $page,
38
        private DriverInterface $driver, private Utils $utils)
39
    {
40
        $this->selectOptions = new SelectOptions($this->driver, $this->utils);
41
    }
42
43
    /**
44
     * @param SelectEntity $selectEntity
45
     *
46
     * @return void
47
     */
48
    private function setFieldsOptions(SelectEntity $selectEntity): void
49
    {
50
        $selectEntity->rights = []; // privilege => 0
51
        $selectEntity->columns = []; // selectable columns
52
        $selectEntity->textLength = 0;
53
        foreach ($selectEntity->fields as $key => $field) {
54
            $name = $this->page->fieldName($field);
55
            if (isset($field->privileges["select"]) && $name != "") {
56
                $selectEntity->columns[$key] = html_entity_decode(strip_tags($name), ENT_QUOTES);
57
                if ($this->page->isShortable($field)) {
58
                    $this->setSelectTextLength($selectEntity);
59
                }
60
            }
61
            $selectEntity->rights[] = $field->privileges;
62
        }
63
    }
64
65
    /**
66
     * Find out foreign keys for each column
67
     *
68
     * @param SelectEntity $selectEntity
69
     *
70
     * @return void
71
     */
72
    private function setForeignKeys(SelectEntity $selectEntity): void
73
    {
74
        $selectEntity->foreignKeys = [];
75
        foreach ($this->driver->foreignKeys($selectEntity->table) as $foreignKey) {
76
            foreach ($foreignKey->source as $val) {
77
                $selectEntity->foreignKeys[$val][] = $foreignKey;
78
            }
79
        }
80
    }
81
82
    /**
83
     * @param array $value
84
     *
85
     * @return bool
86
     */
87
    private function colHasValidValue(array $value): bool
88
    {
89
        return $value['fun'] === 'count' ||
90
            ($value['col'] !== '' && (!$value['fun'] ||
91
                in_array($value['fun'], $this->driver->functions()) ||
92
                in_array($value['fun'], $this->driver->grouping())));
93
    }
94
95
    /**
96
     * @param array $where AND conditions
97
     * @param array $foreignKeys
98
     *
99
     * @return bool
100
     */
101
    // private function setSelectEmail(array $where, array $foreignKeys)
102
    // {
103
    //     return false;
104
    // }
105
106
    /**
107
     * @param SelectEntity $selectEntity
108
     *
109
     * @return void
110
     */
111
    private function setSelectColumns(SelectEntity $selectEntity): void
112
    {
113
        $selectEntity->select = []; // select expressions, empty for *
114
        $selectEntity->group = []; // expressions without aggregation - will be used for GROUP BY if an aggregation function is used
115
        $values = $this->utils->input->values;
116
        foreach ($values['columns'] as $key => $value) {
117
            if ($this->colHasValidValue($value)) {
118
                $column = '*';
119
                if ($value['col'] !== '') {
120
                    $column = $this->driver->escapeId($value['col']);
121
                }
122
                $selectEntity->select[$key] = $this->page->applySqlFunction($value['fun'], $column);
123
                if (!in_array($value['fun'], $this->driver->grouping())) {
124
                    $selectEntity->group[] = $selectEntity->select[$key];
125
                }
126
            }
127
        }
128
    }
129
130
    /**
131
     * @param array $value
132
     * @param array $fields
133
     *
134
     * @return string
135
     */
136
    private function getWhereCondition(array $value, array $fields): string
137
    {
138
        $op = $value['op'];
139
        $val = $value['val'];
140
        $col = $value['col'];
141
        // Todo: use match
142
        if (preg_match('~IN$~', $op)) {
143
            $in = $this->driver->processLength($val);
144
            return " $op " . ($in !== '' ? $in : '(NULL)');
145
        }
146
        if ($op === 'SQL') {
147
            return ' ' . $val; // SQL injection
148
        }
149
        if ($op === 'LIKE %%') {
150
            return ' LIKE ' . $this->page->getUnconvertedFieldValue($fields[$col], "%$val%");
151
        }
152
        if ($op === 'ILIKE %%') {
153
            return ' ILIKE ' . $this->page->getUnconvertedFieldValue($fields[$col], "%$val%");
154
        }
155
        if ($op === 'FIND_IN_SET') {
156
            return ')';
157
        }
158
        if (!preg_match('~NULL$~', $op)) {
159
            return " $op " . $this->page->getUnconvertedFieldValue($fields[$col], $val);
160
        }
161
        return " $op";
162
    }
163
164
    /**
165
     * @param TableFieldEntity $field
166
     * @param array $value
167
     *
168
     * @return bool
169
     */
170
    private function selectFieldIsValid(TableFieldEntity $field, array $value): bool
171
    {
172
        $op = $value['op'];
173
        $val = $value['val'];
174
        $in = preg_match('~IN$~', $op) ? ',' : '';
175
        return (preg_match('~^[-\d.' . $in . ']+$~', $val) ||
176
                !preg_match('~' . $this->driver->numberRegex() . '|bit~', $field->type)) &&
177
            (!preg_match("~[\x80-\xFF]~", $val) ||
178
                preg_match('~char|text|enum|set~', $field->type)) &&
179
            (!preg_match('~date|timestamp~', $field->type) ||
180
                preg_match('~^\d+-\d+-\d+~', $val));
181
    }
182
183
    /**
184
     * @param array $value
185
     * @param array $fields
186
     *
187
     * @return string
188
     */
189
    private function getSelectExpression(array $value, array $fields): string
190
    {
191
        $op = $value['op'];
192
        $col = $value['col'];
193
        $prefix = '';
194
        if ($op === 'FIND_IN_SET') {
195
            $prefix = $op .'(' . $this->driver->quote($value['val']) . ', ';
196
        }
197
        $condition = $this->getWhereCondition($value, $fields);
198
        if ($col !== '') {
199
            return $prefix . $this->driver->convertSearch($this->driver->escapeId($col),
200
                    $value, $fields[$col]) . $condition;
201
        }
202
        // find anywhere
203
        $clauses = [];
204
        foreach ($fields as $name => $field) {
205
            if ($this->selectFieldIsValid($field, $value)) {
206
                $clauses[] = $prefix . $this->driver->convertSearch($this->driver->escapeId($name),
207
                        $value, $field) . $condition;
208
            }
209
        }
210
        if (empty($clauses)) {
211
            return '1 = 0';
212
        }
213
        return '(' . implode(' OR ', $clauses) . ')';
214
    }
215
216
    /**
217
     * @param IndexEntity $index
218
     * @param int $i
219
     *
220
     * @return string
221
     */
222
    private function getMatchExpression(IndexEntity $index, int $i): string
223
    {
224
        $columns = array_map(function ($column) {
225
            return $this->driver->escapeId($column);
226
        }, $index->columns);
227
        $fulltext = $this->utils->input->values['fulltext'][$i] ?? '';
228
        $match = $this->driver->quote($fulltext);
229
        if (isset($this->utils->input->values['boolean'][$i])) {
230
            $match .= ' IN BOOLEAN MODE';
231
        }
232
        return 'MATCH (' . implode(', ', $columns) . ') AGAINST (' . $match . ')';
233
    }
234
235
    /**
236
     * @param SelectEntity $selectEntity
237
     *
238
     * @return void
239
     */
240
    private function setSelectWhere(SelectEntity $selectEntity): void
241
    {
242
        $selectEntity->where = [];
243
        foreach ($selectEntity->indexes as $i => $index) {
244
            $fulltext = $this->utils->input->values['fulltext'][$i] ?? '';
245
            if ($index->type === 'FULLTEXT' && $fulltext !== '') {
246
                $selectEntity->where[] = $this->getMatchExpression($index, $i);
247
            }
248
        }
249
        foreach ((array) $this->utils->input->values['where'] as $value) {
250
            if (($value['col'] !== '' ||  $value['val'] !== '') &&
251
                in_array($value['op'], $this->driver->operators())) {
252
                $selectEntity->where[] = $this
253
                    ->getSelectExpression($value, $selectEntity->fields);
254
            }
255
        }
256
    }
257
258
    /**
259
     * @param SelectEntity $selectEntity
260
     *
261
     * @return void
262
     */
263
    private function setSelectOrder(SelectEntity $selectEntity): void
264
    {
265
        $values = $this->utils->input->values;
266
        $selectEntity->order = [];
267
        foreach ($values['order'] as $key => $value) {
268
            if ($value !== '') {
269
                $regexp = '~^((COUNT\(DISTINCT |[A-Z0-9_]+\()(`(?:[^`]|``)+`|"(?:[^"]|"")+")\)|COUNT\(\*\))$~';
270
                if (preg_match($regexp, $value) !== false) {
271
                    $value = $this->driver->escapeId($value);
272
                }
273
                if (isset($values['desc'][$key]) && intval($values['desc'][$key]) !== 0) {
274
                    $value .= ' DESC';
275
                }
276
                $selectEntity->order[] = $value;
277
            }
278
        }
279
    }
280
281
    /**
282
     * @param SelectEntity $selectEntity
283
     *
284
     * @return void
285
     */
286
    private function setSelectLimit(SelectEntity $selectEntity): void
287
    {
288
        $selectEntity->limit = intval($this->utils->input->values['limit'] ?? 50);
289
    }
290
291
    /**
292
     * @param SelectEntity $selectEntity
293
     *
294
     * @return void
295
     */
296
    private function setSelectTextLength(SelectEntity $selectEntity): void
297
    {
298
        $selectEntity->textLength = intval($this->utils->input->values['text_length'] ?? 100);
299
    }
300
301
    /**
302
     * @param SelectEntity $selectEntity
303
     *
304
     * @return void
305
     */
306
    private function setPrimaryKey(SelectEntity $selectEntity): void
307
    {
308
        $primary = null;
309
        $selectEntity->unselected = [];
310
        foreach ($selectEntity->indexes as $index) {
311
            if ($index->type === "PRIMARY") {
312
                $primary = array_flip($index->columns);
313
                $selectEntity->unselected = ($selectEntity->select ? $primary : []);
314
                foreach ($selectEntity->unselected as $key => $val) {
315
                    if (in_array($this->driver->escapeId($key), $selectEntity->select)) {
316
                        unset($selectEntity->unselected[$key]);
317
                    }
318
                }
319
                break;
320
            }
321
        }
322
323
        $oid = $selectEntity->tableStatus->oid;
324
        if ($oid && !$primary) {
325
            /*$primary = */$selectEntity->unselected = [$oid => 0];
326
            // Make an index for the OID
327
            $index = new IndexEntity();
328
            $index->type = "PRIMARY";
329
            $index->columns = [$oid];
330
            $selectEntity->indexes[] = $index;
331
        }
332
    }
333
334
    /**
335
     * @param SelectEntity $selectEntity
336
     *
337
     * @return void
338
     */
339
    private function setSelectEntity(SelectEntity $selectEntity): void
340
    {
341
        $select2 = $selectEntity->select;
342
        $group2 = $selectEntity->group;
343
        if (empty($select2)) {
344
            $select2[] = "*";
345
            $convert_fields = $this->driver->convertFields($selectEntity->columns,
346
                $selectEntity->fields, $selectEntity->select);
347
            if ($convert_fields) {
348
                $select2[] = substr($convert_fields, 2);
349
            }
350
        }
351
        foreach ($selectEntity->select as $key => $val) {
352
            $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...
353
            if ($field && ($as = $this->driver->convertField($field))) {
354
                $select2[$key] = "$as AS $val";
355
            }
356
        }
357
        $isGroup = count($selectEntity->group) < count($selectEntity->select);
358
        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...
359
            foreach ($unselected as $key => $val) {
360
                $select2[] = $this->driver->escapeId($key);
361
                if (!empty($group2)) {
362
                    $group2[] = $this->driver->escapeId($key);
363
                }
364
            }
365
        }
366
367
        // From driver.inc.php
368
        $selectEntity->tableSelect = new TableSelectEntity($selectEntity->table,
369
            $select2, $selectEntity->where, $group2, $selectEntity->order,
370
            $selectEntity->limit, $selectEntity->page);
371
    }
372
373
    /**
374
     * Get required data for select on tables
375
     *
376
     * @param SelectEntity $selectEntity
377
     *
378
     * @return SelectEntity
379
     * @throws Exception
380
     */
381
    public function prepareSelect(SelectEntity $selectEntity): SelectEntity
382
    {
383
        $this->selectOptions->setDefaultOptions($selectEntity);
384
385
        // From select.inc.php
386
        $selectEntity->fields = $this->driver->fields($selectEntity->table);
387
        $this->setFieldsOptions($selectEntity);
388
        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...
389
            throw new Exception($this->utils->trans->lang('Unable to select the table') .
390
                ($selectEntity->fields ? "." : ": " . $this->driver->error()));
391
        }
392
393
        $selectEntity->indexes = $this->driver->indexes($selectEntity->table);
394
        $this->setForeignKeys($selectEntity);
395
        $this->setSelectColumns($selectEntity);
396
397
        $this->setSelectWhere($selectEntity);
398
        $this->setSelectOrder($selectEntity);
399
        $this->setSelectLimit($selectEntity);
400
        $this->setPrimaryKey($selectEntity);
401
402
        // $set = null;
403
        // if(isset($rights["insert"]) || !this->driver->support("table")) {
404
        //     $set = "";
405
        //     foreach((array) $queryOptions["where"] as $val) {
406
        //         if($foreignKeys[$val["col"]] && count($foreignKeys[$val["col"]]) == 1 && ($val["op"] == "="
407
        //             || (!$val["op"] && !preg_match('~[_%]~', $val["val"])) // LIKE in Editor
408
        //         )) {
409
        //             $set .= "&set" . urlencode("[" . $this->driver->bracketEscape($val["col"]) . "]") . "=" . urlencode($val["val"]);
410
        //         }
411
        //     }
412
        // }
413
        // $this->page->selectLinks($tableStatus, $set);
414
415
        // if($page == "last")
416
        // {
417
        //     $isGroup = count($group) < count($select);
418
        //     $found_rows = $this->driver->result($this->driver->getRowCountQuery($table, $where, $isGroup, $group));
419
        //     $page = \floor(\max(0, $found_rows - 1) / $limit);
420
        // }
421
422
        $this->selectOptions->setSelectOptions($selectEntity);
423
        $this->setSelectEntity($selectEntity);
424
425
        $query = $this->driver->buildSelectQuery($selectEntity->tableSelect);
426
        // From adminer.inc.php
427
        $selectEntity->query = str_replace("\n", " ", $query);
428
429
        return $selectEntity;
430
    }
431
}
432