Passed
Push — main ( 8fee60...168c17 )
by Thierry
03:11 queued 01:20
created

Util::applySqlFunction()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 3
c 0
b 0
f 0
nc 8
nop 2
dl 0
loc 5
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\InputInterface;
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 InputInterface $input
22
     */
23
    public function __construct(TranslatorInterface $trans, InputInterface $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;
0 ignored issues
show
Bug introduced by
Accessing values on the interface Lagdo\DbAdmin\Driver\InputInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
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([]);
0 ignored issues
show
Bug introduced by
Accessing values on the interface Lagdo\DbAdmin\Driver\InputInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
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 hint for edit field
367
     *
368
     * @param string $table     Table name
369
     * @param TableFieldEntity $field   Single field from fields()
370
     * @param string $value
371
     *
372
     * @return string
373
     */
374
    // public function editHint(string $table, TableFieldEntity $field, string $value)
375
    // {
376
    //     return '';
377
    // }
378
379
    /**
380
     * Get a link to use in select table
381
     *
382
     * @param string $value     Raw value of the field
383
     * @param TableFieldEntity $field   Single field returned from fields()
384
     *
385
     * @return string|null
386
     */
387
    // private function selectLink(string $value, TableFieldEntity $field)
388
    // {
389
    // }
390
391
    /**
392
     * Print enum input field
393
     *
394
     * @param string $type Field type: "radio" or "checkbox"
395
     * @param string $attrs
396
     * @param TableFieldEntity $field
397
     * @param mixed $value int|string|array
398
     * @param string $empty
399
     *
400
     * @return null
401
     */
402
    // public function enum_input(string $type, string $attrs, TableFieldEntity $field, $value, string $empty = null)
403
    // {
404
    //     \preg_match_all("~'((?:[^']|'')*)'~", $field->length, $matches);
405
    //     $input = ($empty !== null ? "<label><input type='$type'$attrs value='$empty'" .
406
    //         ((is_array($value) ? in_array($empty, $value) : $value === 0) ? ' checked' : '') .
407
    //         '><i>' . $this->trans->lang('empty') . '</i></label>' : '');
408
    //     foreach ($matches[1] as $i => $val) {
409
    //         $val = stripcslashes(str_replace("''", "'", $val));
410
    //         $checked = (is_int($value) ? $value == $i+1 : (is_array($value) ? in_array($i+1, $value) : $value === $val));
411
    //         $input .= " <label><input type='$type'$attrs value='" . ($i+1) . "'" .
412
    //             ($checked ? ' checked' : '') . '>' . $this->util->html($val) . '</label>';
413
    //     }
414
    //     return $input;
415
    // }
416
417
    /**
418
     * Get options to display edit field
419
     *
420
     * @param bool $select
421
     * @param TableFieldEntity $field Single field from fields()
422
     * @param string $attrs Attributes to use inside the tag
423
     * @param mixed $value
424
     *
425
     * @return array
426
     */
427
    public function editInput(bool $select, TableFieldEntity $field, string $attrs, $value)
428
    {
429
        if ($field->type !== 'enum') {
430
            return [];
431
        }
432
        $inputs = [];
433
        if (($select)) {
434
            $inputs[] = "<label><input type='radio'$attrs value='-1' checked><i>" .
435
                $this->trans->lang('original') . '</i></label> ';
436
        }
437
        if (($field->null)) {
438
            $inputs[] = "<label><input type='radio'$attrs value=''" .
439
                ($value !== null || ($select) ? '' : ' checked') . '><i>NULL</i></label> ';
440
        }
441
442
        // From functions.inc.php (function enum_input())
443
        $empty = 0; // 0 - empty
444
        $type = 'radio';
445
        $inputs[] = "<label><input type='$type'$attrs value='$empty'" .
446
            ((\is_array($value) ? \in_array($empty, $value) : $value === 0) ? ' checked' : '') .
447
            '><i>' . $this->trans->lang('empty') . '</i></label>';
448
449
        \preg_match_all("~'((?:[^']|'')*)'~", $field->length, $matches);
450
        foreach ($matches[1] as $i => $val) {
451
            $val = \stripcslashes(\str_replace("''", "'", $val));
452
            $checked = (\is_int($value) ? $value == $i + 1 :
453
                (\is_array($value) ? \in_array($i+1, $value) : $value === $val));
454
            $inputs[] = "<label><input type='$type'$attrs value='" . ($i+1) . "'" .
455
                ($checked ? ' checked' : '') . '>' . $this->html($val) . '</label>';
456
        }
457
458
        return $inputs;
459
    }
460
461
    /**
462
     * Get file contents from $_FILES
463
     *
464
     * @param string $key
465
     * @param bool $decompress
466
     *
467
     * @return int|string
468
     */
469
    private function getFile(string $key, bool $decompress = false)
470
    {
471
        $file = $_FILES[$key];
472
        if (!$file) {
473
            return null;
474
        }
475
        foreach ($file as $key => $val) {
476
            $file[$key] = (array) $val;
477
        }
478
        $queries = '';
479
        foreach ($file['error'] as $key => $error) {
480
            if ($error) {
481
                return $error;
482
            }
483
            $name = $file['name'][$key];
484
            $tmpName = $file['tmp_name'][$key];
485
            $content = \file_get_contents($decompress && \preg_match('~\.gz$~', $name) ?
486
                "compress.zlib://$tmpName" : $tmpName); //! may not be reachable because of open_basedir
487
            if ($decompress) {
488
                $start = \substr($content, 0, 3);
489
                if (\function_exists('iconv') && \preg_match("~^\xFE\xFF|^\xFF\xFE~", $start, $regs)) {
490
                    // not ternary operator to save memory
491
                    $content = \iconv('utf-16', 'utf-8', $content);
492
                } elseif ($start == "\xEF\xBB\xBF") { // UTF-8 BOM
493
                    $content = \substr($content, 3);
494
                }
495
                $queries .= $content . "\n\n";
496
            } else {
497
                $queries .= $content;
498
            }
499
        }
500
        //! support SQL files not ending with semicolon
501
        return $queries;
502
    }
503
504
    /**
505
     * Filter length value including enums
506
     *
507
     * @param string $length
508
     *
509
     * @return string
510
     */
511
    public function processLength(string $length)
512
    {
513
        if (!$length) {
514
            return '';
515
        }
516
        $enumLength = $this->driver->enumLength();
517
        return (\preg_match("~^\\s*\\(?\\s*$enumLength(?:\\s*,\\s*$enumLength)*+\\s*\\)?\\s*\$~", $length) &&
518
            \preg_match_all("~$enumLength~", $length, $matches) ? '(' . \implode(',', $matches[0]) . ')' :
519
            \preg_replace('~^[0-9].*~', '(\0)', \preg_replace('~[^-0-9,+()[\]]~', '', $length))
520
        );
521
    }
522
523
    /**
524
     * Create SQL string from field type
525
     *
526
     * @param TableFieldEntity $field
527
     * @param string $collate
528
     *
529
     * @return string
530
     */
531
    private function processType(TableFieldEntity $field, string $collate = 'COLLATE')
532
    {
533
        $values = [
534
            'unsigned' => $field->unsigned,
535
            'collation' => $field->collation,
536
        ];
537
        return ' ' . $field->type . $this->processLength($field->length) .
538
            (\preg_match($this->driver->numberRegex(), $field->type) &&
539
            \in_array($values['unsigned'], $this->driver->unsigned()) ?
540
            " {$values['unsigned']}" : '') . (\preg_match('~char|text|enum|set~', $field->type) &&
541
            $values['collation'] ? " $collate " . $this->driver->quote($values['collation']) : '');
542
    }
543
544
    /**
545
     * Create SQL string from field
546
     *
547
     * @param TableFieldEntity $field Basic field information
548
     * @param TableFieldEntity $typeField Information about field type
549
     *
550
     * @return array
551
     */
552
    public function processField(TableFieldEntity $field, TableFieldEntity $typeField)
553
    {
554
        return [
555
            $this->driver->escapeId(trim($field->name)),
556
            $this->processType($typeField),
557
            ($field->null ? ' NULL' : ' NOT NULL'), // NULL for timestamp
558
            $this->driver->defaultValue($field),
559
            (\preg_match('~timestamp|datetime~', $field->type) && $field->onUpdate ?
560
                " ON UPDATE {$field->onUpdate}" : ''),
561
            ($this->driver->support('comment') && $field->comment != '' ?
562
                ' COMMENT ' . $this->driver->quote($field->comment) : ''),
563
            ($field->autoIncrement ? $this->driver->autoIncrement() : null),
564
        ];
565
    }
566
567
    /**
568
     * Process edit input field
569
     *
570
     * @param TableFieldEntity $field
571
     * @param array $inputs The user inputs
572
     *
573
     * @return mixed
574
     */
575
    public function processInput(TableFieldEntity $field, array $inputs)
576
    {
577
        $idf = $this->bracketEscape($field->name);
578
        $function = $inputs['function'][$idf] ?? '';
579
        $value = $inputs['fields'][$idf];
580
        if ($field->type == 'enum') {
581
            if ($value == -1) {
582
                return false;
583
            }
584
            if ($value == '') {
585
                return 'NULL';
586
            }
587
            return +$value;
588
        }
589
        if ($field->autoIncrement && $value == '') {
590
            return null;
591
        }
592
        if ($function == 'orig') {
593
            return (\preg_match('~^CURRENT_TIMESTAMP~i', $field->onUpdate) ?
594
                $this->driver->escapeId($field->name) : false);
595
        }
596
        if ($function == 'NULL') {
597
            return 'NULL';
598
        }
599
        if ($field->type == 'set') {
600
            return \array_sum((array) $value);
601
        }
602
        if ($function == 'json') {
603
            $value = \json_decode($value, true);
604
            if (!\is_array($value)) {
605
                return false; //! report errors
606
            }
607
            return $value;
608
        }
609
        if (\preg_match('~blob|bytea|raw|file~', $field->type) && $this->iniBool('file_uploads')) {
610
            $file = $this->getFile("fields-$idf");
611
            if (!\is_string($file)) {
612
                return false; //! report errors
613
            }
614
            return $this->driver->quoteBinary($file);
615
        }
616
        return $this->_processInput($field, $value, $function);
617
    }
618
619
    /**
620
     * Apply SQL function
621
     *
622
     * @param string $function
623
     * @param string $column escaped column identifier
624
     *
625
     * @return string
626
     */
627
    public function applySqlFunction(string $function, string $column)
628
    {
629
        return ($function ? ($function == 'unixepoch' ?
630
            "DATETIME($column, '$function')" : ($function == 'count distinct' ?
631
            'COUNT(DISTINCT ' : strtoupper("$function(")) . "$column)") : $column);
632
    }
633
634
    /**
635
     * Process columns box in select
636
     *
637
     * @return array
638
     */
639
    public function processSelectColumns()
640
    {
641
        $select = []; // select expressions, empty for *
642
        $group = []; // expressions without aggregation - will be used for GROUP BY if an aggregation function is used
643
        foreach ((array) $this->input->values['columns'] as $key => $val) {
0 ignored issues
show
Bug introduced by
Accessing values on the interface Lagdo\DbAdmin\Driver\InputInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
644
            if ($val['fun'] == 'count' ||
645
                ($val['col'] != '' && (!$val['fun'] ||
646
                \in_array($val['fun'], $this->driver->functions()) ||
647
                \in_array($val['fun'], $this->driver->grouping())))) {
648
                $select[$key] = $this->applySqlFunction(
649
                    $val['fun'],
650
                    ($val['col'] != '' ? $this->driver->escapeId($val['col']) : '*')
651
                );
652
                if (!in_array($val['fun'], $this->driver->grouping())) {
653
                    $group[] = $select[$key];
654
                }
655
            }
656
        }
657
        return [$select, $group];
658
    }
659
660
    /**
661
     * Process sent input
662
     *
663
     * @param TableFieldEntity $field Single field from fields()
664
     * @param string $value
665
     * @param string $function
666
     *
667
     * @return string
668
     */
669
    private function _processInput(TableFieldEntity $field, string $value, string $function = '')
670
    {
671
        if ($function == 'SQL') {
672
            return $value; // SQL injection
673
        }
674
        $name = $field->name;
675
        $expression = $this->driver->quote($value);
676
        if (\preg_match('~^(now|getdate|uuid)$~', $function)) {
677
            $expression = "$function()";
678
        } elseif (\preg_match('~^current_(date|timestamp)$~', $function)) {
679
            $expression = $function;
680
        } elseif (\preg_match('~^([+-]|\|\|)$~', $function)) {
681
            $expression = $this->driver->escapeId($name) . " $function $expression";
682
        } elseif (\preg_match('~^[+-] interval$~', $function)) {
683
            $expression = $this->driver->escapeId($name) . " $function " .
684
                (\preg_match("~^(\\d+|'[0-9.: -]') [A-Z_]+\$~i", $value) ? $value : $expression);
685
        } elseif (\preg_match('~^(addtime|subtime|concat)$~', $function)) {
686
            $expression = "$function(" . $this->driver->escapeId($name) . ", $expression)";
687
        } elseif (\preg_match('~^(md5|sha1|password|encrypt)$~', $function)) {
688
            $expression = "$function($expression)";
689
        }
690
        return $this->driver->unconvertField($field, $expression);
691
    }
692
693
    /**
694
     * Process search box in select
695
     *
696
     * @param array $fields
697
     * @param array $indexes
698
     *
699
     * @return array
700
     */
701
    public function processSelectSearch(array $fields, array $indexes)
702
    {
703
        $expressions = [];
704
        foreach ($indexes as $i => $index) {
705
            if ($index->type == 'FULLTEXT' && $this->input->values['fulltext'][$i] != '') {
0 ignored issues
show
Bug introduced by
Accessing values on the interface Lagdo\DbAdmin\Driver\InputInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
706
                $columns = \array_map(function ($column) {
707
                    return $this->driver->escapeId($column);
708
                }, $index->columns);
709
                $expressions[] = 'MATCH (' . \implode(', ', $columns) . ') AGAINST (' .
710
                    $this->driver->quote($this->input->values['fulltext'][$i]) .
711
                    (isset($this->input->values['boolean'][$i]) ? ' IN BOOLEAN MODE' : '') . ')';
712
            }
713
        }
714
        foreach ((array) $this->input->values['where'] as $key => $val) {
715
            if ("$val[col]$val[val]" != '' && in_array($val['op'], $this->driver->operators())) {
716
                $prefix = '';
717
                $cond = " $val[op]";
718
                if (\preg_match('~IN$~', $val['op'])) {
719
                    $in = $this->processLength($val['val']);
720
                    $cond .= ' ' . ($in != '' ? $in : '(NULL)');
721
                } elseif ($val['op'] == 'SQL') {
722
                    $cond = " $val[val]"; // SQL injection
723
                } elseif ($val['op'] == 'LIKE %%') {
724
                    $cond = ' LIKE ' . $this->_processInput($fields[$val['col']], "%$val[val]%");
725
                } elseif ($val['op'] == 'ILIKE %%') {
726
                    $cond = ' ILIKE ' . $this->_processInput($fields[$val['col']], "%$val[val]%");
727
                } elseif ($val['op'] == 'FIND_IN_SET') {
728
                    $prefix = "$val[op](" . $this->driver->quote($val['val']) . ', ';
729
                    $cond = ')';
730
                } elseif (!\preg_match('~NULL$~', $val['op'])) {
731
                    $cond .= ' ' . $this->_processInput($fields[$val['col']], $val['val']);
732
                }
733
                if ($val['col'] != '') {
734
                    $expressions[] = $prefix . $this->driver->convertSearch(
735
                        $this->driver->escapeId($val['col']),
736
                        $val,
737
                        $fields[$val['col']]
738
                    ) . $cond;
739
                } else {
740
                    // find anywhere
741
                    $cols = [];
742
                    foreach ($fields as $name => $field) {
743
                        if ((\preg_match('~^[-\d.' . (\preg_match('~IN$~', $val['op']) ? ',' : '') . ']+$~', $val['val']) ||
744
                            !\preg_match('~' . $this->driver->numberRegex() . '|bit~', $field->type)) &&
745
                            (!\preg_match("~[\x80-\xFF]~", $val['val']) || \preg_match('~char|text|enum|set~', $field->type)) &&
746
                            (!\preg_match('~date|timestamp~', $field->type) || \preg_match('~^\d+-\d+-\d+~', $val['val']))
747
                        ) {
748
                            $cols[] = $prefix . $this->driver->convertSearch($this->driver->escapeId($name), $val, $field) . $cond;
749
                        }
750
                    }
751
                    $expressions[] = ($cols ? '(' . \implode(' OR ', $cols) . ')' : '1 = 0');
752
                }
753
            }
754
        }
755
        return $expressions;
756
    }
757
758
    /**
759
     * Process order box in select
760
     *
761
     * @return array Expressions to join by comma
762
     */
763
    public function processSelectOrder()
764
    {
765
        $expressions = [];
766
        foreach ((array) $this->input->values['order'] as $key => $val) {
767
            if ($val != '') {
768
                $regexp = '~^((COUNT\(DISTINCT |[A-Z0-9_]+\()(`(?:[^`]|``)+`|"(?:[^"]|"")+")\)|COUNT\(\*\))$~';
769
                $expressions[] = (\preg_match($regexp, $val) ? $val : $this->driver->escapeId($val)) . //! MS SQL uses []
770
                    (isset($this->input->values['desc'][$key]) ? ' DESC' : '');
0 ignored issues
show
Bug introduced by
Accessing values on the interface Lagdo\DbAdmin\Driver\InputInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
771
            }
772
        }
773
        return $expressions;
774
    }
775
776
    /**
777
     * Process limit box in select
778
     *
779
     * @return string
780
     */
781
    public function processSelectLimit()
782
    {
783
        return (isset($this->input->values['limit']) ? $this->input->values['limit'] : '50');
0 ignored issues
show
Bug introduced by
Accessing values on the interface Lagdo\DbAdmin\Driver\InputInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
784
    }
785
786
    /**
787
     * Process length box in select
788
     *
789
     * @return int
790
     */
791
    public function processSelectLength()
792
    {
793
        return (isset($this->input->values['text_length']) ? intval($this->input->values['text_length']) : 100);
0 ignored issues
show
Bug introduced by
Accessing values on the interface Lagdo\DbAdmin\Driver\InputInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
794
    }
795
796
    /**
797
     * Process extras in select form
798
     *
799
     * @param array $where AND conditions
800
     * @param array $foreignKeys
801
     *
802
     * @return bool
803
     */
804
    // public function processSelectEmail(array $where, array $foreignKeys)
805
    // {
806
    //     return false;
807
    // }
808
809
    /**
810
     * Value printed in select table
811
     *
812
     * @param mixed $value HTML-escaped value to print
813
     * @param string $link Link to foreign key
814
     * @param string $type Field type
815
     * @param mixed $original Original value before escaping
816
     *
817
     * @return string
818
     */
819
    private function _selectValue($value, string $link, string $type, $original)
820
    {
821
        $clause = ($value === null ? '<i>NULL</i>' :
822
            (\preg_match('~char|binary|boolean~', $type) && !\preg_match('~var~', $type) ?
823
            "<code>$value</code>" : $value));
824
        if (\preg_match('~blob|bytea|raw|file~', $type) && !$this->isUtf8($value)) {
825
            $clause = '<i>' . $this->trans->lang('%d byte(s)', \strlen($original)) . '</i>';
826
        }
827
        if (\preg_match('~json~', $type)) {
828
            $clause = "<code class='jush-js'>$clause</code>";
829
        }
830
        return ($link ? "<a href='" . $this->html($link) . "'" .
831
            ($this->isUrl($link) ? $this->blankTarget() : '') . ">$clause</a>" : $clause);
832
    }
833
834
    /**
835
     * Format value to use in select
836
     *
837
     * @param mixed $value
838
     * @param string $link
839
     * @param TableFieldEntity $field
840
     * @param int|string|null $textLength
841
     *
842
     * @return string
843
     */
844
    public function selectValue($value, string $link, TableFieldEntity $field, $textLength)
845
    {
846
        // if (\is_array($value)) {
847
        //     $expression = '';
848
        //     foreach ($value as $k => $v) {
849
        //         $expression .= '<tr>' . ($value != \array_values($value) ?
850
        //             '<th>' . $this->html($k) :
851
        //             '') . '<td>' . $this->selectValue($v, $link, $field, $textLength);
852
        //     }
853
        //     return "<table cellspacing='0'>$expression</table>";
854
        // }
855
        // if (!$link) {
856
        //     $link = $this->selectLink($value, $field);
857
        // }
858
        if ($link === '') {
859
            if ($this->isMail($value)) {
860
                $link = "mailto:$value";
861
            }
862
            elseif ($this->isUrl($value)) {
863
                $link = $value; // IE 11 and all modern browsers hide referrer
864
            }
865
            else {
866
                $link = '';
867
            }
868
        }
869
        $expression = $value;
870
        if (!empty($expression)) {
871
            if (!$this->isUtf8($expression)) {
872
                $expression = "\0"; // htmlspecialchars of binary data returns an empty string
873
            } elseif ($textLength != '' && $this->isShortable($field)) {
874
                // usage of LEFT() would reduce traffic but complicate query -
875
                // expected average speedup: .001 s VS .01 s on local network
876
                $expression = $this->shortenUtf8($expression, \max(0, +$textLength));
877
            } else {
878
                $expression = $this->html($expression);
879
            }
880
        }
881
        return $this->_selectValue($expression, $link, $field->type, $value);
882
    }
883
884
    /**
885
     * Query printed in SQL command before execution
886
     *
887
     * @param string $query Query to be executed
888
     *
889
     * @return string
890
     */
891
    public function sqlCommandQuery(string $query)
892
    {
893
        return $this->shortenUtf8(trim($query), 1000);
894
    }
895
}
896