Passed
Push — main ( 759879...de5ba5 )
by Thierry
01:57
created

TableSelectAdmin::getLimitOptions()   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 1
dl 0
loc 3
rs 10
1
<?php
2
3
namespace Lagdo\DbAdmin\DbAdmin;
4
5
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
6
use Lagdo\DbAdmin\Driver\Entity\TableSelectEntity;
7
8
use Exception;
9
10
use function intval;
11
use function count;
12
use function str_replace;
13
use function compact;
14
use function preg_match;
15
use function microtime;
16
use function html_entity_decode;
17
use function strip_tags;
18
use function array_flip;
19
use function in_array;
20
use function trim;
21
use function md5;
22
use function strlen;
23
use function strpos;
24
25
/**
26
 * Admin table select functions
27
 */
28
class TableSelectAdmin extends AbstractAdmin
29
{
30
    /**
31
     * Print columns box in select
32
     * @param array $select Result of processSelectColumns()[0]
33
     * @param array $columns Selectable columns
34
     * @param array $options
35
     * @return array
36
     */
37
    private function getColumnsOptions(array $select, array $columns, array $options): array
38
    {
39
        return [
40
            'select' => $select,
41
            'values' => (array)$options["columns"],
42
            'columns' => $columns,
43
            'functions' => $this->driver->functions(),
44
            'grouping' => $this->driver->grouping(),
45
        ];
46
    }
47
48
    /**
49
     * Print search box in select
50
     *
51
     * @param array $columns Selectable columns
52
     * @param array $indexes
53
     * @param array $options
54
     *
55
     * @return array
56
     */
57
    private function getFiltersOptions(array $columns, array $indexes, array $options): array
58
    {
59
        $fulltexts = [];
60
        foreach ($indexes as $i => $index) {
61
            $fulltexts[$i] = $index->type == "FULLTEXT" ? $this->util->html($options["fulltext"][$i]) : '';
62
        }
63
        return [
64
            // 'where' => $where,
65
            'values' => (array)$options["where"],
66
            'columns' => $columns,
67
            'indexes' => $indexes,
68
            'operators' => $this->driver->operators(),
69
            'fulltexts' => $fulltexts,
70
        ];
71
    }
72
73
    /**
74
     * Print order box in select
75
     *
76
     * @param array $columns Selectable columns
77
     * @param array $options
78
     *
79
     * @return array
80
     */
81
    private function getSortingOptions(array $columns, array $options): array
82
    {
83
        $values = [];
84
        $descs = (array)$options["desc"];
85
        foreach ((array)$options["order"] as $key => $value) {
86
            $values[] = [
87
                'col' => $value,
88
                'desc' => $descs[$key] ?? 0,
89
            ];
90
        }
91
        return [
92
            // 'order' => $order,
93
            'values' => $values,
94
            'columns' => $columns,
95
        ];
96
    }
97
98
    /**
99
     * Print limit box in select
100
     *
101
     * @param string $limit Result of processSelectLimit()
102
     *
103
     * @return array
104
     */
105
    private function getLimitOptions(string $limit): array
106
    {
107
        return ['value' => $this->util->html($limit)];
108
    }
109
110
    /**
111
     * Print text length box in select
112
     *
113
     * @param int $textLength Result of processSelectLength()
114
     *
115
     * @return array
116
     */
117
    private function getLengthOptions($textLength): array
118
    {
119
        return [
120
            'value' => $textLength === 0 ? 0 : $this->util->html($textLength),
121
        ];
122
    }
123
124
    /**
125
     * Print action box in select
126
     *
127
     * @param array $indexes
128
     *
129
     * @return array
130
     */
131
    // private function getActionOptions(array $indexes)
132
    // {
133
    //     $columns = [];
134
    //     foreach ($indexes as $index) {
135
    //         $current_key = \reset($index->columns);
136
    //         if ($index->type != "FULLTEXT" && $current_key) {
137
    //             $columns[$current_key] = 1;
138
    //         }
139
    //     }
140
    //     $columns[""] = 1;
141
    //     return ['columns' => $columns];
142
    // }
143
144
    /**
145
     * Print command box in select
146
     *
147
     * @return bool whether to print default commands
148
     */
149
    // private function getCommandOptions()
150
    // {
151
    //     return !$this->driver->isInformationSchema($this->driver->database());
152
    // }
153
154
    /**
155
     * Print import box in select
156
     *
157
     * @return bool whether to print default import
158
     */
159
    // private function getImportOptions()
160
    // {
161
    //     return !$this->driver->isInformationSchema($this->driver->database());
162
    // }
163
164
    /**
165
     * Print extra text in the end of a select form
166
     *
167
     * @param array $emailFields Fields holding e-mails
168
     * @param array $columns Selectable columns
169
     *
170
     * @return array
171
     */
172
    // private function getEmailOptions(array $emailFields, array $columns)
173
    // {
174
    // }
175
176
    /**
177
     * @param array $queryOptions
178
     *
179
     * @return int
180
     */
181
    private function setDefaultOptions(array &$queryOptions): int
182
    {
183
        $defaultOptions = [
184
            'columns' => [],
185
            'where' => [],
186
            'order' => [],
187
            'desc' => [],
188
            'fulltext' => [],
189
            'limit' => '50',
190
            'text_length' => '100',
191
            'page' => '1',
192
        ];
193
        foreach ($defaultOptions as $name => $value) {
194
            if (!isset($queryOptions[$name])) {
195
                $queryOptions[$name] = $value;
196
            }
197
        }
198
        $page = intval($queryOptions['page']);
199
        if ($page > 0) {
200
            $page -= 1; // Page numbers start at 0 here, instead of 1.
201
        }
202
        $queryOptions['page'] = $page;
203
        return $page;
204
    }
205
206
    /**
207
     * @param array $fields
208
     *
209
     * @return array
210
     */
211
    private function getFieldsOptions(array $fields): array
212
    {
213
        $rights = []; // privilege => 0
214
        $columns = []; // selectable columns
215
        $textLength = 0;
216
        foreach ($fields as $key => $field) {
217
            $name = $this->util->fieldName($field);
218
            if (isset($field->privileges["select"]) && $name != "") {
219
                $columns[$key] = html_entity_decode(strip_tags($name), ENT_QUOTES);
220
                if ($this->util->isShortable($field)) {
221
                    $textLength = $this->util->processSelectLength();
222
                }
223
            }
224
            $rights[] = $field->privileges;
225
        }
226
        return [$rights, $columns, $textLength];
227
    }
228
229
    /**
230
     * @param array $indexes
231
     * @param array $select
232
     * @param mixed $tableStatus
233
     *
234
     * @return array|null
235
     */
236
    private function setPrimaryKey(array &$indexes, array $select, $tableStatus)
237
    {
238
        $primary = $unselected = null;
239
        foreach ($indexes as $index) {
240
            if ($index->type == "PRIMARY") {
241
                $primary = array_flip($index->columns);
242
                $unselected = ($select ? $primary : []);
243
                foreach ($unselected as $key => $val) {
244
                    if (in_array($this->driver->escapeId($key), $select)) {
245
                        unset($unselected[$key]);
246
                    }
247
                }
248
                break;
249
            }
250
        }
251
252
        $oid = $tableStatus->oid;
253
        if ($oid && !$primary) {
254
            /*$primary = */$unselected = [$oid => 0];
255
            $indexes[] = ["type" => "PRIMARY", "columns" => [$oid]];
256
        }
257
258
        return $unselected;
259
    }
260
261
    /**
262
     * @param array $select
263
     * @param array $columns
264
     * @param array $indexes
265
     * @param int $limit
266
     * @param int $textLength
267
     * @param array $queryOptions
268
     *
269
     * @return array
270
     */
271
    private function getAllOptions(array $select, array $columns, array $indexes,
272
        int $limit, int $textLength, array $queryOptions): array
273
    {
274
        return [
275
            'columns' => $this->getColumnsOptions($select, $columns, $queryOptions),
276
            'filters' => $this->getFiltersOptions($columns, $indexes, $queryOptions),
277
            'sorting' => $this->getSortingOptions($columns, $queryOptions),
278
            'limit' => $this->getLimitOptions($limit),
279
            'length' => $this->getLengthOptions($textLength),
280
            // 'action' => $this->getActionOptions($indexes),
281
        ];
282
    }
283
284
    /**
285
     * @param string $table
286
     * @param array $columns
287
     * @param array $fields
288
     * @param array $select
289
     * @param array $group
290
     * @param array $where
291
     * @param array $order
292
     * @param array $unselected
293
     * @param int $limit
294
     * @param int $page
295
     *
296
     * @return TableSelectEntity
297
     */
298
    private function getSelectEntity(string $table, array $columns, array $fields, array $select,
299
        array $group, array $where, array $order, array $unselected, int $limit, int $page): TableSelectEntity
300
    {
301
        $select2 = $select;
302
        $group2 = $group;
303
        if (!$select2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $select2 of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
304
            $select2[] = "*";
305
            $convert_fields = $this->driver->convertFields($columns, $fields, $select);
306
            if ($convert_fields) {
307
                $select2[] = \substr($convert_fields, 2);
308
            }
309
        }
310
        foreach ($select as $key => $val) {
311
            $field = $fields[$this->driver->unescapeId($val)] ?? null;
312
            if ($field && ($as = $this->driver->convertField($field))) {
313
                $select2[$key] = "$as AS $val";
314
            }
315
        }
316
        $isGroup = count($group) < count($select);
317
        if (!$isGroup && $unselected) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $unselected of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
318
            foreach ($unselected as $key => $val) {
319
                $select2[] = $this->driver->escapeId($key);
320
                if ($group2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $group2 of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
321
                    $group2[] = $this->driver->escapeId($key);
322
                }
323
            }
324
        }
325
326
        // From driver.inc.php
327
        return new TableSelectEntity($table, $select2, $where, $group2, $order, $limit, $page);
328
    }
329
330
    /**
331
     * Get required data for create/update on tables
332
     *
333
     * @param string $table The table name
334
     * @param array $queryOptions The query options
335
     *
336
     * @return array
337
     * @throws Exception
338
     */
339
    private function prepareSelect(string $table, array &$queryOptions = []): array
340
    {
341
        $page = $this->setDefaultOptions($queryOptions);
342
        $this->util->input()->setValues($queryOptions);
0 ignored issues
show
Bug introduced by
The method setValues() does not exist on Lagdo\DbAdmin\Driver\InputInterface. ( Ignorable by Annotation )

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

342
        $this->util->input()->/** @scrutinizer ignore-call */ setValues($queryOptions);

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...
343
344
        // From select.inc.php
345
        $fields = $this->driver->fields($table);
346
        list(, $columns, $textLength) = $this->getFieldsOptions($fields);
347
        if (!$columns && $this->driver->support("table")) {
348
            throw new Exception($this->trans->lang('Unable to select the table') .
349
                ($fields ? "." : ": " . $this->driver->error()));
350
        }
351
352
        $indexes = $this->driver->indexes($table);
353
        $foreignKeys = $this->admin->columnForeignKeys($table);
354
        list($select, $group) = $this->util->processSelectColumns();
355
        $where = $this->util->processSelectSearch($fields, $indexes);
356
        $order = $this->util->processSelectOrder();
357
        $limit = $this->util->processSelectLimit();
358
        $tableStatus = $this->driver->tableStatusOrName($table);
359
        $unselected = $this->setPrimaryKey($indexes, $select, $tableStatus);
360
        $tableName = $this->util->tableName($tableStatus);
361
362
        // $set = null;
363
        // if(isset($rights["insert"]) || !support("table")) {
364
        //     $set = "";
365
        //     foreach((array) $queryOptions["where"] as $val) {
366
        //         if($foreignKeys[$val["col"]] && count($foreignKeys[$val["col"]]) == 1 && ($val["op"] == "="
367
        //             || (!$val["op"] && !preg_match('~[_%]~', $val["val"])) // LIKE in Editor
368
        //         )) {
369
        //             $set .= "&set" . urlencode("[" . $this->util->bracketEscape($val["col"]) . "]") . "=" . urlencode($val["val"]);
370
        //         }
371
        //     }
372
        // }
373
        // $this->util->selectLinks($tableStatus, $set);
374
375
        // if($page == "last")
376
        // {
377
        //     $isGroup = count($group) < count($select);
378
        //     $found_rows = $this->driver->result($this->driver->sqlForRowCount($table, $where, $isGroup, $group));
379
        //     $page = \floor(\max(0, $found_rows - 1) / $limit);
380
        // }
381
382
        $options = $this->getAllOptions($select, $columns, $indexes, $limit, $textLength, $queryOptions);
383
        $entity = $this->getSelectEntity($table, $columns, $fields, $select, $group, $where, $order, $unselected, $limit, $page);
0 ignored issues
show
Bug introduced by
It seems like $unselected can also be of type null; however, parameter $unselected of Lagdo\DbAdmin\DbAdmin\Ta...dmin::getSelectEntity() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

383
        $entity = $this->getSelectEntity($table, $columns, $fields, $select, $group, $where, $order, /** @scrutinizer ignore-type */ $unselected, $limit, $page);
Loading history...
384
        $query = $this->driver->buildSelectQuery($entity);
385
        // From adminer.inc.php
386
        $query = str_replace("\n", " ", $query);
387
388
        return [$options, $query, $select, $fields, $foreignKeys, $columns, $indexes,
389
            $where, $group, $order, $limit, $page, $textLength, $tableName, $unselected];
390
    }
391
392
    /**
393
     * Get required data for create/update on tables
394
     *
395
     * @param string $table The table name
396
     * @param array $queryOptions The query options
397
     *
398
     * @return array
399
     * @throws Exception
400
     */
401
    public function getSelectData(string $table, array $queryOptions = []): array
402
    {
403
        list($options, $query) = $this->prepareSelect($table, $queryOptions);
404
405
        $query = $this->util->html($query);
406
        $mainActions = [
407
            'select-exec' => $this->trans->lang('Execute'),
408
            'insert-table' => $this->trans->lang('New item'),
409
            'select-back' => $this->trans->lang('Back'),
410
        ];
411
412
        return compact('mainActions', 'options', 'query');
413
    }
414
415
    /**
416
     * @param string $query
417
     * @param int $page
418
     *
419
     * @return array
420
     */
421
    private function executeQuery(string $query, int $page): array
422
    {
423
        // From driver.inc.php
424
        $start = microtime(true);
425
        $statement = $this->driver->execute($query);
426
        // From adminer.inc.php
427
        $duration = $this->trans->formatTime($start); // Compute and format the duration
428
429
        if (!$statement) {
430
            return ['error' => $this->driver->error()];
431
        }
432
        // From select.inc.php
433
        $rows = [];
434
        while (($row = $statement->fetchAssoc())) {
435
            if ($page && $this->driver->jush() == "oracle") {
436
                unset($row["RNUM"]);
437
            }
438
            $rows[] = $row;
439
        }
440
441
        return [$rows, $duration];
442
    }
443
444
    /**
445
     * @param array $rows
446
     * @param array $select
447
     * @param array $fields
448
     * @param array $unselected
449
     *
450
     * @return array
451
     */
452
    private function getResultHeaders(array $rows, array $select, array $fields, array $unselected): array
453
    {
454
        // Results headers
455
        $headers = [
456
            '', // !$group && $select ? '' : lang('Modify');
457
        ];
458
        $names = [];
459
        // $functions = [];
460
        reset($select);
461
        $rank = 1;
462
        foreach ($rows[0] as $key => $value) {
463
            $header = [];
464
            if (!isset($unselected[$key])) {
465
                $value = $queryOptions["columns"][key($select)] ?? [];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $queryOptions seems to never exist and therefore isset should always be false.
Loading history...
466
                $fun = $value["fun"] ?? '';
467
                $field = $fields[$select ? ($value ? $value["col"] : current($select)) : $key];
468
                $name = ($field ? $this->util->fieldName($field, $rank) : ($fun ? "*" : $key));
469
                $header = compact('value', 'field', 'name');
470
                if ($name != "") {
471
                    $rank++;
472
                    $names[$key] = $name;
473
                    $column = $this->driver->escapeId($key);
474
                    // $href = remove_from_uri('(order|desc)[^=]*|page') . '&order%5B0%5D=' . urlencode($key);
475
                    // $desc = "&desc%5B0%5D=1";
476
                    $header['column'] = $column;
477
                    $header['key'] = $this->util->html($this->util->bracketEscape($key));
478
                    $header['sql'] = $this->util->applySqlFunction($fun, $name); //! columns looking like functions
479
                }
480
                // $functions[$key] = $fun;
481
                next($select);
482
            }
483
            $headers[] = $header;
484
        }
485
        return [$headers, $names];
486
    }
487
488
    /**
489
     * @param array $rows
490
     * @param array $queryOptions
491
     *
492
     * @return array
493
     */
494
    /*private function getValuesLengths(array $rows, array $queryOptions): array
495
    {
496
         $lengths = [];
497
         if($queryOptions["modify"])
498
         {
499
             foreach($rows as $row)
500
             {
501
                 foreach($row as $key => $value)
502
                 {
503
                     $lengths[$key] = \max($lengths[$key], \min(40, strlen(\utf8_decode($value))));
504
                 }
505
             }
506
         }
507
         return $lengths;
508
    }*/
509
510
    /**
511
     * @param array $row
512
     * @param array $indexes
513
     *
514
     * @return array
515
     */
516
    private function getUniqueIds(array $row, array $indexes): array
517
    {
518
        $uniqueIds = $this->util->uniqueIds($row, $indexes);
519
        if (empty($uniqueIds)) {
520
            $pattern = '~^(COUNT\((\*|(DISTINCT )?`(?:[^`]|``)+`)\)|(AVG|GROUP_CONCAT|MAX|MIN|SUM)\(`(?:[^`]|``)+`\))$~';
521
            foreach ($row as $key => $value) {
522
                if (!preg_match($pattern, $key)) {
523
                    //! columns looking like functions
524
                    $uniqueIds[$key] = $value;
525
                }
526
            }
527
        }
528
        return $uniqueIds;
529
    }
530
531
    /**
532
     * @param array $row
533
     * @param array $indexes
534
     *
535
     * @return array
536
     */
537
    private function getRowIds(array $row, array $indexes): array
538
    {
539
        $uniqueIds = $this->getUniqueIds($row, $indexes);
540
        // Unique identifier to edit returned data.
541
        // $unique_idf = "";
542
        $rowIds = ['where' => [], 'null' => []];
543
        foreach ($uniqueIds as $key => $value) {
544
            $key = trim($key);
545
            $type = '';
546
            $collation = '';
547
            if (isset($fields[$key])) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fields seems to never exist and therefore isset should always be false.
Loading history...
548
                $type = $fields[$key]->type;
549
                $collation = $fields[$key]->collation;
550
            }
551
            if (($this->driver->jush() == "sql" || $this->driver->jush() == "pgsql") &&
552
                preg_match('~char|text|enum|set~', $type) && strlen($value) > 64) {
553
                $key = (strpos($key, '(') ? $key : $this->driver->escapeId($key)); //! columns looking like functions
554
                $key = "MD5(" . ($this->driver->jush() != 'sql' || preg_match("~^utf8~", $collation) ?
555
                        $key : "CONVERT($key USING " . $this->driver->charset() . ")") . ")";
556
                $value = md5($value);
557
            }
558
            if ($value !== null) {
559
                $rowIds['where'][$this->util->bracketEscape($key)] = $value;
560
            } else {
561
                $rowIds['null'][] = $this->util->bracketEscape($key);
562
            }
563
            // $unique_idf .= "&" . ($value !== null ? \urlencode("where[" . $this->util->bracketEscape($key) . "]") .
564
            //     "=" . \urlencode($value) : \urlencode("null[]") . "=" . \urlencode($key));
565
        }
566
        return $rowIds;
567
    }
568
569
    /**
570
     * @param array $row
571
     * @param array $names
572
     * @param int $textLength
573
     *
574
     * @return array
575
     */
576
    private function getRowColumns(array $row, array $names, int $textLength): array
577
    {
578
        $cols = [];
579
        foreach ($row as $key => $value) {
580
            if (isset($names[$key])) {
581
                $field = $fields[$key] ?? new TableFieldEntity();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fields does not exist. Did you maybe mean $email_fields?
Loading history...
582
                $value = $this->driver->value($value, $field);
583
                if ($value != "" && (!isset($email_fields[$key]) || $email_fields[$key] != "")) {
584
                    //! filled e-mails can be contained on other pages
585
                    $email_fields[$key] = ($this->util->isMail($value) ? $names[$key] : "");
586
                }
587
588
                $link = "";
589
590
                $value = $this->util->selectValue($value, $link, $field, $textLength);
591
                $text = preg_match('~text|lob~', $field->type);
592
593
                $cols[] = compact(/*'id', */'text', 'value'/*, 'editable'*/);
594
            }
595
        }
596
        return $cols;
597
    }
598
599
    /**
600
     * Get required data for create/update on tables
601
     *
602
     * @param string $table The table name
603
     * @param array $queryOptions The query options
604
     *
605
     * @return array
606
     * @throws Exception
607
     */
608
    public function execSelect(string $table, array $queryOptions): array
609
    {
610
        list(, $query, $select, $fields, , , $indexes, $where, $group, , $limit, $page,
611
            $textLength, , $unselected) = $this->prepareSelect($table, $queryOptions);
612
613
        list($rows, $duration) = $this->executeQuery($query, $page);
614
        if (!$rows) {
615
            return ['error' => $this->trans->lang('No rows.')];
616
        }
617
        // $backward_keys = $this->driver->backwardKeys($table, $tableName);
618
        // lengths = $this->getValuesLengths($rows, $queryOptions);
619
620
        list($headers, $names) = $this->getResultHeaders($rows, $select, $fields, $unselected);
621
622
        $results = [];
623
        foreach ($rows as $row) {
624
            // Unique identifier to edit returned data.
625
            $rowIds = $this->getRowIds($row, $indexes);
626
            $cols = $this->getRowColumns($row, $names, $textLength);
627
            $results[] = ['ids' => $rowIds, 'cols' => $cols];
628
        }
629
630
        $isGroup = count($group) < count($select);
631
        $total = $this->driver->result($this->driver->sqlForRowCount($table, $where, $isGroup, $group));
632
633
        $rows = $results;
634
        $error = null;
635
        return compact('duration', 'headers', 'query', 'rows', 'limit', 'total', 'error');
636
    }
637
}
638