Passed
Push — main ( d64c56...742ea5 )
by Thierry
03:47 queued 01:37
created

TableAdmin::status()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 6
rs 10
1
<?php
2
3
namespace Lagdo\DbAdmin\DbAdmin;
4
5
use Lagdo\DbAdmin\Driver\Entity\TableFieldEntity;
6
use Lagdo\DbAdmin\Driver\Entity\ForeignKeyEntity;
7
8
use Exception;
9
10
/**
11
 * Admin table functions
12
 */
13
class TableAdmin extends AbstractAdmin
14
{
15
    /**
16
     * The current table status
17
     *
18
     * @var mixed
19
     */
20
    protected $tableStatus = null;
21
22
    /**
23
     * @var array
24
     */
25
    protected $referencableTables = [];
26
27
    /**
28
     * @var array
29
     */
30
    protected $foreignKeys = [];
31
32
    /**
33
     * Get the current table status
34
     *
35
     * @param string $table
36
     *
37
     * @return mixed
38
     */
39
    protected function status(string $table)
40
    {
41
        if (!$this->tableStatus) {
42
            $this->tableStatus = $this->driver->tableStatusOrName($table, true);
43
        }
44
        return $this->tableStatus;
45
    }
46
47
    /**
48
     * Print links after select heading
49
     * Copied from selectLinks() in adminer.inc.php
50
     *
51
     * @param string $set New item options, NULL for no new item
52
     *
53
     * @return array
54
     */
55
    protected function getTableLinks($set = null)
56
    {
57
        $links = [
58
            'select' => $this->trans->lang('Select data'),
59
        ];
60
        if ($this->driver->support('table') || $this->driver->support('indexes')) {
61
            $links['table'] = $this->trans->lang('Show structure');
62
        }
63
        if ($this->driver->support('table')) {
64
            $links['alter'] = $this->trans->lang('Alter table');
65
        }
66
        if ($set !== null) {
67
            $links['edit'] = $this->trans->lang('New item');
68
        }
69
        // $links['docs'] = \doc_link([$this->driver->jush() => $this->driver->tableHelp($name)], '?');
70
71
        return $links;
72
    }
73
74
    /**
75
     * Get details about a table
76
     *
77
     * @param string $table     The table name
78
     *
79
     * @return array
80
     */
81
    public function getTableInfo(string $table)
82
    {
83
        $mainActions = [
84
            'edit-table' => $this->trans->lang('Alter table'),
85
            'drop-table' => $this->trans->lang('Drop table'),
86
            'select-table' => $this->trans->lang('Select'),
87
            'insert-table' => $this->trans->lang('New item'),
88
        ];
89
90
        // From table.inc.php
91
        $status = $this->status($table);
92
        $name = $this->util->tableName($status);
93
        $title = $this->trans->lang('Table') . ': ' . ($name != '' ? $name : $this->util->html($table));
94
95
        $comment = $status->comment;
96
97
        $tabs = [
98
            'fields' => $this->trans->lang('Columns'),
99
            // 'indexes' => $this->trans->lang('Indexes'),
100
            // 'foreign-keys' => $this->trans->lang('Foreign keys'),
101
            // 'triggers' => $this->trans->lang('Triggers'),
102
        ];
103
        if ($this->driver->isView($status)) {
104
            if ($this->driver->support('view_trigger')) {
105
                $tabs['triggers'] = $this->trans->lang('Triggers');
106
            }
107
        } else {
108
            if ($this->driver->support('indexes')) {
109
                $tabs['indexes'] = $this->trans->lang('Indexes');
110
            }
111
            if ($this->driver->supportForeignKeys($status)) {
112
                $tabs['foreign-keys'] = $this->trans->lang('Foreign keys');
113
            }
114
            if ($this->driver->support('trigger')) {
115
                $tabs['triggers'] = $this->trans->lang('Triggers');
116
            }
117
        }
118
119
        return \compact('mainActions', 'title', 'comment', 'tabs');
120
    }
121
122
    /**
123
     * Get the fields of a table
124
     *
125
     * @param string $table     The table name
126
     *
127
     * @return array
128
     */
129
    public function getTableFields(string $table)
130
    {
131
        // From table.inc.php
132
        $fields = $this->driver->fields($table);
133
        if (empty($fields)) {
134
            throw new Exception($this->driver->error());
135
        }
136
137
        $mainActions = $this->getTableLinks();
138
139
        $tabs = [
140
            'fields' => $this->trans->lang('Columns'),
141
            // 'indexes' => $this->trans->lang('Indexes'),
142
            // 'foreign-keys' => $this->trans->lang('Foreign keys'),
143
            // 'triggers' => $this->trans->lang('Triggers'),
144
        ];
145
        if ($this->driver->support('indexes')) {
146
            $tabs['indexes'] = $this->trans->lang('Indexes');
147
        }
148
        if ($this->driver->supportForeignKeys($this->status($table))) {
149
            $tabs['foreign-keys'] = $this->trans->lang('Foreign keys');
150
        }
151
        if ($this->driver->support('trigger')) {
152
            $tabs['triggers'] = $this->trans->lang('Triggers');
153
        }
154
155
        $headers = [
156
            $this->trans->lang('Name'),
157
            $this->trans->lang('Type'),
158
            $this->trans->lang('Collation'),
159
        ];
160
        $hasComment = $this->driver->support('comment');
161
        if ($hasComment) {
162
            $headers[] = $this->trans->lang('Comment');
163
        }
164
165
        $details = [];
166
        foreach ($fields as $field) {
167
            $type = $this->util->html($field->fullType);
168
            if ($field->null) {
169
                $type .= ' <i>nullable</i>'; // ' <i>NULL</i>';
170
            }
171
            if ($field->autoIncrement) {
172
                $type .= ' <i>' . $this->trans->lang('Auto Increment') . '</i>';
173
            }
174
            if ($field->default !== '') {
175
                $type .= /*' ' . $this->trans->lang('Default value') .*/ ' [<b>' . $this->util->html($field->default) . '</b>]';
176
            }
177
            $detail = [
178
                'name' => $this->util->html($field->name),
179
                'type' => $type,
180
                'collation' => $this->util->html($field->collation),
181
            ];
182
            if ($hasComment) {
183
                $detail['comment'] = $this->util->html($field->comment);
184
            }
185
186
            $details[] = $detail;
187
        }
188
189
        return \compact('mainActions', 'headers', 'details');
190
    }
191
192
    /**
193
     * Get the indexes of a table
194
     *
195
     * @param string $table     The table name
196
     *
197
     * @return array|null
198
     */
199
    public function getTableIndexes(string $table)
200
    {
201
        if (!$this->driver->support('indexes')) {
202
            return null;
203
        }
204
205
        // From table.inc.php
206
        $indexes = $this->driver->indexes($table);
207
        $mainActions = [
208
            'create' => $this->trans->lang('Alter indexes'),
209
        ];
210
211
        $headers = [
212
            $this->trans->lang('Name'),
213
            $this->trans->lang('Type'),
214
            $this->trans->lang('Column'),
215
        ];
216
217
        $details = [];
218
        // From adminer.inc.php
219
        foreach ($indexes as $name => $index) {
220
            \ksort($index->columns); // enforce correct columns order
221
            $print = [];
222
            foreach ($index->columns as $key => $val) {
223
                $value = '<i>' . $this->util->html($val) . '</i>';
224
                if (\array_key_exists($key, $index->lengths)) {
225
                    $value .= '(' . $index->lengths[$key] . ')';
226
                }
227
                if (\array_key_exists($key, $index->descs)) {
228
                    $value .= ' DESC';
229
                }
230
                $print[] = $value;
231
            }
232
            $details[] = [
233
                'name' => $this->util->html($name),
234
                'type' => $index->type,
235
                'desc' => \implode(', ', $print),
236
            ];
237
        }
238
239
        return \compact('mainActions', 'headers', 'details');
240
    }
241
242
    /**
243
     * Get the foreign keys of a table
244
     *
245
     * @param string $table     The table name
246
     *
247
     * @return array|null
248
     */
249
    public function getTableForeignKeys(string $table)
250
    {
251
        $status = $this->status($table);
252
        if (!$this->driver->supportForeignKeys($status)) {
253
            return null;
254
        }
255
256
        // From table.inc.php
257
        $mainActions = [
258
            $this->trans->lang('Add foreign key'),
259
        ];
260
261
        $headers = [
262
            $this->trans->lang('Name'),
263
            $this->trans->lang('Source'),
264
            $this->trans->lang('Target'),
265
            $this->trans->lang('ON DELETE'),
266
            $this->trans->lang('ON UPDATE'),
267
        ];
268
269
        $foreignKeys = $this->driver->foreignKeys($table);
270
        $details = [];
271
        // From table.inc.php
272
        foreach ($foreignKeys as $name => $foreignKey) {
273
            $target = '';
274
            if ($foreignKey->database != '') {
275
                $target .= '<b>' . $this->util->html($foreignKey->database) . '</b>.';
276
            }
277
            if ($foreignKey->schema != '') {
278
                $target .= '<b>' . $this->util->html($foreignKey->schema) . '</b>.';
279
            }
280
            $target = $this->util->html($foreignKey->table) .
281
                '(' . \implode(', ', \array_map(function ($key) {
282
                    return $this->util->html($key);
283
                }, $foreignKey->target)) . ')';
284
            $details[] = [
285
                'name' => $this->util->html($name),
286
                'source' => '<i>' . \implode(
287
                    '</i>, <i>',
288
                    \array_map(function ($key) {
289
                        return $this->util->html($key);
290
                    }, $foreignKey->source)
291
                ) . '</i>',
292
                'target' => $target,
293
                'onDelete' => $this->util->html($foreignKey->onDelete),
294
                'onUpdate' => $this->util->html($foreignKey->onUpdate),
295
            ];
296
        }
297
298
        return \compact('mainActions', 'headers', 'details');
299
    }
300
301
    /**
302
     * Get the triggers of a table
303
     *
304
     * @param string $table     The table name
305
     *
306
     * @return array|null
307
     */
308
    public function getTableTriggers(string $table)
309
    {
310
        if (!$this->driver->support('trigger')) {
311
            return null;
312
        }
313
314
        $mainActions = [
315
            $this->trans->lang('Add trigger'),
316
        ];
317
318
        $headers = [
319
            $this->trans->lang('Name'),
320
            '&nbsp;',
321
            '&nbsp;',
322
            '&nbsp;',
323
        ];
324
325
        $details = [];
326
        // From table.inc.php
327
        $triggers = $this->driver->triggers($table);
328
        foreach ($triggers as $name => $trigger) {
329
            $details[] = [
330
                $this->util->html($trigger->timing),
331
                $this->util->html($trigger->event),
332
                $this->util->html($name),
333
                $this->trans->lang('Alter'),
334
            ];
335
        }
336
337
        return \compact('mainActions', 'headers', 'details');
338
    }
339
340
    /**
341
     * Get foreign keys
342
     *
343
     * @param string $table     The table name
344
     *
345
     * @return void
346
     */
347
    private function getForeignKeys(string $table = '')
348
    {
349
        $this->referencableTables = $this->driver->referencableTables($table);
350
        $this->foreignKeys = [];
351
        foreach ($this->referencableTables as $tableName => $field) {
352
            $name = \str_replace('`', '``', $tableName) . '`' . \str_replace('`', '``', $field->name);
353
            // not escapeId() - used in JS
354
            $this->foreignKeys[$name] = $tableName;
355
        }
356
    }
357
358
    /**
359
     * Get field types
360
     *
361
     * @param string $type  The type name
362
     *
363
     * @return array
364
     */
365
    public function getFieldTypes(string $type = '')
366
    {
367
        // From includes/editing.inc.php
368
        $extraTypes = [];
369
        if ($type && !$this->driver->typeExists($type) && !isset($this->foreignKeys[$type]) &&
370
            !\array_key_exists($this->trans->lang('Current'), $extraTypes)) {
371
            $extraTypes[$this->trans->lang('Current')] = [$type];
372
        }
373
        if ($this->foreignKeys) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->foreignKeys 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...
374
            $this->driver->setStructuredType($this->trans->lang('Foreign keys'), $this->foreignKeys);
375
        }
376
        return \array_merge($extraTypes, $this->driver->structuredTypes());
377
    }
378
379
    /**
380
     * Get required data for create/update on tables
381
     *
382
     * @param string $table The table name
383
     *
384
     * @return array
385
     */
386
    public function getTableData(string $table = '')
387
    {
388
        $mainActions = [
389
            'table-save' => $this->trans->lang('Save'),
390
            'table-cancel' => $this->trans->lang('Cancel'),
391
        ];
392
393
        // From create.inc.php
394
        $status = [];
395
        $fields = [];
396
        if ($table !== '') {
397
            $status = $this->driver->tableStatus($table);
398
            if (!$status) {
399
                throw new Exception($this->trans->lang('No tables.'));
400
            }
401
            $fields = $this->driver->fields($table);
402
        }
403
404
        $this->getForeignKeys($table);
405
406
        $hasAutoIncrement = false;
407
        foreach ($fields as &$field) {
408
            $hasAutoIncrement = $hasAutoIncrement || $field->autoIncrement;
409
            $field->hasDefault = $field->default !== null;
410
            if (\preg_match('~^CURRENT_TIMESTAMP~i', $field->onUpdate)) {
411
                $field->onUpdate = 'CURRENT_TIMESTAMP';
412
            }
413
414
            $type = $field->type;
415
            $field->types = $this->getFieldTypes($type);
416
            $field->lengthRequired = !$field->length && \preg_match('~var(char|binary)$~', $type);
417
            $field->collationHidden = !\preg_match('~(char|text|enum|set)$~', $type);
418
            $field->unsignedHidden = !(!$type || \preg_match($this->driver->numberRegex(), $type));
419
            $field->onUpdateHidden = !\preg_match('~timestamp|datetime~', $type);
420
            $field->onDeleteHidden = !\preg_match('~`~', $type);
421
        }
422
        $options = [
423
            'hasAutoIncrement' => $hasAutoIncrement,
424
            'onUpdate' => ['CURRENT_TIMESTAMP'],
425
            'onDelete' => $this->driver->onActions(),
426
        ];
427
428
        $collations = $this->driver->collations();
429
        $engines = $this->driver->engines();
430
        $support = [
431
            'columns' => $this->driver->support('columns'),
432
            'comment' => $this->driver->support('comment'),
433
            'partitioning' => $this->driver->support('partitioning'),
434
            'move_col' => $this->driver->support('move_col'),
435
            'drop_col' => $this->driver->support('drop_col'),
436
        ];
437
438
        $foreignKeys = $this->foreignKeys;
439
        $unsigned = $this->driver->unsigned();
440
        // Give the var a better name
441
        $table = $status;
442
        return \compact('mainActions', 'table', 'foreignKeys', 'fields',
443
            'options', 'collations', 'engines', 'support', 'unsigned');
444
    }
445
446
    /**
447
     * Get fields for a new column
448
     *
449
     * @return TableFieldEntity
450
     */
451
    public function getTableField()
452
    {
453
        $this->getForeignKeys();
454
        $field = new TableFieldEntity();
455
        $field->types = $this->getFieldTypes();
456
        return $field;
457
    }
458
459
    /**
460
     * Create or alter a table
461
     *
462
     * @param array  $values    The table values
463
     * @param string $table     The table name
464
     * @param array $origFields The table fields
465
     *
466
     * @return array
467
     */
468
    private function createOrAlterTable(array $values, string $table = '', array $origFields = [])
469
    {
470
        // From create.inc.php
471
        $values['fields'] = (array)$values['fields'];
472
        if ($values['autoIncrementCol']) {
473
            $values['fields'][$values['autoIncrementCol']]['autoIncrement'] = true;
474
        }
475
476
        $fields = [];
477
        $allFields = [];
478
        $useAllFields = false;
479
        $foreign = [];
480
        $origField = \reset($origFields);
481
        $after = ' FIRST';
482
483
        $this->getForeignKeys();
484
485
        foreach ($values['fields'] as $key => $field) {
486
            $orig = $field['orig'];
487
            $field = TableFieldEntity::make($field);
488
            $foreignKey = $this->foreignKeys[$field->type] ?? null;
489
            //! can collide with user defined type
490
            $typeField = ($foreignKey === null ? $field :
491
                TableFieldEntity::make($this->referencableTables[$foreignKey]));
492
            // Originally, deleted fields have the "field" field set to an empty string.
493
            // But in our implementation, the "name" field is not set.
494
            if ($field->name != '') {
495
                $field->autoIncrement = ($key == $values['autoIncrementCol']);
496
497
                $processedField = $this->util->processField($field, $typeField);
498
                $allFields[] = [$orig, $processedField, $after];
499
                if (!$origField || $field->changed($origField)) {
500
                    $fields[] = [$orig, $processedField, $after];
501
                    if ($orig != '' || $after) {
502
                        $useAllFields = true;
503
                    }
504
                }
505
                if ($foreignKey !== null) {
506
                    $fkey = new ForeignKeyEntity();
507
                    $fkey->table = $this->foreignKeys[$field->type];
508
                    $fkey->source = [$field->name];
509
                    $fkey->target = [$typeField['field']];
510
                    $fkey->onDelete = $field->onDelete;
511
                    $foreign[$this->driver->escapeId($field->name)] =
512
                        ($table != '' && $this->driver->jush() != 'sqlite' ? 'ADD' : ' ') .
513
                        $this->driver->formatForeignKey($fkey);
514
                }
515
                $after = ' AFTER ' . $this->driver->escapeId($field->name);
516
            } elseif ($orig != '') {
517
                // A missing "name" field and a not empty "orig" field means the column is to be dropped.
518
                // We also append null in the array because the drivers code accesses field at position 1.
519
                $useAllFields = true;
520
                $fields[] = [$orig, null];
521
            }
522
            if ($orig != '') {
523
                $origField = \next($origFields);
524
                if (!$origField) {
525
                    $after = '';
526
                }
527
            }
528
        }
529
530
        // For now, partitioning is not implemented
531
        $partitioning = '';
532
        // if($partition_by[$values['partition_by']])
533
        // {
534
        //     $partitions = [];
535
        //     if($values['partition_by'] == 'RANGE' || $values['partition_by'] == 'LIST')
536
        //     {
537
        //         foreach(\array_filter($values['partition_names']) as $key => $val)
538
        //         {
539
        //             $value = $values['partition_values'][$key];
540
        //             $partitions[] = "\n  PARTITION " . $this->driver->escapeId($val) .
541
        //                 ' VALUES ' . ($values['partition_by'] == 'RANGE' ? 'LESS THAN' : 'IN') .
542
        //                 ($value != '' ? ' ($value)' : ' MAXVALUE'); //! SQL injection
543
        //         }
544
        //     }
545
        //     $partitioning .= "\nPARTITION BY $values[partition_by]($values[partition])" .
546
        //         ($partitions // $values['partition'] can be expression, not only column
547
        //         ? ' (' . \implode(',', $partitions) . "\n)"
548
        //         : ($values['partitions'] ? ' PARTITIONS ' . (+$values['partitions']) : '')
549
        //     );
550
        // }
551
        // elseif($this->driver->support('partitioning') &&
552
        //     \preg_match('~partitioned~', $this->tableStatus->Create_options))
553
        // {
554
        //     $partitioning .= "\nREMOVE PARTITIONING";
555
        // }
556
557
        if (!isset($values['comment'])) {
558
            $values['comment'] = '';
559
        }
560
        if (!isset($values['engine']) || !$values['engine']) {
561
            $values['engine'] = '';
562
        }
563
        if (!isset($values['collation']) || !$values['collation']) {
564
            $values['collation'] = '';
565
        }
566
567
        if ($this->tableStatus != null) {
568
            // if ($values['comment'] == $this->tableStatus->comment) {
569
            //     $values['comment'] = null;
570
            // }
571
            if ($values['engine'] == $this->tableStatus->engine) {
572
                $values['engine'] = '';
573
            }
574
            if ($values['collation'] == $this->tableStatus->collation) {
575
                $values['collation'] = '';
576
            }
577
        }
578
579
        $name = \trim($values['name']);
580
        $autoIncrement = $this->util->number($this->util->input()->getAutoIncrementStep());
581
        if ($this->driver->jush() == 'sqlite' && ($useAllFields || $foreign)) {
582
            $fields = $allFields;
583
        }
584
585
        $success = $this->driver->alterTable($table, $name, $fields, $foreign, $values['comment'],
586
            $values['engine'], $values['collation'], \intval($autoIncrement), $partitioning);
587
588
        $error = $this->driver->error();
589
590
        return \compact('success', 'error');
591
    }
592
593
    /**
594
     * Create a table
595
     *
596
     * @param array  $values    The table values
597
     *
598
     * @return array
599
     */
600
    public function createTable(array $values)
601
    {
602
        $results = $this->createOrAlterTable($values);
603
        $results['message'] = $this->trans->lang('Table has been created.');
604
        return $results;
605
    }
606
607
    /**
608
     * Alter a table
609
     *
610
     * @param string $table     The table name
611
     * @param array  $values    The table values
612
     *
613
     * @return array
614
     */
615
    public function alterTable(string $table, array $values)
616
    {
617
        $origFields = $this->driver->fields($table);
618
        $this->tableStatus = $this->driver->tableStatus($table);
619
        if (!$this->tableStatus) {
620
            throw new Exception($this->trans->lang('No tables.'));
621
        }
622
623
        $results = $this->createOrAlterTable($values, $table, $origFields);
624
        $results['message'] = $this->trans->lang('Table has been altered.');
625
        return $results;
626
    }
627
628
    /**
629
     * Drop a table
630
     *
631
     * @param string $table     The table name
632
     *
633
     * @return array
634
     */
635
    public function dropTable(string $table)
636
    {
637
        $success = $this->driver->dropTables([$table]);
638
639
        $error = $this->driver->error();
640
641
        $message = $this->trans->lang('Table has been dropped.');
642
643
        return \compact('success', 'message', 'error');
644
    }
645
}
646