Passed
Push — main ( 3c8d3b...bf9e72 )
by Thierry
01:52
created

ExportAdmin::dumpDatabaseCreation()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 12
rs 9.6111
1
<?php
2
3
namespace Lagdo\DbAdmin\DbAdmin;
4
5
use Lagdo\DbAdmin\Driver\Db\StatementInterface;
6
use Lagdo\DbAdmin\Driver\Entity\TableEntity;
7
use Exception;
8
9
use function count;
10
use function compact;
11
use function strlen;
12
use function in_array;
13
use function preg_replace;
14
use function preg_match;
15
use function str_replace;
16
use function implode;
17
use function is_numeric;
18
use function array_keys;
19
use function array_map;
20
use function array_unique;
21
use function array_merge;
22
23
/**
24
 * Admin export functions
25
 */
26
class ExportAdmin extends AbstractAdmin
27
{
28
    /**
29
     * The databases to dump
30
     *
31
     * @var array
32
     */
33
    protected $databases;
34
35
    /**
36
     * The tables to dump
37
     *
38
     * @var array
39
     */
40
    protected $tables;
41
42
    /**
43
     * The dump options
44
     *
45
     * @var array
46
     */
47
    protected $options;
48
49
    /**
50
     * The queries generated by the dump
51
     *
52
     * @var array
53
     */
54
    protected $queries = [];
55
56
    // Temp vars for data dumps
57
    private $insert = '';
58
    private $buffer = '';
59
    private $suffix = '';
60
    private $views = [];
61
    private $fkeys = [];
62
63
    /**
64
     * @param string $database
65
     * @param string $table
66
     *
67
     * @return array
68
     */
69
    private function getBaseOptions(string $database, string $table): array
70
    {
71
        // From dump.inc.php
72
        $db_style = ['', 'USE', 'DROP+CREATE', 'CREATE'];
73
        $table_style = ['', 'DROP+CREATE', 'CREATE'];
74
        $data_style = ['', 'TRUNCATE+INSERT', 'INSERT'];
75
        if ($this->driver->jush() == 'sql') { //! use insertOrUpdate() in all drivers
76
            $data_style[] = 'INSERT+UPDATE';
77
        }
78
        // \parse_str($_COOKIE['adminer_export'], $row);
79
        // if(!$row) {
80
        $row = [
81
            'output' => 'text',
82
            'format' => 'sql',
83
            'db_style' => ($database != '' ? '' : 'CREATE'),
84
            'table_style' => 'DROP+CREATE',
85
            'data_style' => 'INSERT',
86
        ];
87
        // }
88
        // if(!isset($row['events'])) { // backwards compatibility
89
        $row['routines'] = $row['events'] = ($table == '');
90
        $row['triggers'] = $row['table_style'];
91
        // }
92
93
        $options = [
94
            'output' => [
95
                'label' => $this->trans->lang('Output'),
96
                'options' => $this->util->dumpOutput(),
97
                'value' => $row['output'],
98
            ],
99
            'format' => [
100
                'label' => $this->trans->lang('Format'),
101
                'options' => $this->util->dumpFormat(),
102
                'value' => $row['format'],
103
            ],
104
            'table_style' => [
105
                'label' => $this->trans->lang('Tables'),
106
                'options' => $table_style,
107
                'value' => $row['table_style'],
108
            ],
109
            'auto_increment' => [
110
                'label' => $this->trans->lang('Auto Increment'),
111
                'value' => 1,
112
                'checked' => $row['autoIncrement'] ?? false,
113
            ],
114
            'data_style' => [
115
                'label' => $this->trans->lang('Data'),
116
                'options' => $data_style,
117
                'value' => $row['data_style'],
118
            ],
119
        ];
120
        if ($this->driver->jush() !== 'sqlite') {
121
            $options['db_style'] = [
122
                'label' => $this->trans->lang('Database'),
123
                'options' => $db_style,
124
                'value' => $row['db_style'],
125
            ];
126
            if ($this->driver->support('routine')) {
127
                $options['routines'] = [
128
                    'label' => $this->trans->lang('Routines'),
129
                    'value' => 1,
130
                    'checked' => $row['routines'],
131
                ];
132
            }
133
            if ($this->driver->support('event')) {
134
                $options['events'] = [
135
                    'label' => $this->trans->lang('Events'),
136
                    'value' => 1,
137
                    'checked' => $row['events'],
138
                ];
139
            }
140
        }
141
        if ($this->driver->support('trigger')) {
142
            $options['triggers'] = [
143
                'label' => $this->trans->lang('Triggers'),
144
                'value' => 1,
145
                'checked' => $row['triggers'],
146
            ];
147
        }
148
        return $options;
149
    }
150
151
    /**
152
     * @return array
153
     */
154
    private function getDbTables(): array
155
    {
156
        $tables = [
157
            'headers' => [$this->trans->lang('Tables'), $this->trans->lang('Data')],
158
            'details' => [],
159
        ];
160
        $tables_list = $this->driver->tables();
161
        foreach ($tables_list as $name => $type) {
162
            $prefix = preg_replace('~_.*~', '', $name);
163
            //! % may be part of table name
164
            // $checked = ($TABLE == '' || $TABLE == (\substr($TABLE, -1) == '%' ? "$prefix%" : $name));
165
            // $results['prefixes'][$prefix]++;
166
167
            $tables['details'][] = compact('prefix', 'name', 'type'/*, 'checked'*/);
168
        }
169
        return $tables;
170
    }
171
172
    /**
173
     * @return array
174
     */
175
    private function getDatabases(): array
176
    {
177
        $databases = [
178
            'headers' => [$this->trans->lang('Database'), $this->trans->lang('Data')],
179
            'details' => [],
180
        ];
181
        $databases_list = $this->driver->databases(false) ?? [];
182
        foreach ($databases_list as $name) {
183
            if (!$this->driver->isInformationSchema($name)) {
184
                $prefix = preg_replace('~_.*~', '', $name);
185
                // $results['prefixes'][$prefix]++;
186
187
                $databases['details'][] = compact('prefix', 'name');
188
            }
189
        }
190
        return $databases;
191
    }
192
193
    /**
194
     * Get data for export
195
     *
196
     * @param string $database      The database name
197
     * @param string $table
198
     *
199
     * @return array
200
     */
201
    public function getExportOptions(string $database, string $table = ''): array
202
    {
203
        $results = [
204
            'options' => $this->getBaseOptions($database, $table),
205
            'prefixes' => [],
206
            'labels' => [
207
                'export' => $this->trans->lang('Export'),
208
            ],
209
        ];
210
        if (($database)) {
211
            $results['tables'] = $this->getDbTables();
212
        } else {
213
            $results['databases'] = $this->getDatabases();
214
        }
215
        return $results;
216
    }
217
218
    /**
219
     * Dump routines and events in the connected database
220
     *
221
     * @param string $database      The database name
222
     *
223
     * @return void
224
     */
225
    protected function dumpRoutinesAndEvents(string $database)
226
    {
227
        // From dump.inc.php
228
        $style = $this->options['db_style'];
229
        $queries = [];
230
231
        if ($this->options['routines']) {
232
            $sql = 'SHOW FUNCTION STATUS WHERE Db = ' . $this->driver->quote($database);
233
            foreach ($this->driver->rows($sql) as $row) {
234
                $sql = 'SHOW CREATE FUNCTION ' . $this->driver->escapeId($row['Name']);
235
                $create = $this->admin->removeDefiner($this->driver->result($sql, 2));
236
                $queries[] = $this->driver->setUtf8mb4($create);
237
                if ($style != 'DROP+CREATE') {
238
                    $queries[] = 'DROP FUNCTION IF EXISTS ' . $this->driver->escapeId($row['Name']) . ';;';
239
                }
240
                $queries[] = "$create;;\n";
241
            }
242
            $sql = 'SHOW PROCEDURE STATUS WHERE Db = ' . $this->driver->quote($database);
243
            foreach ($this->driver->rows($sql) as $row) {
244
                $sql = 'SHOW CREATE PROCEDURE ' . $this->driver->escapeId($row['Name']);
245
                $create = $this->admin->removeDefiner($this->driver->result($sql, 2));
246
                $queries[] = $this->driver->setUtf8mb4($create);
247
                if ($style != 'DROP+CREATE') {
248
                    $queries[] = 'DROP PROCEDURE IF EXISTS ' . $this->driver->escapeId($row['Name']) . ';;';
249
                }
250
                $queries[] = "$create;;\n";
251
            }
252
        }
253
254
        if ($this->options['events']) {
255
            foreach ($this->driver->rows('SHOW EVENTS') as $row) {
256
                $sql = 'SHOW CREATE EVENT ' . $this->driver->escapeId($row['Name']);
257
                $create = $this->admin->removeDefiner($this->driver->result($sql, 3));
258
                $queries[] = $this->driver->setUtf8mb4($create);
259
                if ($style != 'DROP+CREATE') {
260
                    $queries[] = 'DROP EVENT IF EXISTS ' . $this->driver->escapeId($row['Name']) . ';;';
261
                }
262
                $queries[] = "$create;;\n";
263
            }
264
        }
265
266
        if (count($queries) > 0) {
267
            $this->queries[] = "DELIMITER ;;\n";
268
            foreach ($queries as $query) {
269
                $this->queries[] = $query;
270
            }
271
            $this->queries[] = "DELIMITER ;;\n";
272
        }
273
    }
274
275
    /**
276
     * Print CSV row
277
     *
278
     * @param array  $row
279
     *
280
     * @return void
281
     */
282
    protected function dumpCsv(array $row)
283
    {
284
        // From functions.inc.php
285
        foreach ($row as $key => $val) {
286
            if (preg_match('~["\n,;\t]|^0|\.\d*0$~', $val) || $val === '') {
287
                $row[$key] = '"' . str_replace('"', '""', $val) . '"';
288
            }
289
        }
290
        $separator = $this->options['format'] == 'csv' ? ',' :
291
            ($this->options['format'] == 'tsv' ? "\t" : ';');
292
        $this->queries[] = implode($separator, $row);
293
    }
294
295
    /**
296
     * Convert a value to string
297
     *
298
     * @param mixed  $val
299
     * @param object $field
300
     *
301
     * @return string
302
     */
303
    protected function convertToString($val, $field): string
304
    {
305
        // From functions.inc.php
306
        if ($val === null) {
307
            return 'NULL';
308
        }
309
        return $this->driver->unconvertField($field, preg_match($this->driver->numberRegex(), $field->type) &&
310
            !preg_match('~\[~', $field->fullType) && is_numeric($val) ?
311
            $val : $this->driver->quote(($val === false ? 0 : $val)));
312
    }
313
314
    /**
315
     * Export table structure
316
     *
317
     * @param string $table
318
     * @param string $style
319
     * @param int    $tableType       0 table, 1 view, 2 temporary view table
320
     *
321
     * @return null prints data
322
     */
323
    protected function dumpTableOrView(string $table, string $style, int $tableType = 0)
324
    {
325
        // From adminer.inc.php
326
        if ($this->options['format'] != 'sql') {
327
            $this->queries[] = "\xef\xbb\xbf"; // UTF-8 byte order mark
328
            if ($style) {
329
                $this->dumpCsv(array_keys($this->driver->fields($table)));
330
            }
331
            return;
332
        }
333
334
        if ($tableType == 2) {
335
            $fields = [];
336
            foreach ($this->driver->fields($table) as $name => $field) {
337
                $fields[] = $this->driver->escapeId($name) . ' ' . $field->fullType;
338
            }
339
            $create = 'CREATE TABLE ' . $this->driver->table($table) . ' (' . implode(', ', $fields) . ')';
340
        } else {
341
            $create = $this->driver->sqlForCreateTable($table, $this->options['autoIncrement'], $style);
342
        }
343
        $this->driver->setUtf8mb4($create);
344
        if ($style && $create) {
345
            if ($style == 'DROP+CREATE' || $tableType == 1) {
346
                $this->queries[] = 'DROP ' . ($tableType == 2 ? 'VIEW' : 'TABLE') .
347
                    ' IF EXISTS ' . $this->driver->table($table) . ';';
348
            }
349
            if ($tableType == 1) {
350
                $create = $this->admin->removeDefiner($create);
351
            }
352
            $this->queries[] = $create . ';';
353
        }
354
    }
355
356
    /**
357
     * @param array $row
358
     * @param StatementInterface $statement
359
     *
360
     * @return array
361
     */
362
    private function getDataRowKeys(array $row, StatementInterface $statement): array
363
    {
364
        $values = [];
365
        $keys = [];
366
        foreach ($row as $val) {
367
            $field = $statement->fetchField();
368
            $keys[] = $field->name();
369
            $key = $this->driver->escapeId($field->name());
370
            $values[] = "$key = VALUES($key)";
371
        }
372
        $this->suffix = ";\n";
373
        if ($this->options['data_style'] == 'INSERT+UPDATE') {
374
            $this->suffix = "\nON DUPLICATE KEY UPDATE " . implode(', ', $values) . ";\n";
375
        }
376
        return $keys;
377
    }
378
379
    /**
380
     * @param string $table
381
     * @param array $fields
382
     * @param array $row
383
     * @param array $keys
384
     *
385
     * @return void
386
     */
387
    private function dumpDataRow(string $table, array $fields, array $row, array $keys)
388
    {
389
        if ($this->options['format'] != 'sql') {
390
            if ($this->options['data_style'] == 'table') {
391
                $this->dumpCsv($keys);
392
                $this->options['data_style'] = 'INSERT';
393
            }
394
            $this->dumpCsv($row);
395
        } else {
396
            if (!$this->insert) {
397
                $this->insert = 'INSERT INTO ' . $this->driver->table($table) . ' (' .
398
                    implode(', ', array_map(function ($key) {
399
                        return $this->driver->escapeId($key);
400
                    }, $keys)) . ') VALUES';
401
            }
402
            foreach ($row as $key => $val) {
403
                $field = $fields[$key];
404
                $row[$key] = $this->convertToString($val, $field);
405
            }
406
            $max_packet = ($this->driver->jush() == 'sqlite' ? 0 : 1048576); // default, minimum is 1024
407
            $s = ($max_packet ? "\n" : ' ') . '(' . implode(",\t", $row) . ')';
408
            if (!$this->buffer) {
409
                $this->buffer = $this->insert . $s;
410
            } elseif (strlen($this->buffer) + 4 + strlen($s) + strlen($this->suffix) < $max_packet) { // 4 - length specification
411
                $this->buffer .= ",$s";
412
            } else {
413
                $this->queries[] = $this->buffer . $this->suffix;
414
                $this->buffer = $this->insert . $s;
415
            }
416
        }
417
    }
418
419
    /** Export table data
420
     *
421
     * @param string $table
422
     * @param string $query
423
     *
424
     * @return null prints data
425
     */
426
    protected function dumpData(string $table, string $query)
427
    {
428
        $fields = [];
429
        if ($this->options['data_style']) {
430
            if ($this->options['format'] == 'sql') {
431
                if ($this->options['data_style'] == 'TRUNCATE+INSERT') {
432
                    $this->queries[] = $this->driver->sqlForTruncateTable($table) . ";\n";
433
                }
434
                $fields = $this->driver->fields($table);
435
            }
436
            $statement = $this->driver->query($query); // 1 - MYSQLI_USE_RESULT //! enum and set as numbers
437
            if ($statement) {
438
                $this->insert = '';
439
                $this->buffer = '';
440
                $this->suffix = '';
441
                $keys = [];
442
                $fetch_function = ($table != '' ? 'fetchAssoc' : 'fetchRow');
443
                while ($row = $statement->$fetch_function()) {
444
                    if (empty($keys)) {
445
                        $keys = $this->getDataRowKeys($row, $statement);
446
                    }
447
                    $this->dumpDataRow($table, $fields, $row, $keys);
448
                }
449
                if ($this->buffer) {
450
                    $this->queries[] = $this->buffer . $this->suffix;
451
                }
452
            } elseif ($this->options['format'] == 'sql') {
453
                $this->queries[] = '-- ' . str_replace("\n", ' ', $this->driver->error()) . "\n";
454
            }
455
        }
456
    }
457
458
    /**
459
     * @param string $table
460
     * @param TableEntity $tableStatus
461
     * @param bool $dumpTable
462
     * @param bool $dumpData
463
     *
464
     * @return void
465
     */
466
    private function dumpTable(string $table, TableEntity $tableStatus, bool $dumpTable, bool $dumpData)
467
    {
468
        if ($dumpTable || $dumpData) {
469
            $this->dumpTableOrView($table, ($dumpTable ? $this->options['table_style'] : ''));
470
            if ($dumpData) {
471
                $fields = $this->driver->fields($table);
472
                $query = 'SELECT *' . $this->driver->convertFields($fields, $fields) .
473
                    ' FROM ' . $this->driver->table($table);
474
                $this->dumpData($table, $query);
475
            }
476
            if ($this->options['is_sql'] && $this->options['triggers'] && $dumpTable &&
477
                ($triggers = $this->driver->sqlForCreateTrigger($table))) {
478
                $this->queries[] = 'DELIMITER ;';
479
                $this->queries[] = $triggers;
480
                $this->queries[] = 'DELIMITER ;';
481
            }
482
            if ($this->options['is_sql']) {
483
                $this->queries[] = '';
484
            }
485
        }
486
    }
487
488
    /**
489
     * Dump tables
490
     *
491
     * @param string $database      The database name
492
     *
493
     * @return void
494
     */
495
    protected function dumpTables(string $database)
496
    {
497
        $dbDumpTable = $this->tables['list'] === '*' && in_array($database, $this->databases['list']);
498
        $dbDumpData = in_array($database, $this->databases['data']);
499
        $this->views = []; // View names
500
        $this->fkeys = []; // Table names for foreign keys
501
        $dbTables = $this->driver->tableStatuses(true);
502
        foreach ($dbTables as $table => $tableStatus) {
503
            $isView = $this->driver->isView($tableStatus);
504
            if ($isView) {
505
                // The views will be dumped after the tables
506
                $this->views[] = $table;
507
                continue;
508
            }
509
            $this->fkeys[] = $table;
510
            $dumpTable = $dbDumpTable || in_array($table, $this->tables['list']);
511
            $dumpData = $dbDumpData || in_array($table, $this->tables['data']);
512
            $this->dumpTable($table, $tableStatus, $dumpTable, $dumpData);
513
        }
514
    }
515
516
    /**
517
     * @return void
518
     */
519
    private function dumpViewsAndFKeys()
520
    {
521
        // Add FKs after creating tables (except in MySQL which uses SET FOREIGN_KEY_CHECKS=0)
522
        if ($this->driver->support('fkeys_sql')) {
523
            foreach ($this->fkeys as $table) {
524
                $this->queries[] = $this->driver->sqlForForeignKeys($table);
525
            }
526
        }
527
        // Dump the views after all the tables
528
        foreach ($this->views as $view) {
529
            $this->dumpTableOrView($view, $this->options['table_style'], 1);
530
        }
531
    }
532
533
    /**
534
     * @param string $database
535
     *
536
     * @return void
537
     */
538
    private function dumpDatabaseCreation(string $database)
539
    {
540
        $style = $this->options['db_style'];
541
        $this->driver->connect($database, '');
542
        $sql = 'SHOW CREATE DATABASE ' . $this->driver->escapeId($database);
543
        if ($this->options['is_sql'] && preg_match('~CREATE~', $style) &&
544
            ($create = $this->driver->result($sql, 1))) {
545
            $this->driver->setUtf8mb4($create);
546
            if ($style == 'DROP+CREATE') {
547
                $this->queries[] = 'DROP DATABASE IF EXISTS ' . $this->driver->escapeId($database) . ';';
548
            }
549
            $this->queries[] = $create . ";\n";
550
        }
551
    }
552
553
    /**
554
     * @param string $database
555
     *
556
     * @return void
557
     */
558
    private function dumpDatabase(string $database)
559
    {
560
        $this->dumpDatabaseCreation($database);
561
        if ($this->options['is_sql'] && $this->driver->jush() === 'sql') {
562
            // Dump routines and events currently works only for MySQL.
563
            if ($this->options['db_style']) {
564
                if (($query = $this->driver->sqlForUseDatabase($database))) {
565
                    $this->queries[] = $query . ';';
566
                }
567
                $this->queries[] = ''; // Empty line
568
            }
569
            $this->dumpRoutinesAndEvents($database);
570
        }
571
572
        if (!$this->options['table_style'] && !$this->options['data_style']) {
573
            return;
574
        }
575
576
        $this->dumpTables($database);
577
        $this->dumpViewsAndFKeys();
578
    }
579
580
    /**
581
     * Export databases
582
     *
583
     * @param array  $databases     The databases to dump
584
     * @param array  $tables        The tables to dump
585
     * @param array  $options       The export options
586
     *
587
     * @return array|string
588
     */
589
    public function exportDatabases(array $databases, array $tables, array $options)
590
    {
591
        // From dump.inc.php
592
        // $tables = array_flip($options['tables']) + array_flip($options['data']);
593
        // $ext = dump_headers((count($tables) == 1 ? key($tables) : DB), (DB == '' || count($tables) > 1));
594
        $options['is_sql'] = preg_match('~sql~', $options['format']);
595
        $this->databases = $databases;
596
        $this->tables = $tables;
597
        $this->options = $options;
598
599
        $headers = null;
600
        if ($this->options['is_sql']) {
601
            $headers = [
602
                'version' => $this->driver->version(),
603
                'driver' => $this->driver->name(),
604
                'server' => str_replace("\n", ' ', $this->driver->serverInfo()),
605
                'sql' => false,
606
                'data_style' => false,
607
            ];
608
            if ($this->driver->jush() == 'sql') {
609
                $headers['sql'] = true;
610
                if (isset($options['data_style'])) {
611
                    $headers['data_style'] = true;
612
                }
613
                // Set some options in database server
614
                $this->driver->query("SET time_zone = '+00:00'");
615
                $this->driver->query("SET sql_mode = ''");
616
            }
617
        }
618
619
        foreach (array_unique(array_merge($databases['list'], $databases['data'])) as $database) {
620
            try {
621
                $this->dumpDatabase($database);
622
            }
623
            catch (Exception $e) {
624
                return $e->getMessage();
625
            }
626
        }
627
628
        if ($this->options['is_sql']) {
629
            $this->queries[] = '-- ' . $this->driver->result('SELECT NOW()');
630
        }
631
632
        return [
633
            'headers' => $headers,
634
            'queries' => $this->queries,
635
        ];
636
    }
637
}
638