Passed
Push — main ( 742ea5...16e90c )
by Thierry
09:19 queued 07:12
created

Util::editHint()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 3
dl 0
loc 3
rs 10
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 string $email
120
     *
121
     * @return bool
122
     */
123
    public function isMail(string $email)
124
    {
125
        $atom = '[-a-z0-9!#$%&\'*+/=?^_`{|}~]'; // characters of local-name
126
        $domain = '[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])'; // one domain component
127
        $pattern = "$atom+(\\.$atom+)*@($domain?\\.)+$domain";
128
        return \is_string($email) && \preg_match("(^$pattern(,\\s*$pattern)*\$)i", $email);
129
    }
130
131
    /**
132
     * Check if the string is URL address
133
     *
134
     * @param mixed $string
135
     *
136
     * @return bool
137
     */
138
    public function isUrl($string)
139
    {
140
        $domain = '[a-z0-9]([-a-z0-9]{0,61}[a-z0-9])'; // one domain component //! IDN
141
        //! restrict path, query and fragment characters
142
        return \preg_match("~^(https?)://($domain?\\.)+$domain(:\\d+)?(/.*)?(\\?.*)?(#.*)?\$~i", $string) > 0;
143
    }
144
145
    /**
146
     * Check if field should be shortened
147
     *
148
     * @param TableFieldEntity $field
149
     *
150
     * @return bool
151
     */
152
    public function isShortable(TableFieldEntity $field)
153
    {
154
        return \preg_match('~char|text|json|lob|geometry|point|linestring|polygon|string|bytea~', $field->type) > 0;
155
    }
156
157
    /**
158
     * @inheritDoc
159
     */
160
    public function iniBool(string $ini)
161
    {
162
        $value = \ini_get($ini);
163
        return (\preg_match('~^(on|true|yes)$~i', $value) || (int) $value); // boolean values set by php_value are strings
164
    }
165
166
    /**
167
     * Get INI bytes value
168
     *
169
     * @param string
170
     *
171
     * @return int
172
     */
173
    public function iniBytes(string $ini)
174
    {
175
        $value = \ini_get($ini);
176
        $unit = \strtolower(\substr($value, -1)); // Get the last char
177
        $ival = \intval(\substr($value, 0, -1)); // Remove the last char
178
        switch ($unit) {
179
            case 'g': $value = $ival * 1024 * 1024 * 1024; break;
180
            case 'm': $value = $ival * 1024 * 1024; break;
181
            case 'k': $value = $ival * 1024; break;
182
        }
183
        return \intval($value);
184
    }
185
186
    /**
187
     * @inheritDoc
188
     */
189
    public function convertEolToHtml(string $string)
190
    {
191
        return \str_replace("\n", '<br>', $string); // nl2br() uses XHTML before PHP 5.3
192
    }
193
194
    /**
195
     * Escape column key used in where()
196
     *
197
     * @param string
198
     *
199
     * @return string
200
     */
201
    public function escapeKey(string $key)
202
    {
203
        if (\preg_match('(^([\w(]+)(' .
204
            \str_replace('_', '.*', \preg_quote($this->driver->escapeId('_'))) . ')([ \w)]+)$)', $key, $match)) {
205
            //! columns looking like functions
206
            return $match[1] . $this->driver->escapeId($this->driver->unescapeId($match[2])) . $match[3]; //! SQL injection
207
        }
208
        return $this->driver->escapeId($key);
209
    }
210
211
    /**
212
     * @inheritDoc
213
     */
214
    public function getFieldsFromEdit()
215
    {
216
        $fields = [];
217
        $values = $this->input->values;
218
        foreach ((array) $values['field_keys'] as $key => $value) {
219
            if ($value != '') {
220
                $value = $this->bracketEscape($value);
221
                $values['function'][$value] = $values['field_funs'][$key];
222
                $values['fields'][$value] = $values['field_vals'][$key];
223
            }
224
        }
225
        foreach ((array) $values['fields'] as $key => $value) {
226
            $name = $this->bracketEscape($key, 1); // 1 - back
227
            $fields[$name] = [
228
                'name' => $name,
229
                'privileges' => ['insert' => 1, 'update' => 1],
230
                'null' => 1,
231
                'autoIncrement' => false, // ($key == $this->driver->primaryIdName()),
232
            ];
233
        }
234
        return $fields;
235
    }
236
237
    /**
238
     * Create repeat pattern for preg
239
     *
240
     * @param string $pattern
241
     * @param int $length
242
     *
243
     * @return string
244
     */
245
    public function repeatPattern(string $pattern, int $length)
246
    {
247
        // fix for Compilation failed: number too big in {} quantifier
248
        // can create {0,0} which is OK
249
        return \str_repeat("$pattern{0,65535}", $length / 65535) . "$pattern{0," . ($length % 65535) . '}';
250
    }
251
252
    /**
253
     * Shorten UTF-8 string
254
     *
255
     * @param string $string
256
     * @param int $length
257
     * @param string $suffix
258
     *
259
     * @return string
260
     */
261
    public function shortenUtf8(string $string, int $length = 80, string $suffix = '')
262
    {
263
        if (!\preg_match('(^(' . $this->repeatPattern("[\t\r\n -\x{10FFFF}]", $length) . ')($)?)u', $string, $match)) {
264
            // ~s causes trash in $match[2] under some PHP versions, (.|\n) is slow
265
            \preg_match('(^(' . $this->repeatPattern("[\t\r\n -~]", $length) . ')($)?)', $string, $match);
266
        }
267
        return $this->html($match[1]) . $suffix . (isset($match[2]) ? '' : '<i>…</i>');
268
    }
269
270
    /**
271
     * Escape or unescape string to use inside form []
272
     *
273
     * @param string $idf
274
     * @param bool $back
275
     *
276
     * @return string
277
     */
278
    public function bracketEscape(string $idf, bool $back = false)
279
    {
280
        // escape brackets inside name='x[]'
281
        static $trans = [':' => ':1', ']' => ':2', '[' => ':3', '"' => ':4'];
282
        return \strtr($idf, ($back ? \array_flip($trans) : $trans));
283
    }
284
285
    /**
286
     * Find unique identifier of a row
287
     *
288
     * @param array $row
289
     * @param array $indexes Result of indexes()
290
     *
291
     * @return array
292
     */
293
    public function uniqueIds(array $row, array $indexes)
294
    {
295
        foreach ($indexes as $index) {
296
            if (\preg_match('~PRIMARY|UNIQUE~', $index->type)) {
297
                $ids = [];
298
                foreach ($index->columns as $key) {
299
                    if (!isset($row[$key])) { // NULL is ambiguous
300
                        continue 2;
301
                    }
302
                    $ids[$key] = $row[$key];
303
                }
304
                return $ids;
305
            }
306
        }
307
        return [];
308
    }
309
310
    /**
311
     * Table caption used in navigation and headings
312
     *
313
     * @param TableEntity $table
314
     *
315
     * @return string
316
     */
317
    public function tableName(TableEntity $table)
318
    {
319
        return $this->html($table->name);
320
    }
321
322
    /**
323
     * Field caption used in select and edit
324
     *
325
     * @param TableFieldEntity $field Single field returned from fields()
326
     * @param int $order Order of column in select
327
     *
328
     * @return string
329
     */
330
    public function fieldName(TableFieldEntity $field, /** @scrutinizer ignore-unused */ int $order = 0)
331
    {
332
        return '<span title="' . $this->html($field->fullType) . '">' . $this->html($field->name) . '</span>';
333
    }
334
335
    /**
336
     * Returns export format options
337
     *
338
     * @return array
339
     */
340
    public function dumpFormat()
341
    {
342
        return ['sql' => 'SQL', 'csv' => 'CSV,', 'csv;' => 'CSV;', 'tsv' => 'TSV'];
343
    }
344
345
    /**
346
     * Returns export output options
347
     *
348
     * @return array
349
     */
350
    public function dumpOutput()
351
    {
352
        $output = ['text' => $this->trans->lang('open'), 'file' => $this->trans->lang('save')];
353
        if (\function_exists('gzencode')) {
354
            $output['gz'] = 'gzip';
355
        }
356
        return $output;
357
    }
358
359
    /**
360
     * Set the path of the file for webserver load
361
     *
362
     * @return string
363
     */
364
    public function importServerPath()
365
    {
366
        return 'adminer.sql';
367
    }
368
369
    /**
370
     * Export database structure
371
     *
372
     * @param string
373
     *
374
     * @return null prints data
375
     */
376
    // public function dumpDatabase($database) {
377
    // }
378
379
    /**
380
     * Print before edit form
381
     *
382
     * @param string $table
383
     * @param array $fields
384
     * @param mixed $row
385
     * @param bool $update
386
     *
387
     * @return null
388
     */
389
    // public function editRowPrint(string $table, array $fields, $row, bool $update)
390
    // {
391
    // }
392
393
    /**
394
     * Functions displayed in edit form
395
     *
396
     * @param TableFieldEntity $field Single field from fields()
397
     *
398
     * @return array
399
     */
400
    public function editFunctions(TableFieldEntity $field)
401
    {
402
        $update = isset($this->input->values['select']); // || $this->where([]);
403
        if ($field->autoIncrement && !$update) {
404
            return [$this->trans->lang('Auto Increment')];
405
        }
406
407
        $clauses = ($field->null ? 'NULL/' : '');
408
        foreach ($this->driver->editFunctions() as $key => $functions) {
409
            if (!$key || (!isset($this->input->values['call']) && $update)) { // relative functions
410
                foreach ($functions as $pattern => $value) {
411
                    if (!$pattern || \preg_match("~$pattern~", $field->type)) {
412
                        $clauses .= "/$value";
413
                    }
414
                }
415
            }
416
            if ($key && !\preg_match('~set|blob|bytea|raw|file|bool~', $field->type)) {
417
                $clauses .= '/SQL';
418
            }
419
        }
420
        return \explode('/', $clauses);
421
    }
422
423
    /**
424
     * Get hint for edit field
425
     *
426
     * @param string $table     Table name
427
     * @param TableFieldEntity $field   Single field from fields()
428
     * @param string $value
429
     *
430
     * @return string
431
     */
432
    // public function editHint(string $table, TableFieldEntity $field, string $value)
433
    // {
434
    //     return '';
435
    // }
436
437
    /**
438
     * Get a link to use in select table
439
     *
440
     * @param string $value     Raw value of the field
441
     * @param TableFieldEntity $field   Single field returned from fields()
442
     *
443
     * @return string|null
444
     */
445
    // private function selectLink(string $value, TableFieldEntity $field)
446
    // {
447
    // }
448
449
    /**
450
     * Value conversion used in select and edit
451
     *
452
     * @param string $value     Raw value of the field
453
     * @param TableFieldEntity $field   Single field returned from fields()
454
     *
455
     * @return string
456
     */
457
    public function editValue(string $value, /** @scrutinizer ignore-unused */ TableFieldEntity $field)
458
    {
459
        return $value;
460
    }
461
462
    /**
463
     * Print enum input field
464
     *
465
     * @param string $type Field type: "radio" or "checkbox"
466
     * @param string $attrs
467
     * @param TableFieldEntity $field
468
     * @param mixed $value int|string|array
469
     * @param string $empty
470
     *
471
     * @return null
472
     */
473
    // public function enum_input(string $type, string $attrs, TableFieldEntity $field, $value, string $empty = null)
474
    // {
475
    //     \preg_match_all("~'((?:[^']|'')*)'~", $field->length, $matches);
476
    //     $input = ($empty !== null ? "<label><input type='$type'$attrs value='$empty'" .
477
    //         ((is_array($value) ? in_array($empty, $value) : $value === 0) ? ' checked' : '') .
478
    //         '><i>' . $this->trans->lang('empty') . '</i></label>' : '');
479
    //     foreach ($matches[1] as $i => $val) {
480
    //         $val = stripcslashes(str_replace("''", "'", $val));
481
    //         $checked = (is_int($value) ? $value == $i+1 : (is_array($value) ? in_array($i+1, $value) : $value === $val));
482
    //         $input .= " <label><input type='$type'$attrs value='" . ($i+1) . "'" .
483
    //             ($checked ? ' checked' : '') . '>' . $this->util->html($adminer->editValue($val, $field)) . '</label>';
484
    //     }
485
    //     return $input;
486
    // }
487
488
    /**
489
     * Get options to display edit field
490
     *
491
     * @param bool $select
492
     * @param TableFieldEntity $field Single field from fields()
493
     * @param string $attrs Attributes to use inside the tag
494
     * @param mixed $value
495
     *
496
     * @return array
497
     */
498
    public function editInput(bool $select, TableFieldEntity $field, string $attrs, $value)
499
    {
500
        if ($field->type !== 'enum') {
501
            return [];
502
        }
503
        $inputs = [];
504
        if (($select)) {
505
            $inputs[] = "<label><input type='radio'$attrs value='-1' checked><i>" .
506
                $this->trans->lang('original') . '</i></label> ';
507
        }
508
        if (($field->null)) {
509
            $inputs[] = "<label><input type='radio'$attrs value=''" .
510
                ($value !== null || ($select) ? '' : ' checked') . '><i>NULL</i></label> ';
511
        }
512
513
        // From functions.inc.php (function enum_input())
514
        $empty = 0; // 0 - empty
515
        $type = 'radio';
516
        $inputs[] = "<label><input type='$type'$attrs value='$empty'" .
517
            ((\is_array($value) ? \in_array($empty, $value) : $value === 0) ? ' checked' : '') .
518
            '><i>' . $this->trans->lang('empty') . '</i></label>';
519
520
        \preg_match_all("~'((?:[^']|'')*)'~", $field->length, $matches);
521
        foreach ($matches[1] as $i => $val) {
522
            $val = \stripcslashes(\str_replace("''", "'", $val));
523
            $checked = (\is_int($value) ? $value == $i + 1 :
524
                (\is_array($value) ? \in_array($i+1, $value) : $value === $val));
525
            $inputs[] = "<label><input type='$type'$attrs value='" . ($i+1) . "'" .
526
                ($checked ? ' checked' : '') . '>' . $this->html($this->editValue($val, $field)) . '</label>';
527
        }
528
529
        return $inputs;
530
    }
531
532
    /**
533
     * Get file contents from $_FILES
534
     *
535
     * @param string $key
536
     * @param bool $decompress
537
     *
538
     * @return int|string
539
     */
540
    private function getFile(string $key, bool $decompress = false)
541
    {
542
        $file = $_FILES[$key];
543
        if (!$file) {
544
            return null;
545
        }
546
        foreach ($file as $key => $val) {
547
            $file[$key] = (array) $val;
548
        }
549
        $queries = '';
550
        foreach ($file['error'] as $key => $error) {
551
            if ($error) {
552
                return $error;
553
            }
554
            $name = $file['name'][$key];
555
            $tmpName = $file['tmp_name'][$key];
556
            $content = \file_get_contents($decompress && \preg_match('~\.gz$~', $name) ?
557
                "compress.zlib://$tmpName" : $tmpName); //! may not be reachable because of open_basedir
558
            if ($decompress) {
559
                $start = \substr($content, 0, 3);
560
                if (\function_exists('iconv') && \preg_match("~^\xFE\xFF|^\xFF\xFE~", $start, $regs)) {
561
                    // not ternary operator to save memory
562
                    $content = \iconv('utf-16', 'utf-8', $content);
563
                } elseif ($start == "\xEF\xBB\xBF") { // UTF-8 BOM
564
                    $content = \substr($content, 3);
565
                }
566
                $queries .= $content . "\n\n";
567
            } else {
568
                $queries .= $content;
569
            }
570
        }
571
        //! support SQL files not ending with semicolon
572
        return $queries;
573
    }
574
575
    /**
576
     * Filter length value including enums
577
     *
578
     * @param string $length
579
     *
580
     * @return string
581
     */
582
    public function processLength(string $length)
583
    {
584
        if (!$length) {
585
            return '';
586
        }
587
        $enumLength = $this->driver->enumLength();
588
        return (\preg_match("~^\\s*\\(?\\s*$enumLength(?:\\s*,\\s*$enumLength)*+\\s*\\)?\\s*\$~", $length) &&
589
            \preg_match_all("~$enumLength~", $length, $matches) ? '(' . \implode(',', $matches[0]) . ')' :
590
            \preg_replace('~^[0-9].*~', '(\0)', \preg_replace('~[^-0-9,+()[\]]~', '', $length))
591
        );
592
    }
593
594
    /**
595
     * Create SQL string from field type
596
     *
597
     * @param TableFieldEntity $field
598
     * @param string $collate
599
     *
600
     * @return string
601
     */
602
    private function processType(TableFieldEntity $field, string $collate = 'COLLATE')
603
    {
604
        $values = [
605
            'unsigned' => $field->unsigned,
606
            'collation' => $field->collation,
607
        ];
608
        return ' ' . $field->type . $this->processLength($field->length) .
609
            (\preg_match($this->driver->numberRegex(), $field->type) &&
610
            \in_array($values['unsigned'], $this->driver->unsigned()) ?
611
            " {$values['unsigned']}" : '') . (\preg_match('~char|text|enum|set~', $field->type) &&
612
            $values['collation'] ? " $collate " . $this->driver->quote($values['collation']) : '');
613
    }
614
615
    /**
616
     * @inheritDoc
617
     */
618
    public function processField(TableFieldEntity $field, TableFieldEntity $typeField)
619
    {
620
        return [
621
            $this->driver->escapeId(trim($field->name)),
622
            $this->processType($typeField),
623
            ($field->null ? ' NULL' : ' NOT NULL'), // NULL for timestamp
624
            $this->driver->defaultValue($field),
625
            (\preg_match('~timestamp|datetime~', $field->type) && $field->onUpdate ?
626
                " ON UPDATE {$field->onUpdate}" : ''),
627
            ($this->driver->support('comment') && $field->comment != '' ?
628
                ' COMMENT ' . $this->driver->quote($field->comment) : ''),
629
            ($field->autoIncrement ? $this->driver->autoIncrement() : null),
630
        ];
631
    }
632
633
    /**
634
     * Process edit input field
635
     *
636
     * @param TableFieldEntity $field
637
     * @param array $inputs The user inputs
638
     *
639
     * @return mixed
640
     */
641
    public function processInput(TableFieldEntity $field, array $inputs)
642
    {
643
        $idf = $this->bracketEscape($field->name);
644
        $function = $inputs['function'][$idf] ?? '';
645
        $value = $inputs['fields'][$idf];
646
        if ($field->type == 'enum') {
647
            if ($value == -1) {
648
                return false;
649
            }
650
            if ($value == '') {
651
                return 'NULL';
652
            }
653
            return +$value;
654
        }
655
        if ($field->autoIncrement && $value == '') {
656
            return null;
657
        }
658
        if ($function == 'orig') {
659
            return (\preg_match('~^CURRENT_TIMESTAMP~i', $field->onUpdate) ?
660
                $this->driver->escapeId($field->name) : false);
661
        }
662
        if ($function == 'NULL') {
663
            return 'NULL';
664
        }
665
        if ($field->type == 'set') {
666
            return \array_sum((array) $value);
667
        }
668
        if ($function == 'json') {
669
            $value = \json_decode($value, true);
670
            if (!\is_array($value)) {
671
                return false; //! report errors
672
            }
673
            return $value;
674
        }
675
        if (\preg_match('~blob|bytea|raw|file~', $field->type) && $this->iniBool('file_uploads')) {
676
            $file = $this->getFile("fields-$idf");
677
            if (!\is_string($file)) {
678
                return false; //! report errors
679
            }
680
            return $this->driver->quoteBinary($file);
0 ignored issues
show
Bug introduced by
The method quoteBinary() does not exist on Lagdo\DbAdmin\Driver\DriverInterface. ( Ignorable by Annotation )

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

680
            return $this->driver->/** @scrutinizer ignore-call */ quoteBinary($file);

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

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

Loading history...
681
        }
682
        return $this->_processInput($field, $value, $function);
683
    }
684
685
    /**
686
     * Apply SQL function
687
     *
688
     * @param string $function
689
     * @param string $column escaped column identifier
690
     *
691
     * @return string
692
     */
693
    public function applySqlFunction(string $function, string $column)
694
    {
695
        return ($function ? ($function == 'unixepoch' ?
696
            "DATETIME($column, '$function')" : ($function == 'count distinct' ?
697
            'COUNT(DISTINCT ' : strtoupper("$function(")) . "$column)") : $column);
698
    }
699
700
    /**
701
     * Process columns box in select
702
     *
703
     * @return array
704
     */
705
    public function processSelectColumns()
706
    {
707
        $select = []; // select expressions, empty for *
708
        $group = []; // expressions without aggregation - will be used for GROUP BY if an aggregation function is used
709
        foreach ((array) $this->input->values['columns'] as $key => $val) {
710
            if ($val['fun'] == 'count' ||
711
                ($val['col'] != '' && (!$val['fun'] ||
712
                \in_array($val['fun'], $this->driver->functions()) ||
713
                \in_array($val['fun'], $this->driver->grouping())))) {
714
                $select[$key] = $this->applySqlFunction(
715
                    $val['fun'],
716
                    ($val['col'] != '' ? $this->driver->escapeId($val['col']) : '*')
717
                );
718
                if (!in_array($val['fun'], $this->driver->grouping())) {
719
                    $group[] = $select[$key];
720
                }
721
            }
722
        }
723
        return [$select, $group];
724
    }
725
726
    /**
727
     * Process sent input
728
     *
729
     * @param TableFieldEntity $field Single field from fields()
730
     * @param string $value
731
     * @param string $function
732
     *
733
     * @return string
734
     */
735
    private function _processInput(TableFieldEntity $field, string $value, string $function = '')
736
    {
737
        if ($function == 'SQL') {
738
            return $value; // SQL injection
739
        }
740
        $name = $field->name;
741
        $expression = $this->driver->quote($value);
742
        if (\preg_match('~^(now|getdate|uuid)$~', $function)) {
743
            $expression = "$function()";
744
        } elseif (\preg_match('~^current_(date|timestamp)$~', $function)) {
745
            $expression = $function;
746
        } elseif (\preg_match('~^([+-]|\|\|)$~', $function)) {
747
            $expression = $this->driver->escapeId($name) . " $function $expression";
748
        } elseif (\preg_match('~^[+-] interval$~', $function)) {
749
            $expression = $this->driver->escapeId($name) . " $function " .
750
                (\preg_match("~^(\\d+|'[0-9.: -]') [A-Z_]+\$~i", $value) ? $value : $expression);
751
        } elseif (\preg_match('~^(addtime|subtime|concat)$~', $function)) {
752
            $expression = "$function(" . $this->driver->escapeId($name) . ", $expression)";
753
        } elseif (\preg_match('~^(md5|sha1|password|encrypt)$~', $function)) {
754
            $expression = "$function($expression)";
755
        }
756
        return $this->driver->unconvertField($field, $expression);
757
    }
758
759
    /**
760
     * Process search box in select
761
     *
762
     * @param array $fields
763
     * @param array $indexes
764
     *
765
     * @return array
766
     */
767
    public function processSelectSearch(array $fields, array $indexes)
768
    {
769
        $expressions = [];
770
        foreach ($indexes as $i => $index) {
771
            if ($index->type == 'FULLTEXT' && $this->input->values['fulltext'][$i] != '') {
772
                $columns = \array_map(function ($column) {
773
                    return $this->driver->escapeId($column);
774
                }, $index->columns);
775
                $expressions[] = 'MATCH (' . \implode(', ', $columns) . ') AGAINST (' .
776
                    $this->driver->quote($this->input->values['fulltext'][$i]) .
777
                    (isset($this->input->values['boolean'][$i]) ? ' IN BOOLEAN MODE' : '') . ')';
778
            }
779
        }
780
        foreach ((array) $this->input->values['where'] as $key => $val) {
781
            if ("$val[col]$val[val]" != '' && in_array($val['op'], $this->driver->operators())) {
782
                $prefix = '';
783
                $cond = " $val[op]";
784
                if (\preg_match('~IN$~', $val['op'])) {
785
                    $in = $this->processLength($val['val']);
786
                    $cond .= ' ' . ($in != '' ? $in : '(NULL)');
787
                } elseif ($val['op'] == 'SQL') {
788
                    $cond = " $val[val]"; // SQL injection
789
                } elseif ($val['op'] == 'LIKE %%') {
790
                    $cond = ' LIKE ' . $this->_processInput($fields[$val['col']], "%$val[val]%");
791
                } elseif ($val['op'] == 'ILIKE %%') {
792
                    $cond = ' ILIKE ' . $this->_processInput($fields[$val['col']], "%$val[val]%");
793
                } elseif ($val['op'] == 'FIND_IN_SET') {
794
                    $prefix = "$val[op](" . $this->driver->quote($val['val']) . ', ';
795
                    $cond = ')';
796
                } elseif (!\preg_match('~NULL$~', $val['op'])) {
797
                    $cond .= ' ' . $this->_processInput($fields[$val['col']], $val['val']);
798
                }
799
                if ($val['col'] != '') {
800
                    $expressions[] = $prefix . $this->driver->convertSearch(
801
                        $this->driver->escapeId($val['col']),
802
                        $val,
803
                        $fields[$val['col']]
804
                    ) . $cond;
805
                } else {
806
                    // find anywhere
807
                    $cols = [];
808
                    foreach ($fields as $name => $field) {
809
                        if ((\preg_match('~^[-\d.' . (\preg_match('~IN$~', $val['op']) ? ',' : '') . ']+$~', $val['val']) ||
810
                            !\preg_match('~' . $this->driver->numberRegex() . '|bit~', $field->type)) &&
811
                            (!\preg_match("~[\x80-\xFF]~", $val['val']) || \preg_match('~char|text|enum|set~', $field->type)) &&
812
                            (!\preg_match('~date|timestamp~', $field->type) || \preg_match('~^\d+-\d+-\d+~', $val['val']))
813
                        ) {
814
                            $cols[] = $prefix . $this->driver->convertSearch($this->driver->escapeId($name), $val, $field) . $cond;
815
                        }
816
                    }
817
                    $expressions[] = ($cols ? '(' . \implode(' OR ', $cols) . ')' : '1 = 0');
818
                }
819
            }
820
        }
821
        return $expressions;
822
    }
823
824
    /**
825
     * Process order box in select
826
     *
827
     * @return array Expressions to join by comma
828
     */
829
    public function processSelectOrder()
830
    {
831
        $expressions = [];
832
        foreach ((array) $this->input->values['order'] as $key => $val) {
833
            if ($val != '') {
834
                $regexp = '~^((COUNT\(DISTINCT |[A-Z0-9_]+\()(`(?:[^`]|``)+`|"(?:[^"]|"")+")\)|COUNT\(\*\))$~';
835
                $expressions[] = (\preg_match($regexp, $val) ? $val : $this->driver->escapeId($val)) . //! MS SQL uses []
836
                    (isset($this->input->values['desc'][$key]) ? ' DESC' : '');
837
            }
838
        }
839
        return $expressions;
840
    }
841
842
    /**
843
     * Process limit box in select
844
     *
845
     * @return string
846
     */
847
    public function processSelectLimit()
848
    {
849
        return (isset($this->input->values['limit']) ? $this->input->values['limit'] : '50');
850
    }
851
852
    /**
853
     * Process length box in select
854
     *
855
     * @return string
856
     */
857
    public function processSelectLength()
858
    {
859
        return (isset($this->input->values['text_length']) ? $this->input->values['text_length'] : '100');
860
    }
861
862
    /**
863
     * Process extras in select form
864
     *
865
     * @param array $where AND conditions
866
     * @param array $foreignKeys
867
     *
868
     * @return bool
869
     */
870
    // public function processSelectEmail(array $where, array $foreignKeys)
871
    // {
872
    //     return false;
873
    // }
874
875
    /**
876
     * Value printed in select table
877
     *
878
     * @param mixed $value HTML-escaped value to print
879
     * @param string $link Link to foreign key
880
     * @param string $type Field type
881
     * @param string $original Original value before applying editValue() and escaping
882
     *
883
     * @return string
884
     */
885
    private function _selectValue($value, string $link, string $type, string $original)
886
    {
887
        $clause = ($value === null ? '<i>NULL</i>' :
888
            (\preg_match('~char|binary|boolean~', $type) && !\preg_match('~var~', $type) ?
889
            "<code>$value</code>" : $value));
890
        if (\preg_match('~blob|bytea|raw|file~', $type) && !$this->isUtf8($value)) {
891
            $clause = '<i>' . $this->trans->lang('%d byte(s)', \strlen($original)) . '</i>';
892
        }
893
        if (\preg_match('~json~', $type)) {
894
            $clause = "<code class='jush-js'>$clause</code>";
895
        }
896
        return ($link ? "<a href='" . $this->html($link) . "'" .
897
            ($this->isUrl($link) ? $this->blankTarget() : '') . ">$clause</a>" : $clause);
898
    }
899
900
    /**
901
     * Format value to use in select
902
     *
903
     * @param mixed $value
904
     * @param string $link
905
     * @param TableFieldEntity $field
906
     * @param int $textLength
907
     *
908
     * @return string
909
     */
910
    public function selectValue($value, string $link, TableFieldEntity $field, int $textLength)
911
    {
912
        // if (\is_array($value)) {
913
        //     $expression = '';
914
        //     foreach ($value as $k => $v) {
915
        //         $expression .= '<tr>' . ($value != \array_values($value) ?
916
        //             '<th>' . $this->html($k) :
917
        //             '') . '<td>' . $this->selectValue($v, $link, $field, $textLength);
918
        //     }
919
        //     return "<table cellspacing='0'>$expression</table>";
920
        // }
921
        // if (!$link) {
922
        //     $link = $this->selectLink($value, $field);
923
        // }
924
        if ($link === '') {
925
            if ($this->isMail($value)) {
926
                $link = "mailto:$value";
927
            }
928
            elseif ($this->isUrl($value)) {
929
                $link = $value; // IE 11 and all modern browsers hide referrer
930
            }
931
            else {
932
                $link = '';
933
            }
934
        }
935
        $expression = $this->editValue($value, $field);
936
        if (!empty($expression)) {
937
            if (!$this->isUtf8($expression)) {
938
                $expression = "\0"; // htmlspecialchars of binary data returns an empty string
939
            } elseif ($textLength != '' && $this->isShortable($field)) {
940
                // usage of LEFT() would reduce traffic but complicate query -
941
                // expected average speedup: .001 s VS .01 s on local network
942
                $expression = $this->shortenUtf8($expression, \max(0, +$textLength));
943
            } else {
944
                $expression = $this->html($expression);
945
            }
946
        }
947
        return $this->_selectValue($expression, $link, $field->type, $value);
948
    }
949
950
    /**
951
     * Query printed in SQL command before execution
952
     *
953
     * @param string $query Query to be executed
954
     *
955
     * @return string
956
     */
957
    public function sqlCommandQuery(string $query)
958
    {
959
        return $this->shortenUtf8(trim($query), 1000);
960
    }
961
}
962