Passed
Push — main ( 6b0a6d...3c8d3b )
by Thierry
02:27
created

ExportAdmin::exportDatabases()   B

Complexity

Conditions 7
Paths 20

Size

Total Lines 48
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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