Passed
Branch main (c57f14)
by Thierry
03:13
created

Util::getSelectFieldValue()   B

Complexity

Conditions 9
Paths 7

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 13
c 0
b 0
f 0
nc 7
nop 3
dl 0
loc 22
rs 8.0555
1
<?php
2
3
namespace Lagdo\DbAdmin\Db;
4
5
use Lagdo\DbAdmin\Driver\Entity\IndexEntity;
6
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
7
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
8
use Lagdo\DbAdmin\Driver\Input;
9
use Lagdo\DbAdmin\Driver\TranslatorInterface;
10
use Lagdo\DbAdmin\Driver\UtilInterface;
11
use Lagdo\DbAdmin\Driver\UtilTrait;
0 ignored issues
show
Bug introduced by
The type Lagdo\DbAdmin\Driver\UtilTrait was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
12
13
use function preg_match;
14
use function strlen;
15
use function array_sum;
16
use function is_string;
17
18
class Util implements UtilInterface
19
{
20
    use UtilTrait;
21
    use Traits\UtilTrait;
22
    use Traits\SelectUtilTrait;
23
    use Traits\QueryInputTrait;
24
    use Traits\DumpUtilTrait;
25
26
    /**
27
     * The constructor
28
     *
29
     * @param TranslatorInterface $trans
30
     * @param Input $input
31
     */
32
    public function __construct(TranslatorInterface $trans, Input $input)
33
    {
34
        $this->trans = $trans;
0 ignored issues
show
Bug Best Practice introduced by
The property trans does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
35
        $this->input = $input;
0 ignored issues
show
Bug Best Practice introduced by
The property input does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
36
    }
37
38
    /**
39
     * @inheritDoc
40
     */
41
    public function name(): string
42
    {
43
        return '<a href="https://www.adminer.org/"' . $this->blankTarget() . ' id="h1">Adminer</a>';
44
    }
45
46
    /**
47
     * Get a target="_blank" attribute
48
     *
49
     * @return string
50
     */
51
    public function blankTarget(): string
52
    {
53
        return ' target="_blank" rel="noreferrer noopener"';
54
    }
55
56
    /**
57
     * Find unique identifier of a row
58
     *
59
     * @param array $row
60
     * @param array $indexes Result of indexes()
61
     *
62
     * @return array
63
     */
64
    public function uniqueIds(array $row, array $indexes): array
65
    {
66
        foreach ($indexes as $index) {
67
            if (preg_match('~PRIMARY|UNIQUE~', $index->type)) {
68
                $ids = [];
69
                foreach ($index->columns as $key) {
70
                    if (!isset($row[$key])) { // NULL is ambiguous
71
                        continue 2;
72
                    }
73
                    $ids[$key] = $row[$key];
74
                }
75
                return $ids;
76
            }
77
        }
78
        return [];
79
    }
80
81
    /**
82
     * Table caption used in navigation and headings
83
     *
84
     * @param TableEntity $table
85
     *
86
     * @return string
87
     */
88
    public function tableName(TableEntity $table): string
89
    {
90
        return $this->html($table->name);
91
    }
92
93
    /**
94
     * Field caption used in select and edit
95
     *
96
     * @param TableFieldEntity $field Single field returned from fields()
97
     * @param int $order Order of column in select
98
     *
99
     * @return string
100
     */
101
    public function fieldName(TableFieldEntity $field, /** @scrutinizer ignore-unused */ int $order = 0): string
102
    {
103
        return '<span title="' . $this->html($field->fullType) . '">' . $this->html($field->name) . '</span>';
104
    }
105
106
    /**
107
     * @param TableFieldEntity $field
108
     * @param array $values First entries
109
     * @param bool $update
110
     *
111
     * @return string[]
112
     */
113
    private function getEditFunctionNames(TableFieldEntity $field, array $values, bool $update): array
114
    {
115
        $names = $values;
116
        foreach ($this->driver->editFunctions() as $key => $functions) {
117
            if (!$key || (!isset($this->input->values['call']) && $update)) { // relative functions
118
                foreach ($functions as $pattern => $value) {
119
                    if (!$pattern || preg_match("~$pattern~", $field->type)) {
120
                        $names[] = $value;
121
                    }
122
                }
123
            }
124
            if ($key && !preg_match('~set|blob|bytea|raw|file|bool~', $field->type)) {
125
                $names[] = 'SQL';
126
            }
127
        }
128
        return $names;
129
    }
130
131
    /**
132
     * Functions displayed in edit form
133
     *
134
     * @param TableFieldEntity $field Single field from fields()
135
     *
136
     * @return array
137
     */
138
    public function editFunctions(TableFieldEntity $field): array
139
    {
140
        $update = isset($this->input->values['select']); // || $this->where([]);
141
        if ($field->autoIncrement && !$update) {
142
            return [$this->trans->lang('Auto Increment')];
143
        }
144
145
        $names = ($field->null ? ['NULL', ''] : ['']);
146
        return $this->getEditFunctionNames($field, $names, $update);
147
    }
148
149
    /**
150
     * Value printed in select table
151
     *
152
     * @param mixed $value HTML-escaped value to print
153
     * @param string $type Field type
154
     * @param mixed $original Original value before escaping
155
     *
156
     * @return string
157
     */
158
    private function getSelectFieldValue($value, string $type, $original): string
159
    {
160
        if ($value === null) {
161
            return '<i>NULL</i>';
162
        }
163
        if (preg_match('~char|binary|boolean~', $type) && !preg_match('~var~', $type)) {
164
            return "<code>$value</code>";
165
        }
166
        if (preg_match('~blob|bytea|raw|file~', $type) && !$this->isUtf8($value)) {
167
            return '<i>' . $this->trans->lang('%d byte(s)', strlen($original)) . '</i>';
168
        }
169
        if (preg_match('~json~', $type)) {
170
            return "<code>$value</code>";
171
        }
172
        if ($this->isMail($value)) {
173
            return '<a href="' . $this->html("mailto:$value") . '">' . $value . '</a>';
174
        }
175
        elseif ($this->isUrl($value)) {
176
            // IE 11 and all modern browsers hide referrer
177
            return '<a href="' . $this->html($value) . '"' . $this->blankTarget() . '>' . $value . '</a>';
178
        }
179
        return $value;
180
    }
181
182
    /**
183
     * Format value to use in select
184
     *
185
     * @param TableFieldEntity $field
186
     * @param mixed $value
187
     * @param int|string|null $textLength
188
     *
189
     * @return string
190
     */
191
    public function selectValue(TableFieldEntity $field, $value, $textLength): string
192
    {
193
        // if (\is_array($value)) {
194
        //     $expression = '';
195
        //     foreach ($value as $k => $v) {
196
        //         $expression .= '<tr>' . ($value != \array_values($value) ?
197
        //             '<th>' . $this->html($k) :
198
        //             '') . '<td>' . $this->selectValue($field, $v, $textLength);
199
        //     }
200
        //     return "<table cellspacing='0'>$expression</table>";
201
        // }
202
        // if (!$link) {
203
        //     $link = $this->selectLink($value, $field);
204
        // }
205
        $expression = $value;
206
        if (!empty($expression)) {
207
            if (!$this->isUtf8($expression)) {
208
                $expression = "\0"; // htmlspecialchars of binary data returns an empty string
209
            } elseif ($textLength != '' && $this->isShortable($field)) {
210
                // usage of LEFT() would reduce traffic but complicate query -
211
                // expected average speedup: .001 s VS .01 s on local network
212
                $expression = $this->shortenUtf8($expression, \max(0, +$textLength));
213
            } else {
214
                $expression = $this->html($expression);
215
            }
216
        }
217
        return $this->getSelectFieldValue($expression, $field->type, $value);
218
    }
219
220
    /**
221
     * @param array $value
222
     * @param array $fields
223
     *
224
     * @return string
225
     */
226
    private function getWhereCondition(array $value, array $fields): string
227
    {
228
        $op = $value['op'];
229
        $val = $value['val'];
230
        $col = $value['col'];
231
        if (preg_match('~IN$~', $op)) {
232
            $in = $this->processLength($val);
233
            return " $op " . ($in !== '' ? $in : '(NULL)');
234
        }
235
        if ($op === 'SQL') {
236
            return ' ' . $val; // SQL injection
237
        }
238
        if ($op === 'LIKE %%') {
239
            return ' LIKE ' . $this->getUnconvertedFieldValue($fields[$col], "%$val%");
240
        }
241
        if ($op === 'ILIKE %%') {
242
            return ' ILIKE ' . $this->getUnconvertedFieldValue($fields[$col], "%$val%");
243
        }
244
        if ($op === 'FIND_IN_SET') {
245
            return ')';
246
        }
247
        if (!preg_match('~NULL$~', $op)) {
248
            return " $op " . $this->getUnconvertedFieldValue($fields[$col], $val);
249
        }
250
        return " $op";
251
    }
252
253
    /**
254
     * @param TableFieldEntity $field
255
     * @param array $value
256
     *
257
     * @return bool
258
     */
259
    private function selectFieldIsValid(TableFieldEntity $field, array $value): bool
260
    {
261
        $op = $value['op'];
262
        $val = $value['val'];
263
        $in = preg_match('~IN$~', $op) ? ',' : '';
264
        return (preg_match('~^[-\d.' . $in . ']+$~', $val) ||
265
                !preg_match('~' . $this->driver->numberRegex() . '|bit~', $field->type)) &&
266
            (!preg_match("~[\x80-\xFF]~", $val) ||
267
                preg_match('~char|text|enum|set~', $field->type)) &&
268
            (!preg_match('~date|timestamp~', $field->type) ||
269
                preg_match('~^\d+-\d+-\d+~', $val));
270
    }
271
272
    /**
273
     * @param array $value
274
     * @param array $fields
275
     *
276
     * @return string
277
     */
278
    private function getSelectExpression(array $value, array $fields): string
279
    {
280
        $op = $value['op'];
281
        $col = $value['col'];
282
        $prefix = '';
283
        if ($op === 'FIND_IN_SET') {
284
            $prefix = $op .'(' . $this->driver->quote($value['val']) . ', ';
285
        }
286
        $condition = $this->getWhereCondition($value, $fields);
287
        if ($col !== '') {
288
            return $prefix . $this->driver->convertSearch($this->driver->escapeId($col),
289
                    $value, $fields[$col]) . $condition;
290
        }
291
        // find anywhere
292
        $clauses = [];
293
        foreach ($fields as $name => $field) {
294
            if ($this->selectFieldIsValid($field, $value)) {
295
                $clauses[] = $prefix . $this->driver->convertSearch($this->driver->escapeId($name),
296
                        $value, $field) . $condition;
297
            }
298
        }
299
        if (empty($clauses)) {
300
            return '1 = 0';
301
        }
302
        return '(' . implode(' OR ', $clauses) . ')';
303
    }
304
305
    /**
306
     * @param IndexEntity $index
307
     * @param int $i
308
     *
309
     * @return string
310
     */
311
    private function getMatchExpression(IndexEntity $index, int $i): string
312
    {
313
        $columns = array_map(function ($column) {
314
            return $this->driver->escapeId($column);
315
        }, $index->columns);
316
        $match = $this->driver->quote($this->input->values['fulltext'][$i]);
317
        if (isset($this->input->values['boolean'][$i])) {
318
            $match .= ' IN BOOLEAN MODE';
319
        }
320
        return 'MATCH (' . implode(', ', $columns) . ') AGAINST (' . $match . ')';
321
    }
322
323
    /**
324
     * Process search box in select
325
     *
326
     * @param array $fields
327
     * @param array $indexes
328
     *
329
     * @return array
330
     */
331
    public function processSelectWhere(array $fields, array $indexes): array
332
    {
333
        $expressions = [];
334
        foreach ($indexes as $i => $index) {
335
            if ($index->type === 'FULLTEXT' && $this->input->values['fulltext'][$i] !== '') {
336
                $expressions[] = $this->getMatchExpression($index, $i);
337
            }
338
        }
339
        foreach ((array) $this->input->values['where'] as $value) {
340
            if (($value['col'] !== '' ||  $value['val'] !== '') &&
341
                in_array($value['op'], $this->driver->operators())) {
342
                $expressions[] = $this->getSelectExpression($value, $fields);
343
            }
344
        }
345
        return $expressions;
346
    }
347
348
    /**
349
     * Create SQL string from field type
350
     *
351
     * @param TableFieldEntity $field
352
     * @param string $collate
353
     *
354
     * @return string
355
     */
356
    private function processType(TableFieldEntity $field, string $collate = 'COLLATE'): string
357
    {
358
        $collation = '';
359
        if (preg_match('~char|text|enum|set~', $field->type) && $field->collation) {
360
            $collation = " $collate " . $this->driver->quote($field->collation);
361
        }
362
        $sign = '';
363
        if (preg_match($this->driver->numberRegex(), $field->type) &&
364
            in_array($field->unsigned, $this->driver->unsigned())) {
365
            $sign = ' ' . $field->unsigned;
366
        }
367
        return ' ' . $field->type . $this->processLength($field->length) . $sign . $collation;
368
    }
369
370
    /**
371
     * Create SQL string from field
372
     *
373
     * @param TableFieldEntity $field Basic field information
374
     * @param TableFieldEntity $typeField Information about field type
375
     *
376
     * @return array
377
     */
378
    public function processField(TableFieldEntity $field, TableFieldEntity $typeField): array
379
    {
380
        $onUpdate = '';
381
        if (preg_match('~timestamp|datetime~', $field->type) && $field->onUpdate) {
382
            $onUpdate = ' ON UPDATE ' . $field->onUpdate;
383
        }
384
        $comment = '';
385
        if ($this->driver->support('comment') && $field->comment !== '') {
386
            $comment = ' COMMENT ' . $this->driver->quote($field->comment);
387
        }
388
        $null = $field->null ? ' NULL' : ' NOT NULL'; // NULL for timestamp
389
        $autoIncrement = $field->autoIncrement ? $this->driver->autoIncrement() : null;
390
        return [$this->driver->escapeId(trim($field->name)), $this->processType($typeField),
391
            $null, $this->driver->defaultValue($field), $onUpdate, $comment, $autoIncrement];
392
    }
393
394
    /**
395
     * @param TableFieldEntity $field
396
     *
397
     * @return string|false
398
     */
399
    private function getBinaryFieldValue(TableFieldEntity $field)
400
    {
401
        if (!$this->iniBool('file_uploads')) {
402
            return false;
403
        }
404
        $idf = $this->bracketEscape($field->name);
405
        $file = $this->getFileContents("fields-$idf");
406
        if (!is_string($file)) {
407
            return false; //! report errors
408
        }
409
        return $this->driver->quoteBinary($file);
410
    }
411
412
    /**
413
     * Process edit input field
414
     *
415
     * @param TableFieldEntity $field
416
     * @param array $inputs The user inputs
417
     *
418
     * @return array|false|float|int|string|null
419
     */
420
    public function processInput(TableFieldEntity $field, array $inputs)
421
    {
422
        $idf = $this->bracketEscape($field->name);
423
        $function = $inputs['function'][$idf] ?? '';
424
        $value = $inputs['fields'][$idf];
425
        if ($field->autoIncrement && $value === '') {
426
            return null;
427
        }
428
        if ($function === 'NULL') {
429
            return 'NULL';
430
        }
431
        if ($field->type === 'enum') {
432
            return $this->getEnumFieldValue($value);
433
        }
434
        if ($function === 'orig') {
435
            return $this->getOrigFieldValue($field);
436
        }
437
        if ($field->type === 'set') {
438
            return array_sum((array) $value);
439
        }
440
        if ($function == 'json') {
441
            return $this->getJsonFieldValue($value);
442
        }
443
        if (preg_match('~blob|bytea|raw|file~', $field->type)) {
444
            return $this->getBinaryFieldValue($field);
445
        }
446
        return $this->getUnconvertedFieldValue($field, $value, $function);
447
    }
448
}
449