Passed
Push — main ( c9a906...21e004 )
by Thierry
01:54
created

Util::_selectValue()   B

Complexity

Conditions 9
Paths 128

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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