Passed
Push — main ( 3e7f40...759879 )
by Thierry
02:03
created

TableSelectAdmin::getSelectData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 7
nc 1
nop 2
dl 0
loc 12
rs 10
c 1
b 0
f 0
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
17
/**
18
 * Admin table select functions
19
 */
20
class TableSelectAdmin extends AbstractAdmin
21
{
22
    /**
23
     * Print columns box in select
24
     * @param array $select Result of processSelectColumns()[0]
25
     * @param array $columns Selectable columns
26
     * @param array $options
27
     * @return array
28
     */
29
    private function getColumnsOptions(array $select, array $columns, array $options): array
30
    {
31
        return [
32
            'select' => $select,
33
            'values' => (array)$options["columns"],
34
            'columns' => $columns,
35
            'functions' => $this->driver->functions(),
36
            'grouping' => $this->driver->grouping(),
37
        ];
38
    }
39
40
    /**
41
     * Print search box in select
42
     *
43
     * @param array $columns Selectable columns
44
     * @param array $indexes
45
     * @param array $options
46
     *
47
     * @return array
48
     */
49
    private function getFiltersOptions(array $columns, array $indexes, array $options): array
50
    {
51
        $fulltexts = [];
52
        foreach ($indexes as $i => $index) {
53
            $fulltexts[$i] = $index->type == "FULLTEXT" ? $this->util->html($options["fulltext"][$i]) : '';
54
        }
55
        return [
56
            // 'where' => $where,
57
            'values' => (array)$options["where"],
58
            'columns' => $columns,
59
            'indexes' => $indexes,
60
            'operators' => $this->driver->operators(),
61
            'fulltexts' => $fulltexts,
62
        ];
63
    }
64
65
    /**
66
     * Print order box in select
67
     *
68
     * @param array $columns Selectable columns
69
     * @param array $options
70
     *
71
     * @return array
72
     */
73
    private function getSortingOptions(array $columns, array $options): array
74
    {
75
        $values = [];
76
        $descs = (array)$options["desc"];
77
        foreach ((array)$options["order"] as $key => $value) {
78
            $values[] = [
79
                'col' => $value,
80
                'desc' => $descs[$key] ?? 0,
81
            ];
82
        }
83
        return [
84
            // 'order' => $order,
85
            'values' => $values,
86
            'columns' => $columns,
87
        ];
88
    }
89
90
    /**
91
     * Print limit box in select
92
     *
93
     * @param string $limit Result of processSelectLimit()
94
     *
95
     * @return array
96
     */
97
    private function getLimitOptions(string $limit): array
98
    {
99
        return ['value' => $this->util->html($limit)];
100
    }
101
102
    /**
103
     * Print text length box in select
104
     *
105
     * @param int $textLength Result of processSelectLength()
106
     *
107
     * @return array
108
     */
109
    private function getLengthOptions($textLength): array
110
    {
111
        return [
112
            'value' => $textLength === 0 ? 0 : $this->util->html($textLength),
113
        ];
114
    }
115
116
    /**
117
     * Print action box in select
118
     *
119
     * @param array $indexes
120
     *
121
     * @return array
122
     */
123
    // private function getActionOptions(array $indexes)
124
    // {
125
    //     $columns = [];
126
    //     foreach ($indexes as $index) {
127
    //         $current_key = \reset($index->columns);
128
    //         if ($index->type != "FULLTEXT" && $current_key) {
129
    //             $columns[$current_key] = 1;
130
    //         }
131
    //     }
132
    //     $columns[""] = 1;
133
    //     return ['columns' => $columns];
134
    // }
135
136
    /**
137
     * Print command box in select
138
     *
139
     * @return bool whether to print default commands
140
     */
141
    // private function getCommandOptions()
142
    // {
143
    //     return !$this->driver->isInformationSchema($this->driver->database());
144
    // }
145
146
    /**
147
     * Print import box in select
148
     *
149
     * @return bool whether to print default import
150
     */
151
    // private function getImportOptions()
152
    // {
153
    //     return !$this->driver->isInformationSchema($this->driver->database());
154
    // }
155
156
    /**
157
     * Print extra text in the end of a select form
158
     *
159
     * @param array $emailFields Fields holding e-mails
160
     * @param array $columns Selectable columns
161
     *
162
     * @return array
163
     */
164
    // private function getEmailOptions(array $emailFields, array $columns)
165
    // {
166
    // }
167
168
    /**
169
     * @param array $queryOptions
170
     *
171
     * @return int
172
     */
173
    private function setDefaultOptions(array &$queryOptions): int
174
    {
175
        $defaultOptions = [
176
            'columns' => [],
177
            'where' => [],
178
            'order' => [],
179
            'desc' => [],
180
            'fulltext' => [],
181
            'limit' => '50',
182
            'text_length' => '100',
183
            'page' => '1',
184
        ];
185
        foreach ($defaultOptions as $name => $value) {
186
            if (!isset($queryOptions[$name])) {
187
                $queryOptions[$name] = $value;
188
            }
189
        }
190
        $page = intval($queryOptions['page']);
191
        if ($page > 0) {
192
            $page -= 1; // Page numbers start at 0 here, instead of 1.
193
        }
194
        $queryOptions['page'] = $page;
195
        return $page;
196
    }
197
198
    /**
199
     * @param array $fields
200
     *
201
     * @return array
202
     */
203
    private function getFieldsOptions(array $fields): array
204
    {
205
        $rights = []; // privilege => 0
206
        $columns = []; // selectable columns
207
        $textLength = 0;
208
        foreach ($fields as $key => $field) {
209
            $name = $this->util->fieldName($field);
210
            if (isset($field->privileges["select"]) && $name != "") {
211
                $columns[$key] = \html_entity_decode(\strip_tags($name), ENT_QUOTES);
212
                if ($this->util->isShortable($field)) {
213
                    $textLength = $this->util->processSelectLength();
214
                }
215
            }
216
            $rights[] = $field->privileges;
217
        }
218
        return [$rights, $columns, $textLength];
219
    }
220
221
    /**
222
     * @param array $indexes
223
     * @param array $select
224
     * @param mixed $tableStatus
225
     *
226
     * @return array|null
227
     */
228
    private function setPrimaryKey(array &$indexes, array $select, $tableStatus)
229
    {
230
        $primary = $unselected = null;
231
        foreach ($indexes as $index) {
232
            if ($index->type == "PRIMARY") {
233
                $primary = \array_flip($index->columns);
234
                $unselected = ($select ? $primary : []);
235
                foreach ($unselected as $key => $val) {
236
                    if (\in_array($this->driver->escapeId($key), $select)) {
237
                        unset($unselected[$key]);
238
                    }
239
                }
240
                break;
241
            }
242
        }
243
244
        $oid = $tableStatus->oid;
245
        if ($oid && !$primary) {
246
            /*$primary = */$unselected = [$oid => 0];
247
            $indexes[] = ["type" => "PRIMARY", "columns" => [$oid]];
248
        }
249
250
        return $unselected;
251
    }
252
253
    /**
254
     * @param array $select
255
     * @param array $columns
256
     * @param array $indexes
257
     * @param int $limit
258
     * @param int $textLength
259
     * @param array $queryOptions
260
     *
261
     * @return array
262
     */
263
    private function getAllOptions(array $select, array $columns, array $indexes,
264
        int $limit, int $textLength, array $queryOptions): array
265
    {
266
        return [
267
            'columns' => $this->getColumnsOptions($select, $columns, $queryOptions),
268
            'filters' => $this->getFiltersOptions($columns, $indexes, $queryOptions),
269
            'sorting' => $this->getSortingOptions($columns, $queryOptions),
270
            'limit' => $this->getLimitOptions($limit),
271
            'length' => $this->getLengthOptions($textLength),
272
            // 'action' => $this->getActionOptions($indexes),
273
        ];
274
    }
275
276
    /**
277
     * @param string $table
278
     * @param array $columns
279
     * @param array $fields
280
     * @param array $select
281
     * @param array $group
282
     * @param array $where
283
     * @param array $order
284
     * @param array $unselected
285
     * @param int $limit
286
     * @param int $page
287
     *
288
     * @return TableSelectEntity
289
     */
290
    private function getSelectEntity(string $table, array $columns, array $fields, array $select,
291
        array $group, array $where, array $order, array $unselected, int $limit, int $page): TableSelectEntity
292
    {
293
        $select2 = $select;
294
        $group2 = $group;
295
        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...
296
            $select2[] = "*";
297
            $convert_fields = $this->driver->convertFields($columns, $fields, $select);
298
            if ($convert_fields) {
299
                $select2[] = \substr($convert_fields, 2);
300
            }
301
        }
302
        foreach ($select as $key => $val) {
303
            $field = $fields[$this->driver->unescapeId($val)] ?? null;
304
            if ($field && ($as = $this->driver->convertField($field))) {
305
                $select2[$key] = "$as AS $val";
306
            }
307
        }
308
        $isGroup = count($group) < count($select);
309
        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...
310
            foreach ($unselected as $key => $val) {
311
                $select2[] = $this->driver->escapeId($key);
312
                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...
313
                    $group2[] = $this->driver->escapeId($key);
314
                }
315
            }
316
        }
317
318
        // From driver.inc.php
319
        return new TableSelectEntity($table, $select2, $where, $group2, $order, $limit, $page);
320
    }
321
322
    /**
323
     * Get required data for create/update on tables
324
     *
325
     * @param string $table The table name
326
     * @param array $queryOptions The query options
327
     *
328
     * @return array
329
     * @throws Exception
330
     */
331
    private function prepareSelect(string $table, array &$queryOptions = []): array
332
    {
333
        $page = $this->setDefaultOptions($queryOptions);
334
        $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

334
        $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...
335
336
        // From select.inc.php
337
        $fields = $this->driver->fields($table);
338
        list(, $columns, $textLength) = $this->getFieldsOptions($fields);
339
        if (!$columns && $this->driver->support("table")) {
340
            throw new Exception($this->trans->lang('Unable to select the table') .
341
                ($fields ? "." : ": " . $this->driver->error()));
342
        }
343
344
        $indexes = $this->driver->indexes($table);
345
        $foreignKeys = $this->admin->columnForeignKeys($table);
346
        list($select, $group) = $this->util->processSelectColumns();
347
        $where = $this->util->processSelectSearch($fields, $indexes);
348
        $order = $this->util->processSelectOrder();
349
        $limit = $this->util->processSelectLimit();
350
        $tableStatus = $this->driver->tableStatusOrName($table);
351
        $unselected = $this->setPrimaryKey($indexes, $select, $tableStatus);
352
        $tableName = $this->util->tableName($tableStatus);
353
354
        // $set = null;
355
        // if(isset($rights["insert"]) || !support("table")) {
356
        //     $set = "";
357
        //     foreach((array) $queryOptions["where"] as $val) {
358
        //         if($foreignKeys[$val["col"]] && count($foreignKeys[$val["col"]]) == 1 && ($val["op"] == "="
359
        //             || (!$val["op"] && !preg_match('~[_%]~', $val["val"])) // LIKE in Editor
360
        //         )) {
361
        //             $set .= "&set" . urlencode("[" . $this->util->bracketEscape($val["col"]) . "]") . "=" . urlencode($val["val"]);
362
        //         }
363
        //     }
364
        // }
365
        // $this->util->selectLinks($tableStatus, $set);
366
367
        // if($page == "last")
368
        // {
369
        //     $isGroup = count($group) < count($select);
370
        //     $found_rows = $this->driver->result($this->driver->sqlForRowCount($table, $where, $isGroup, $group));
371
        //     $page = \floor(\max(0, $found_rows - 1) / $limit);
372
        // }
373
374
        $options = $this->getAllOptions($select, $columns, $indexes, $limit, $textLength, $queryOptions);
375
        $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

375
        $entity = $this->getSelectEntity($table, $columns, $fields, $select, $group, $where, $order, /** @scrutinizer ignore-type */ $unselected, $limit, $page);
Loading history...
376
        $query = $this->driver->buildSelectQuery($entity);
377
        // From adminer.inc.php
378
        $query = str_replace("\n", " ", $query);
379
380
        return [$options, $query, $select, $fields, $foreignKeys, $columns, $indexes,
381
            $where, $group, $order, $limit, $page, $textLength, $tableName, $unselected];
382
    }
383
384
    /**
385
     * Get required data for create/update on tables
386
     *
387
     * @param string $table The table name
388
     * @param array $queryOptions The query options
389
     *
390
     * @return array
391
     * @throws Exception
392
     */
393
    public function getSelectData(string $table, array $queryOptions = []): array
394
    {
395
        list($options, $query) = $this->prepareSelect($table, $queryOptions);
396
397
        $query = $this->util->html($query);
398
        $mainActions = [
399
            'select-exec' => $this->trans->lang('Execute'),
400
            'insert-table' => $this->trans->lang('New item'),
401
            'select-back' => $this->trans->lang('Back'),
402
        ];
403
404
        return compact('mainActions', 'options', 'query');
405
    }
406
407
    /**
408
     * Get required data for create/update on tables
409
     *
410
     * @param string $table The table name
411
     * @param array $queryOptions The query options
412
     *
413
     * @return array
414
     * @throws Exception
415
     */
416
    public function execSelect(string $table, array $queryOptions): array
417
    {
418
        list(, $query, $select, $fields, , , $indexes, $where, $group, , $limit, $page,
419
            $textLength, , $unselected) = $this->prepareSelect($table, $queryOptions);
420
421
        $error = null;
422
        // From driver.inc.php
423
        $start = microtime(true);
424
        $statement = $this->driver->execute($query);
425
        // From adminer.inc.php
426
        $duration = $this->trans->formatTime($start); // Compute and format the duration
427
428
        if (!$statement) {
429
            return ['error' => $this->driver->error()];
430
        }
431
        // From select.inc.php
432
        $rows = [];
433
        while (($row = $statement->fetchAssoc())) {
434
            if ($page && $this->driver->jush() == "oracle") {
435
                unset($row["RNUM"]);
436
            }
437
            $rows[] = $row;
438
        }
439
        if (!$rows) {
440
            return ['error' => $this->trans->lang('No rows.')];
441
        }
442
        // $backward_keys = $this->driver->backwardKeys($table, $tableName);
443
444
        // Results headers
445
        $headers = [
446
            '', // !$group && $select ? '' : lang('Modify');
447
        ];
448
        $names = [];
449
        // $functions = [];
450
        reset($select);
451
        $rank = 1;
452
        foreach ($rows[0] as $key => $value) {
453
            $header = [];
454
            if (!isset($unselected[$key])) {
455
                $value = $queryOptions["columns"][key($select)] ?? [];
456
                $fun = $value["fun"] ?? '';
457
                $field = $fields[$select ? ($value ? $value["col"] : current($select)) : $key];
458
                $name = ($field ? $this->util->fieldName($field, $rank) : ($fun ? "*" : $key));
459
                $header = compact('value', 'field', 'name');
460
                if ($name != "") {
461
                    $rank++;
462
                    $names[$key] = $name;
463
                    $column = $this->driver->escapeId($key);
464
                    // $href = remove_from_uri('(order|desc)[^=]*|page') . '&order%5B0%5D=' . urlencode($key);
465
                    // $desc = "&desc%5B0%5D=1";
466
                    $header['column'] = $column;
467
                    $header['key'] = $this->util->html($this->util->bracketEscape($key));
468
                    $header['sql'] = $this->util->applySqlFunction($fun, $name); //! columns looking like functions
469
                }
470
                // $functions[$key] = $fun;
471
                next($select);
472
            }
473
            $headers[] = $header;
474
        }
475
476
        // $lengths = [];
477
        // if($queryOptions["modify"])
478
        // {
479
        //     foreach($rows as $row)
480
        //     {
481
        //         foreach($row as $key => $value)
482
        //         {
483
        //             $lengths[$key] = \max($lengths[$key], \min(40, strlen(\utf8_decode($value))));
484
        //         }
485
        //     }
486
        // }
487
488
        $results = [];
489
        foreach ($rows as $n => $row) {
490
            $uniqueIds = $this->util->uniqueIds($rows[$n], $indexes);
491
            if (empty($uniqueIds)) {
492
                foreach ($row as $key => $value) {
493
                    if (!\preg_match('~^(COUNT\((\*|(DISTINCT )?`(?:[^`]|``)+`)\)|(AVG|GROUP_CONCAT|MAX|MIN|SUM)\(`(?:[^`]|``)+`\))$~', $key)) {
494
                        //! columns looking like functions
495
                        $uniqueIds[$key] = $value;
496
                    }
497
                }
498
            }
499
500
            // Unique identifier to edit returned data.
501
            // $unique_idf = "";
502
            $rowIds = [
503
                'where' => [],
504
                'null' => [],
505
            ];
506
            foreach ($uniqueIds as $key => $value) {
507
                $key = \trim($key);
508
                $type = '';
509
                $collation = '';
510
                if (isset($fields[$key])) {
511
                    $type = $fields[$key]->type;
512
                    $collation = $fields[$key]->collation;
513
                }
514
                if (($this->driver->jush() == "sql" || $this->driver->jush() == "pgsql") &&
515
                    \preg_match('~char|text|enum|set~', $type) && strlen($value) > 64) {
516
                    $key = (\strpos($key, '(') ? $key : $this->driver->escapeId($key)); //! columns looking like functions
517
                    $key = "MD5(" . ($this->driver->jush() != 'sql' || \preg_match("~^utf8~", $collation) ?
518
                        $key : "CONVERT($key USING " . $this->driver->charset() . ")") . ")";
519
                    $value = \md5($value);
520
                }
521
                if ($value !== null) {
522
                    $rowIds['where'][$this->util->bracketEscape($key)] = $value;
523
                } else {
524
                    $rowIds['null'][] = $this->util->bracketEscape($key);
525
                }
526
                // $unique_idf .= "&" . ($value !== null ? \urlencode("where[" . $this->util->bracketEscape($key) . "]") .
527
                //     "=" . \urlencode($value) : \urlencode("null[]") . "=" . \urlencode($key));
528
            }
529
530
            $cols = [];
531
            foreach ($row as $key => $value) {
532
                if (isset($names[$key])) {
533
                    $field = $fields[$key] ?? new TableFieldEntity();
534
                    $value = $this->driver->value($value, $field);
535
                    if ($value != "" && (!isset($email_fields[$key]) || $email_fields[$key] != "")) {
536
                        //! filled e-mails can be contained on other pages
537
                        $email_fields[$key] = ($this->util->isMail($value) ? $names[$key] : "");
538
                    }
539
540
                    $link = "";
541
542
                    $value = $this->util->selectValue($value, $link, $field, $textLength);
543
                    $text = preg_match('~text|lob~', $field->type);
544
545
                    $cols[] = compact(/*'id', */'text', 'value'/*, 'editable'*/);
546
                }
547
            }
548
            $results[] = ['ids' => $rowIds, 'cols' => $cols];
549
        }
550
551
        $isGroup = count($group) < count($select);
552
        $total = $this->driver->result($this->driver->sqlForRowCount($table, $where, $isGroup, $group));
553
554
        $rows = $results;
555
        return compact('duration', 'headers', 'query', 'rows', 'limit', 'total', 'error');
556
    }
557
}
558