Passed
Branch main (6b0a6d)
by Thierry
03:16
created

Util::processSelectLength()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 1
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Lagdo\DbAdmin\Db;
4
5
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
6
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
7
use Lagdo\DbAdmin\Driver\Input;
8
use Lagdo\DbAdmin\Driver\TranslatorInterface;
9
use Lagdo\DbAdmin\Driver\UtilInterface;
10
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...
11
use function intval;
12
13
class Util implements UtilInterface
14
{
15
    use UtilTrait;
16
17
    /**
18
     * The constructor
19
     *
20
     * @param TranslatorInterface $trans
21
     * @param Input $input
22
     */
23
    public function __construct(TranslatorInterface $trans, Input $input)
24
    {
25
        $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...
26
        $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...
27
    }
28
29
    /**
30
     * @inheritDoc
31
     */
32
    public function name(): string
33
    {
34
        return '<a href="https://www.adminer.org/"' . $this->blankTarget() . ' id="h1">Adminer</a>';
35
    }
36
37
    /**
38
     * Get a target="_blank" attribute
39
     *
40
     * @return string
41
     */
42
    public function blankTarget(): string
43
    {
44
        return ' target="_blank" rel="noreferrer noopener"';
45
    }
46
47
    /**
48
     * Get escaped error message
49
     *
50
     * @return string
51
     */
52
    public function error(): string
53
    {
54
        return $this->html($this->driver->error());
55
    }
56
57
    /**
58
     * Check if the string is e-mail address
59
     *
60
     * @param mixed $email
61
     *
62
     * @return bool
63
     */
64
    public function isMail($email): bool
65
    {
66
        if (!\is_string($email)) {
67
            return false;
68
        }
69
        $atom = '[-a-z0-9!#$%&\'*+/=?^_`{|}~]'; // characters of local-name
70
        $domain = '[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])'; // one domain component
71
        $pattern = "$atom+(\\.$atom+)*@($domain?\\.)+$domain";
72
        return \preg_match("(^$pattern(,\\s*$pattern)*\$)i", $email) > 0;
73
    }
74
75
    /**
76
     * Check if the string is URL address
77
     *
78
     * @param mixed $string
79
     *
80
     * @return bool
81
     */
82
    public function isUrl($string): bool
83
    {
84
        $domain = '[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])'; // one domain component //! IDN
85
        //! restrict path, query and fragment characters
86
        return \preg_match("~^(https?)://($domain?\\.)+$domain(:\\d+)?(/.*)?(\\?.*)?(#.*)?\$~i", $string) > 0;
87
    }
88
89
    /**
90
     * Check if field should be shortened
91
     *
92
     * @param TableFieldEntity $field
93
     *
94
     * @return bool
95
     */
96
    public function isShortable(TableFieldEntity $field): bool
97
    {
98
        return \preg_match('~char|text|json|lob|geometry|point|linestring|polygon|string|bytea~', $field->type) > 0;
99
    }
100
101
    /**
102
     * Get INI boolean value
103
     *
104
     * @param string $ini
105
     *
106
     * @return bool
107
     */
108
    public function iniBool(string $ini): bool
109
    {
110
        $value = \ini_get($ini);
111
        return (\preg_match('~^(on|true|yes)$~i', $value) || (int) $value); // boolean values set by php_value are strings
112
    }
113
114
    /**
115
     * Get INI bytes value
116
     *
117
     * @param string
118
     *
119
     * @return int
120
     */
121
    public function iniBytes(string $ini): int
122
    {
123
        $value = \ini_get($ini);
124
        $unit = \strtolower(\substr($value, -1)); // Get the last char
125
        $ival = \intval(\substr($value, 0, -1)); // Remove the last char
126
        switch ($unit) {
127
            case 'g': $value = $ival * 1024 * 1024 * 1024; break;
128
            case 'm': $value = $ival * 1024 * 1024; break;
129
            case 'k': $value = $ival * 1024; break;
130
        }
131
        return \intval($value);
132
    }
133
134
    /**
135
     * Escape column key used in where()
136
     *
137
     * @param string
138
     *
139
     * @return string
140
     */
141
    public function escapeKey(string $key): string
142
    {
143
        if (\preg_match('(^([\w(]+)(' .
144
            \str_replace('_', '.*', \preg_quote($this->driver->escapeId('_'))) . ')([ \w)]+)$)', $key, $match)) {
145
            //! columns looking like functions
146
            return $match[1] . $this->driver->escapeId($this->driver->unescapeId($match[2])) . $match[3]; //! SQL injection
147
        }
148
        return $this->driver->escapeId($key);
149
    }
150
151
    /**
152
     * Compute fields() from input edit data
153
     *
154
     * @return array
155
     */
156
    public function getFieldsFromEdit()
157
    {
158
        $fields = [];
159
        $values = $this->input->values;
160
        foreach ((array) $values['field_keys'] as $key => $value) {
161
            if ($value != '') {
162
                $value = $this->bracketEscape($value);
163
                $values['function'][$value] = $values['field_funs'][$key];
164
                $values['fields'][$value] = $values['field_vals'][$key];
165
            }
166
        }
167
        foreach ((array) $values['fields'] as $key => $value) {
168
            $name = $this->bracketEscape($key, 1); // 1 - back
169
            $fields[$name] = [
170
                'name' => $name,
171
                'privileges' => ['insert' => 1, 'update' => 1],
172
                'null' => 1,
173
                'autoIncrement' => false, // ($key == $this->driver->primaryIdName()),
174
            ];
175
        }
176
        return $fields;
177
    }
178
179
    /**
180
     * Create repeat pattern for preg
181
     *
182
     * @param string $pattern
183
     * @param int $length
184
     *
185
     * @return string
186
     */
187
    public function repeatPattern(string $pattern, int $length)
188
    {
189
        // fix for Compilation failed: number too big in {} quantifier
190
        // can create {0,0} which is OK
191
        return \str_repeat("$pattern{0,65535}", $length / 65535) . "$pattern{0," . ($length % 65535) . '}';
192
    }
193
194
    /**
195
     * Shorten UTF-8 string
196
     *
197
     * @param string $string
198
     * @param int $length
199
     * @param string $suffix
200
     *
201
     * @return string
202
     */
203
    public function shortenUtf8(string $string, int $length = 80, string $suffix = '')
204
    {
205
        if (!\preg_match('(^(' . $this->repeatPattern("[\t\r\n -\x{10FFFF}]", $length) . ')($)?)u', $string, $match)) {
206
            // ~s causes trash in $match[2] under some PHP versions, (.|\n) is slow
207
            \preg_match('(^(' . $this->repeatPattern("[\t\r\n -~]", $length) . ')($)?)', $string, $match);
208
        }
209
        return $this->html($match[1]) . $suffix . (isset($match[2]) ? '' : '<i>…</i>');
210
    }
211
212
    /**
213
     * Escape or unescape string to use inside form []
214
     *
215
     * @param string $idf
216
     * @param bool $back
217
     *
218
     * @return string
219
     */
220
    public function bracketEscape(string $idf, bool $back = false)
221
    {
222
        // escape brackets inside name='x[]'
223
        static $trans = [':' => ':1', ']' => ':2', '[' => ':3', '"' => ':4'];
224
        return \strtr($idf, ($back ? \array_flip($trans) : $trans));
225
    }
226
227
    /**
228
     * Find unique identifier of a row
229
     *
230
     * @param array $row
231
     * @param array $indexes Result of indexes()
232
     *
233
     * @return array
234
     */
235
    public function uniqueIds(array $row, array $indexes)
236
    {
237
        foreach ($indexes as $index) {
238
            if (\preg_match('~PRIMARY|UNIQUE~', $index->type)) {
239
                $ids = [];
240
                foreach ($index->columns as $key) {
241
                    if (!isset($row[$key])) { // NULL is ambiguous
242
                        continue 2;
243
                    }
244
                    $ids[$key] = $row[$key];
245
                }
246
                return $ids;
247
            }
248
        }
249
        return [];
250
    }
251
252
    /**
253
     * Table caption used in navigation and headings
254
     *
255
     * @param TableEntity $table
256
     *
257
     * @return string
258
     */
259
    public function tableName(TableEntity $table)
260
    {
261
        return $this->html($table->name);
262
    }
263
264
    /**
265
     * Field caption used in select and edit
266
     *
267
     * @param TableFieldEntity $field Single field returned from fields()
268
     * @param int $order Order of column in select
269
     *
270
     * @return string
271
     */
272
    public function fieldName(TableFieldEntity $field, /** @scrutinizer ignore-unused */ int $order = 0)
273
    {
274
        return '<span title="' . $this->html($field->fullType) . '">' . $this->html($field->name) . '</span>';
275
    }
276
277
    /**
278
     * Returns export format options
279
     *
280
     * @return array
281
     */
282
    public function dumpFormat()
283
    {
284
        return ['sql' => 'SQL', 'csv' => 'CSV,', 'csv;' => 'CSV;', 'tsv' => 'TSV'];
285
    }
286
287
    /**
288
     * Returns export output options
289
     *
290
     * @return array
291
     */
292
    public function dumpOutput()
293
    {
294
        $output = ['text' => $this->trans->lang('open'), 'file' => $this->trans->lang('save')];
295
        if (\function_exists('gzencode')) {
296
            $output['gz'] = 'gzip';
297
        }
298
        return $output;
299
    }
300
301
    /**
302
     * Set the path of the file for webserver load
303
     *
304
     * @return string
305
     */
306
    public function importServerPath()
307
    {
308
        return 'adminer.sql';
309
    }
310
311
    /**
312
     * Export database structure
313
     *
314
     * @param string
315
     *
316
     * @return null prints data
317
     */
318
    // public function dumpDatabase($database) {
319
    // }
320
321
    /**
322
     * Print before edit form
323
     *
324
     * @param string $table
325
     * @param array $fields
326
     * @param mixed $row
327
     * @param bool $update
328
     *
329
     * @return null
330
     */
331
    // public function editRowPrint(string $table, array $fields, $row, bool $update)
332
    // {
333
    // }
334
335
    /**
336
     * Functions displayed in edit form
337
     *
338
     * @param TableFieldEntity $field Single field from fields()
339
     *
340
     * @return array
341
     */
342
    public function editFunctions(TableFieldEntity $field)
343
    {
344
        $update = isset($this->input->values['select']); // || $this->where([]);
345
        if ($field->autoIncrement && !$update) {
346
            return [$this->trans->lang('Auto Increment')];
347
        }
348
349
        $clauses = ($field->null ? 'NULL/' : '');
350
        foreach ($this->driver->editFunctions() as $key => $functions) {
351
            if (!$key || (!isset($this->input->values['call']) && $update)) { // relative functions
352
                foreach ($functions as $pattern => $value) {
353
                    if (!$pattern || \preg_match("~$pattern~", $field->type)) {
354
                        $clauses .= "/$value";
355
                    }
356
                }
357
            }
358
            if ($key && !\preg_match('~set|blob|bytea|raw|file|bool~', $field->type)) {
359
                $clauses .= '/SQL';
360
            }
361
        }
362
        return \explode('/', $clauses);
363
    }
364
365
    /**
366
     * Get file contents from $_FILES
367
     *
368
     * @param string $key
369
     * @param bool $decompress
370
     *
371
     * @return int|string
372
     */
373
    private function getFile(string $key, bool $decompress = false)
374
    {
375
        $file = $_FILES[$key];
376
        if (!$file) {
377
            return null;
378
        }
379
        foreach ($file as $key => $val) {
380
            $file[$key] = (array) $val;
381
        }
382
        $queries = '';
383
        foreach ($file['error'] as $key => $error) {
384
            if ($error) {
385
                return $error;
386
            }
387
            $name = $file['name'][$key];
388
            $tmpName = $file['tmp_name'][$key];
389
            $content = \file_get_contents($decompress && \preg_match('~\.gz$~', $name) ?
390
                "compress.zlib://$tmpName" : $tmpName); //! may not be reachable because of open_basedir
391
            if ($decompress) {
392
                $start = \substr($content, 0, 3);
393
                if (\function_exists('iconv') && \preg_match("~^\xFE\xFF|^\xFF\xFE~", $start, $regs)) {
394
                    // not ternary operator to save memory
395
                    $content = \iconv('utf-16', 'utf-8', $content);
396
                } elseif ($start == "\xEF\xBB\xBF") { // UTF-8 BOM
397
                    $content = \substr($content, 3);
398
                }
399
                $queries .= $content . "\n\n";
400
            } else {
401
                $queries .= $content;
402
            }
403
        }
404
        //! support SQL files not ending with semicolon
405
        return $queries;
406
    }
407
408
    /**
409
     * Filter length value including enums
410
     *
411
     * @param string $length
412
     *
413
     * @return string
414
     */
415
    public function processLength(string $length)
416
    {
417
        if (!$length) {
418
            return '';
419
        }
420
        $enumLength = $this->driver->enumLength();
421
        return (\preg_match("~^\\s*\\(?\\s*$enumLength(?:\\s*,\\s*$enumLength)*+\\s*\\)?\\s*\$~", $length) &&
422
            \preg_match_all("~$enumLength~", $length, $matches) ? '(' . \implode(',', $matches[0]) . ')' :
423
            \preg_replace('~^[0-9].*~', '(\0)', \preg_replace('~[^-0-9,+()[\]]~', '', $length))
424
        );
425
    }
426
427
    /**
428
     * Create SQL string from field type
429
     *
430
     * @param TableFieldEntity $field
431
     * @param string $collate
432
     *
433
     * @return string
434
     */
435
    private function processType(TableFieldEntity $field, string $collate = 'COLLATE')
436
    {
437
        $values = [
438
            'unsigned' => $field->unsigned,
439
            'collation' => $field->collation,
440
        ];
441
        return ' ' . $field->type . $this->processLength($field->length) .
442
            (\preg_match($this->driver->numberRegex(), $field->type) &&
443
            \in_array($values['unsigned'], $this->driver->unsigned()) ?
444
            " {$values['unsigned']}" : '') . (\preg_match('~char|text|enum|set~', $field->type) &&
445
            $values['collation'] ? " $collate " . $this->driver->quote($values['collation']) : '');
446
    }
447
448
    /**
449
     * Create SQL string from field
450
     *
451
     * @param TableFieldEntity $field Basic field information
452
     * @param TableFieldEntity $typeField Information about field type
453
     *
454
     * @return array
455
     */
456
    public function processField(TableFieldEntity $field, TableFieldEntity $typeField)
457
    {
458
        return [
459
            $this->driver->escapeId(trim($field->name)),
460
            $this->processType($typeField),
461
            ($field->null ? ' NULL' : ' NOT NULL'), // NULL for timestamp
462
            $this->driver->defaultValue($field),
463
            (\preg_match('~timestamp|datetime~', $field->type) && $field->onUpdate ?
464
                " ON UPDATE {$field->onUpdate}" : ''),
465
            ($this->driver->support('comment') && $field->comment != '' ?
466
                ' COMMENT ' . $this->driver->quote($field->comment) : ''),
467
            ($field->autoIncrement ? $this->driver->autoIncrement() : null),
468
        ];
469
    }
470
471
    /**
472
     * Process edit input field
473
     *
474
     * @param TableFieldEntity $field
475
     * @param array $inputs The user inputs
476
     *
477
     * @return mixed
478
     */
479
    public function processInput(TableFieldEntity $field, array $inputs)
480
    {
481
        $idf = $this->bracketEscape($field->name);
482
        $function = $inputs['function'][$idf] ?? '';
483
        $value = $inputs['fields'][$idf];
484
        if ($field->type == 'enum') {
485
            if ($value == -1) {
486
                return false;
487
            }
488
            if ($value == '') {
489
                return 'NULL';
490
            }
491
            return +$value;
492
        }
493
        if ($field->autoIncrement && $value == '') {
494
            return null;
495
        }
496
        if ($function == 'orig') {
497
            return (\preg_match('~^CURRENT_TIMESTAMP~i', $field->onUpdate) ?
498
                $this->driver->escapeId($field->name) : false);
499
        }
500
        if ($function == 'NULL') {
501
            return 'NULL';
502
        }
503
        if ($field->type == 'set') {
504
            return \array_sum((array) $value);
505
        }
506
        if ($function == 'json') {
507
            $value = \json_decode($value, true);
508
            if (!\is_array($value)) {
509
                return false; //! report errors
510
            }
511
            return $value;
512
        }
513
        if (\preg_match('~blob|bytea|raw|file~', $field->type) && $this->iniBool('file_uploads')) {
514
            $file = $this->getFile("fields-$idf");
515
            if (!\is_string($file)) {
516
                return false; //! report errors
517
            }
518
            return $this->driver->quoteBinary($file);
519
        }
520
        return $this->_processInput($field, $value, $function);
521
    }
522
523
    /**
524
     * Apply SQL function
525
     *
526
     * @param string $function
527
     * @param string $column escaped column identifier
528
     *
529
     * @return string
530
     */
531
    public function applySqlFunction(string $function, string $column)
532
    {
533
        return ($function ? ($function == 'unixepoch' ?
534
            "DATETIME($column, '$function')" : ($function == 'count distinct' ?
535
            'COUNT(DISTINCT ' : strtoupper("$function(")) . "$column)") : $column);
536
    }
537
538
    /**
539
     * Process columns box in select
540
     *
541
     * @return array
542
     */
543
    public function processSelectColumns()
544
    {
545
        $select = []; // select expressions, empty for *
546
        $group = []; // expressions without aggregation - will be used for GROUP BY if an aggregation function is used
547
        foreach ((array) $this->input->values['columns'] as $key => $val) {
548
            if ($val['fun'] == 'count' ||
549
                ($val['col'] != '' && (!$val['fun'] ||
550
                \in_array($val['fun'], $this->driver->functions()) ||
551
                \in_array($val['fun'], $this->driver->grouping())))) {
552
                $select[$key] = $this->applySqlFunction(
553
                    $val['fun'],
554
                    ($val['col'] != '' ? $this->driver->escapeId($val['col']) : '*')
555
                );
556
                if (!in_array($val['fun'], $this->driver->grouping())) {
557
                    $group[] = $select[$key];
558
                }
559
            }
560
        }
561
        return [$select, $group];
562
    }
563
564
    /**
565
     * Process sent input
566
     *
567
     * @param TableFieldEntity $field Single field from fields()
568
     * @param string $value
569
     * @param string $function
570
     *
571
     * @return string
572
     */
573
    private function _processInput(TableFieldEntity $field, string $value, string $function = '')
574
    {
575
        if ($function == 'SQL') {
576
            return $value; // SQL injection
577
        }
578
        $name = $field->name;
579
        $expression = $this->driver->quote($value);
580
        if (\preg_match('~^(now|getdate|uuid)$~', $function)) {
581
            $expression = "$function()";
582
        } elseif (\preg_match('~^current_(date|timestamp)$~', $function)) {
583
            $expression = $function;
584
        } elseif (\preg_match('~^([+-]|\|\|)$~', $function)) {
585
            $expression = $this->driver->escapeId($name) . " $function $expression";
586
        } elseif (\preg_match('~^[+-] interval$~', $function)) {
587
            $expression = $this->driver->escapeId($name) . " $function " .
588
                (\preg_match("~^(\\d+|'[0-9.: -]') [A-Z_]+\$~i", $value) ? $value : $expression);
589
        } elseif (\preg_match('~^(addtime|subtime|concat)$~', $function)) {
590
            $expression = "$function(" . $this->driver->escapeId($name) . ", $expression)";
591
        } elseif (\preg_match('~^(md5|sha1|password|encrypt)$~', $function)) {
592
            $expression = "$function($expression)";
593
        }
594
        return $this->driver->unconvertField($field, $expression);
595
    }
596
597
    /**
598
     * Process search box in select
599
     *
600
     * @param array $fields
601
     * @param array $indexes
602
     *
603
     * @return array
604
     */
605
    public function processSelectSearch(array $fields, array $indexes)
606
    {
607
        $expressions = [];
608
        foreach ($indexes as $i => $index) {
609
            if ($index->type == 'FULLTEXT' && $this->input->values['fulltext'][$i] != '') {
610
                $columns = \array_map(function ($column) {
611
                    return $this->driver->escapeId($column);
612
                }, $index->columns);
613
                $expressions[] = 'MATCH (' . \implode(', ', $columns) . ') AGAINST (' .
614
                    $this->driver->quote($this->input->values['fulltext'][$i]) .
615
                    (isset($this->input->values['boolean'][$i]) ? ' IN BOOLEAN MODE' : '') . ')';
616
            }
617
        }
618
        foreach ((array) $this->input->values['where'] as $key => $val) {
619
            if ("$val[col]$val[val]" != '' && in_array($val['op'], $this->driver->operators())) {
620
                $prefix = '';
621
                $cond = " $val[op]";
622
                if (\preg_match('~IN$~', $val['op'])) {
623
                    $in = $this->processLength($val['val']);
624
                    $cond .= ' ' . ($in != '' ? $in : '(NULL)');
625
                } elseif ($val['op'] == 'SQL') {
626
                    $cond = " $val[val]"; // SQL injection
627
                } elseif ($val['op'] == 'LIKE %%') {
628
                    $cond = ' LIKE ' . $this->_processInput($fields[$val['col']], "%$val[val]%");
629
                } elseif ($val['op'] == 'ILIKE %%') {
630
                    $cond = ' ILIKE ' . $this->_processInput($fields[$val['col']], "%$val[val]%");
631
                } elseif ($val['op'] == 'FIND_IN_SET') {
632
                    $prefix = "$val[op](" . $this->driver->quote($val['val']) . ', ';
633
                    $cond = ')';
634
                } elseif (!\preg_match('~NULL$~', $val['op'])) {
635
                    $cond .= ' ' . $this->_processInput($fields[$val['col']], $val['val']);
636
                }
637
                if ($val['col'] != '') {
638
                    $expressions[] = $prefix . $this->driver->convertSearch(
639
                        $this->driver->escapeId($val['col']),
640
                        $val,
641
                        $fields[$val['col']]
642
                    ) . $cond;
643
                } else {
644
                    // find anywhere
645
                    $cols = [];
646
                    foreach ($fields as $name => $field) {
647
                        if ((\preg_match('~^[-\d.' . (\preg_match('~IN$~', $val['op']) ? ',' : '') . ']+$~', $val['val']) ||
648
                            !\preg_match('~' . $this->driver->numberRegex() . '|bit~', $field->type)) &&
649
                            (!\preg_match("~[\x80-\xFF]~", $val['val']) || \preg_match('~char|text|enum|set~', $field->type)) &&
650
                            (!\preg_match('~date|timestamp~', $field->type) || \preg_match('~^\d+-\d+-\d+~', $val['val']))
651
                        ) {
652
                            $cols[] = $prefix . $this->driver->convertSearch($this->driver->escapeId($name), $val, $field) . $cond;
653
                        }
654
                    }
655
                    $expressions[] = ($cols ? '(' . \implode(' OR ', $cols) . ')' : '1 = 0');
656
                }
657
            }
658
        }
659
        return $expressions;
660
    }
661
662
    /**
663
     * Process order box in select
664
     *
665
     * @return array
666
     */
667
    public function processSelectOrder(): array
668
    {
669
        $expressions = [];
670
        foreach ((array) $this->input->values['order'] as $key => $val) {
671
            if ($val != '') {
672
                $regexp = '~^((COUNT\(DISTINCT |[A-Z0-9_]+\()(`(?:[^`]|``)+`|"(?:[^"]|"")+")\)|COUNT\(\*\))$~';
673
                $expressions[] = (\preg_match($regexp, $val) ? $val : $this->driver->escapeId($val)) . //! MS SQL uses []
674
                    (isset($this->input->values['desc'][$key]) ? ' DESC' : '');
675
            }
676
        }
677
        return $expressions;
678
    }
679
680
    /**
681
     * Process limit box in select
682
     *
683
     * @return int
684
     */
685
    public function processSelectLimit(): int
686
    {
687
        return (isset($this->input->values['limit']) ? intval($this->input->values['limit']) : 50);
688
    }
689
690
    /**
691
     * Process length box in select
692
     *
693
     * @return int
694
     */
695
    public function processSelectLength(): int
696
    {
697
        return (isset($this->input->values['text_length']) ? intval($this->input->values['text_length']) : 100);
698
    }
699
700
    /**
701
     * Process extras in select form
702
     *
703
     * @param array $where AND conditions
704
     * @param array $foreignKeys
705
     *
706
     * @return bool
707
     */
708
    // public function processSelectEmail(array $where, array $foreignKeys)
709
    // {
710
    //     return false;
711
    // }
712
713
    /**
714
     * Value printed in select table
715
     *
716
     * @param mixed $value HTML-escaped value to print
717
     * @param string $link Link to foreign key
718
     * @param string $type Field type
719
     * @param mixed $original Original value before escaping
720
     *
721
     * @return string
722
     */
723
    private function _selectValue($value, string $link, string $type, $original): string
724
    {
725
        $clause = ($value === null ? '<i>NULL</i>' :
726
            (\preg_match('~char|binary|boolean~', $type) && !\preg_match('~var~', $type) ?
727
            "<code>$value</code>" : $value));
728
        if (\preg_match('~blob|bytea|raw|file~', $type) && !$this->isUtf8($value)) {
729
            $clause = '<i>' . $this->trans->lang('%d byte(s)', \strlen($original)) . '</i>';
730
        }
731
        if (\preg_match('~json~', $type)) {
732
            $clause = "<code class='jush-js'>$clause</code>";
733
        }
734
        return ($link ? "<a href='" . $this->html($link) . "'" .
735
            ($this->isUrl($link) ? $this->blankTarget() : '') . ">$clause</a>" : $clause);
736
    }
737
738
    /**
739
     * Format value to use in select
740
     *
741
     * @param mixed $value
742
     * @param string $link
743
     * @param TableFieldEntity $field
744
     * @param int|string|null $textLength
745
     *
746
     * @return string
747
     */
748
    public function selectValue($value, string $link, TableFieldEntity $field, $textLength): string
749
    {
750
        // if (\is_array($value)) {
751
        //     $expression = '';
752
        //     foreach ($value as $k => $v) {
753
        //         $expression .= '<tr>' . ($value != \array_values($value) ?
754
        //             '<th>' . $this->html($k) :
755
        //             '') . '<td>' . $this->selectValue($v, $link, $field, $textLength);
756
        //     }
757
        //     return "<table cellspacing='0'>$expression</table>";
758
        // }
759
        // if (!$link) {
760
        //     $link = $this->selectLink($value, $field);
761
        // }
762
        if ($link === '') {
763
            if ($this->isMail($value)) {
764
                $link = "mailto:$value";
765
            }
766
            elseif ($this->isUrl($value)) {
767
                $link = $value; // IE 11 and all modern browsers hide referrer
768
            }
769
        }
770
        $expression = $value;
771
        if (!empty($expression)) {
772
            if (!$this->isUtf8($expression)) {
773
                $expression = "\0"; // htmlspecialchars of binary data returns an empty string
774
            } elseif ($textLength != '' && $this->isShortable($field)) {
775
                // usage of LEFT() would reduce traffic but complicate query -
776
                // expected average speedup: .001 s VS .01 s on local network
777
                $expression = $this->shortenUtf8($expression, \max(0, +$textLength));
778
            } else {
779
                $expression = $this->html($expression);
780
            }
781
        }
782
        return $this->_selectValue($expression, $link, $field->type, $value);
783
    }
784
785
    /**
786
     * Query printed in SQL command before execution
787
     *
788
     * @param string $query Query to be executed
789
     *
790
     * @return string
791
     */
792
    public function sqlCommandQuery(string $query)
793
    {
794
        return $this->shortenUtf8(trim($query), 1000);
795
    }
796
}
797