Passed
Push — master ( 13323c...7f793c )
by Maurício
08:09
created

Export::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
/* vim: set expandtab sw=4 ts=4 sts=4: */
3
/**
4
 * function for the main export logic
5
 *
6
 * @package PhpMyAdmin
7
 */
8
declare(strict_types=1);
9
10
namespace PhpMyAdmin;
11
12
use PhpMyAdmin\Plugins\ExportPlugin;
13
14
/**
15
 * PhpMyAdmin\Export class
16
 *
17
 * @package PhpMyAdmin
18
 */
19
class Export
20
{
21
    /**
22
     * @var DatabaseInterface
23
     */
24
    private $dbi;
25
26
    /**
27
     * Export constructor.
28
     * @param DatabaseInterface $dbi DatabaseInterface instance
29
     */
30
    public function __construct($dbi)
31
    {
32
        $this->dbi = $dbi;
33
    }
34
35
    /**
36
     * Sets a session variable upon a possible fatal error during export
37
     *
38
     * @return void
39
     */
40
    public function shutdown(): void
41
    {
42
        $error = error_get_last();
43
        if ($error != null && mb_strpos($error['message'], "execution time")) {
44
            //set session variable to check if there was error while exporting
45
            $_SESSION['pma_export_error'] = $error['message'];
46
        }
47
    }
48
49
    /**
50
     * Detect ob_gzhandler
51
     *
52
     * @return bool
53
     */
54
    public function isGzHandlerEnabled(): bool
55
    {
56
        return in_array('ob_gzhandler', ob_list_handlers());
57
    }
58
59
    /**
60
     * Detect whether gzencode is needed; it might not be needed if
61
     * the server is already compressing by itself
62
     *
63
     * @return bool Whether gzencode is needed
64
     */
65
    public function gzencodeNeeded(): bool
66
    {
67
        /*
68
         * We should gzencode only if the function exists
69
         * but we don't want to compress twice, therefore
70
         * gzencode only if transparent compression is not enabled
71
         * and gz compression was not asked via $cfg['OBGzip']
72
         * but transparent compression does not apply when saving to server
73
         */
74
        $chromeAndGreaterThan43 = PMA_USR_BROWSER_AGENT == 'CHROME'
0 ignored issues
show
introduced by
The condition PhpMyAdmin\PMA_USR_BROWSER_AGENT == 'CHROME' is always false.
Loading history...
75
            && PMA_USR_BROWSER_VER >= 43; // see bug #4942
76
77
        return function_exists('gzencode')
78
            && ((! ini_get('zlib.output_compression')
79
                    && ! $this->isGzHandlerEnabled())
80
                || $GLOBALS['save_on_server']
81
                || $chromeAndGreaterThan43);
82
    }
83
84
    /**
85
     * Output handler for all exports, if needed buffering, it stores data into
86
     * $dump_buffer, otherwise it prints them out.
87
     *
88
     * @param string $line the insert statement
89
     *
90
     * @return bool Whether output succeeded
91
     */
92
    public function outputHandler(?string $line): bool
93
    {
94
        global $time_start, $dump_buffer, $dump_buffer_len, $save_filename;
95
96
        // Kanji encoding convert feature
97
        if ($GLOBALS['output_kanji_conversion']) {
98
            $line = Encoding::kanjiStrConv(
99
                $line,
0 ignored issues
show
Bug introduced by
It seems like $line can also be of type null; however, parameter $str of PhpMyAdmin\Encoding::kanjiStrConv() does only seem to accept string, 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

99
                /** @scrutinizer ignore-type */ $line,
Loading history...
100
                $GLOBALS['knjenc'],
101
                isset($GLOBALS['xkana']) ? $GLOBALS['xkana'] : ''
102
            );
103
        }
104
105
        // If we have to buffer data, we will perform everything at once at the end
106
        if ($GLOBALS['buffer_needed']) {
107
            $dump_buffer .= $line;
108
            if ($GLOBALS['onfly_compression']) {
109
                $dump_buffer_len += strlen($line);
110
111
                if ($dump_buffer_len > $GLOBALS['memory_limit']) {
112
                    if ($GLOBALS['output_charset_conversion']) {
113
                        $dump_buffer = Encoding::convertString(
114
                            'utf-8',
115
                            $GLOBALS['charset'],
116
                            $dump_buffer
117
                        );
118
                    }
119
                    if ($GLOBALS['compression'] == 'gzip'
120
                        && $this->gzencodeNeeded()
121
                    ) {
122
                        // as a gzipped file
123
                        // without the optional parameter level because it bugs
124
                        $dump_buffer = gzencode($dump_buffer);
125
                    }
126
                    if ($GLOBALS['save_on_server']) {
127
                        $write_result = @fwrite($GLOBALS['file_handle'], $dump_buffer);
128
                        // Here, use strlen rather than mb_strlen to get the length
129
                        // in bytes to compare against the number of bytes written.
130
                        if ($write_result != strlen($dump_buffer)) {
131
                            $GLOBALS['message'] = Message::error(
132
                                __('Insufficient space to save the file %s.')
133
                            );
134
                            $GLOBALS['message']->addParam($save_filename);
135
                            return false;
136
                        }
137
                    } else {
138
                        echo $dump_buffer;
139
                    }
140
                    $dump_buffer = '';
141
                    $dump_buffer_len = 0;
142
                }
143
            } else {
144
                $time_now = time();
145
                if ($time_start >= $time_now + 30) {
146
                    $time_start = $time_now;
147
                    header('X-pmaPing: Pong');
148
                } // end if
149
            }
150
        } elseif ($GLOBALS['asfile']) {
151
            if ($GLOBALS['output_charset_conversion']) {
152
                $line = Encoding::convertString(
153
                    'utf-8',
154
                    $GLOBALS['charset'],
155
                    $line
0 ignored issues
show
Bug introduced by
It seems like $line can also be of type null; however, parameter $what of PhpMyAdmin\Encoding::convertString() does only seem to accept string, 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

155
                    /** @scrutinizer ignore-type */ $line
Loading history...
156
                );
157
            }
158
            if ($GLOBALS['save_on_server'] && mb_strlen($line) > 0) {
159
                if (! is_null($GLOBALS['file_handle'])) {
160
                    $write_result = @fwrite($GLOBALS['file_handle'], $line);
161
                } else {
162
                    $write_result = false;
163
                }
164
                // Here, use strlen rather than mb_strlen to get the length
165
                // in bytes to compare against the number of bytes written.
166
                if (! $write_result
0 ignored issues
show
Bug Best Practice introduced by
The expression $write_result of type false|integer is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
167
                    || $write_result != strlen($line)
168
                ) {
169
                    $GLOBALS['message'] = Message::error(
170
                        __('Insufficient space to save the file %s.')
171
                    );
172
                    $GLOBALS['message']->addParam($save_filename);
173
                    return false;
174
                }
175
                $time_now = time();
176
                if ($time_start >= $time_now + 30) {
177
                    $time_start = $time_now;
178
                    header('X-pmaPing: Pong');
179
                } // end if
180
            } else {
181
                // We export as file - output normally
182
                echo $line;
183
            }
184
        } else {
185
            // We export as html - replace special chars
186
            echo htmlspecialchars($line);
187
        }
188
        return true;
189
    }
190
191
    /**
192
     * Returns HTML containing the footer for a displayed export
193
     *
194
     * @param string $back_button   the link for going Back
195
     * @param string $refreshButton the link for refreshing page
196
     *
197
     * @return string the HTML output
198
     */
199
    public function getHtmlForDisplayedExportFooter(
200
        string $back_button,
201
        string $refreshButton
202
    ): string {
203
        /**
204
         * Close the html tags and add the footers for on-screen export
205
         */
206
        return '</textarea>'
207
            . '    </form>'
208
            . '<br>'
209
            // bottom back button
210
            . $back_button
211
            . $refreshButton
212
            . '</div>'
213
            . '<script type="text/javascript">' . "\n"
214
            . '//<![CDATA[' . "\n"
215
            . 'var $body = $("body");' . "\n"
216
            . '$("#textSQLDUMP")' . "\n"
217
            . '.width($body.width() - 50)' . "\n"
218
            . '.height($body.height() - 100);' . "\n"
219
            . '//]]>' . "\n"
220
            . '</script>' . "\n";
221
    }
222
223
    /**
224
     * Computes the memory limit for export
225
     *
226
     * @return int the memory limit
227
     */
228
    public function getMemoryLimit(): int
229
    {
230
        $memory_limit = trim(ini_get('memory_limit'));
231
        $memory_limit_num = (int) substr($memory_limit, 0, -1);
232
        $lowerLastChar = strtolower(substr($memory_limit, -1));
233
        // 2 MB as default
234
        if (empty($memory_limit) || '-1' == $memory_limit) {
235
            $memory_limit = 2 * 1024 * 1024;
236
        } elseif ($lowerLastChar == 'm') {
237
            $memory_limit = $memory_limit_num * 1024 * 1024;
238
        } elseif ($lowerLastChar == 'k') {
239
            $memory_limit = $memory_limit_num * 1024;
240
        } elseif ($lowerLastChar == 'g') {
241
            $memory_limit = $memory_limit_num * 1024 * 1024 * 1024;
242
        } else {
243
            $memory_limit = (int) $memory_limit;
244
        }
245
246
        // Some of memory is needed for other things and as threshold.
247
        // During export I had allocated (see memory_get_usage function)
248
        // approx 1.2MB so this comes from that.
249
        if ($memory_limit > 1500000) {
250
            $memory_limit -= 1500000;
251
        }
252
253
        // Some memory is needed for compression, assume 1/3
254
        $memory_limit /= 8;
255
        return $memory_limit;
256
    }
257
258
    /**
259
     * Return the filename and MIME type for export file
260
     *
261
     * @param string       $export_type       type of export
262
     * @param string       $remember_template whether to remember template
263
     * @param ExportPlugin $export_plugin     the export plugin
264
     * @param string       $compression       compression asked
265
     * @param string       $filename_template the filename template
266
     *
267
     * @return string[] the filename template and mime type
268
     */
269
    public function getFilenameAndMimetype(
270
        string $export_type,
271
        string $remember_template,
272
        ExportPlugin $export_plugin,
273
        string $compression,
274
        string $filename_template
275
    ): array {
276
        if ($export_type == 'server') {
277
            if (! empty($remember_template)) {
278
                $GLOBALS['PMA_Config']->setUserValue(
279
                    'pma_server_filename_template',
280
                    'Export/file_template_server',
281
                    $filename_template
282
                );
283
            }
284
        } elseif ($export_type == 'database') {
285
            if (! empty($remember_template)) {
286
                $GLOBALS['PMA_Config']->setUserValue(
287
                    'pma_db_filename_template',
288
                    'Export/file_template_database',
289
                    $filename_template
290
                );
291
            }
292
        } else {
293
            if (! empty($remember_template)) {
294
                $GLOBALS['PMA_Config']->setUserValue(
295
                    'pma_table_filename_template',
296
                    'Export/file_template_table',
297
                    $filename_template
298
                );
299
            }
300
        }
301
        $filename = Util::expandUserString($filename_template);
302
        // remove dots in filename (coming from either the template or already
303
        // part of the filename) to avoid a remote code execution vulnerability
304
        $filename = Sanitize::sanitizeFilename($filename, $replaceDots = true);
305
306
        // Grab basic dump extension and mime type
307
        // Check if the user already added extension;
308
        // get the substring where the extension would be if it was included
309
        $extension_start_pos = mb_strlen($filename) - mb_strlen(
310
            $export_plugin->getProperties()->getExtension()
311
        ) - 1;
312
        $user_extension = mb_substr(
313
            $filename,
314
            $extension_start_pos,
315
            mb_strlen($filename)
316
        );
317
        $required_extension = "." . $export_plugin->getProperties()->getExtension();
318
        if (mb_strtolower($user_extension) != $required_extension) {
319
            $filename  .= $required_extension;
320
        }
321
        $mime_type  = $export_plugin->getProperties()->getMimeType();
322
323
        // If dump is going to be compressed, set correct mime_type and add
324
        // compression to extension
325
        if ($compression == 'gzip') {
326
            $filename  .= '.gz';
327
            $mime_type = 'application/x-gzip';
328
        } elseif ($compression == 'zip') {
329
            $filename  .= '.zip';
330
            $mime_type = 'application/zip';
331
        }
332
        return [
333
            $filename,
334
            $mime_type,
335
        ];
336
    }
337
338
    /**
339
     * Open the export file
340
     *
341
     * @param string  $filename     the export filename
342
     * @param boolean $quick_export whether it's a quick export or not
343
     *
344
     * @return array the full save filename, possible message and the file handle
345
     */
346
    public function openFile(string $filename, bool $quick_export): array
347
    {
348
        $file_handle = null;
349
        $message = '';
350
        $doNotSaveItOver = true;
351
352
        if (isset($_POST['quick_export_onserver_overwrite'])) {
353
            $doNotSaveItOver = $_POST['quick_export_onserver_overwrite'] != 'saveitover';
354
        }
355
356
        $save_filename = Util::userDir($GLOBALS['cfg']['SaveDir'])
357
            . preg_replace('@[/\\\\]@', '_', $filename);
358
359
        if (@file_exists($save_filename)
360
            && ((! $quick_export && empty($_POST['onserver_overwrite']))
361
            || ($quick_export
362
            && $doNotSaveItOver))
363
        ) {
364
            $message = Message::error(
365
                __(
366
                    'File %s already exists on server, '
367
                    . 'change filename or check overwrite option.'
368
                )
369
            );
370
            $message->addParam($save_filename);
371
        } elseif (@is_file($save_filename) && ! @is_writable($save_filename)) {
372
            $message = Message::error(
373
                __(
374
                    'The web server does not have permission '
375
                    . 'to save the file %s.'
376
                )
377
            );
378
            $message->addParam($save_filename);
379
        } elseif (! $file_handle = @fopen($save_filename, 'w')) {
380
            $message = Message::error(
381
                __(
382
                    'The web server does not have permission '
383
                    . 'to save the file %s.'
384
                )
385
            );
386
            $message->addParam($save_filename);
387
        }
388
        return [
389
            $save_filename,
390
            $message,
391
            $file_handle,
392
        ];
393
    }
394
395
    /**
396
     * Close the export file
397
     *
398
     * @param resource $file_handle   the export file handle
399
     * @param string   $dump_buffer   the current dump buffer
400
     * @param string   $save_filename the export filename
401
     *
402
     * @return Message a message object (or empty string)
403
     */
404
    public function closeFile(
405
        $file_handle,
406
        string $dump_buffer,
407
        string $save_filename
408
    ): Message {
409
        $write_result = @fwrite($file_handle, $dump_buffer);
410
        fclose($file_handle);
411
        // Here, use strlen rather than mb_strlen to get the length
412
        // in bytes to compare against the number of bytes written.
413
        if (strlen($dump_buffer) > 0
414
            && (! $write_result || $write_result != strlen($dump_buffer))
415
        ) {
416
            $message = new Message(
417
                __('Insufficient space to save the file %s.'),
418
                Message::ERROR,
419
                [$save_filename]
420
            );
421
        } else {
422
            $message = new Message(
423
                __('Dump has been saved to file %s.'),
424
                Message::SUCCESS,
425
                [$save_filename]
426
            );
427
        }
428
        return $message;
429
    }
430
431
    /**
432
     * Compress the export buffer
433
     *
434
     * @param array|string $dump_buffer the current dump buffer
435
     * @param string       $compression the compression mode
436
     * @param string       $filename    the filename
437
     *
438
     * @return array|string|bool
439
     */
440
    public function compress($dump_buffer, string $compression, string $filename)
441
    {
442
        if ($compression == 'zip' && function_exists('gzcompress')) {
443
            $zipExtension = new ZipExtension();
444
            $filename = substr($filename, 0, -4); // remove extension (.zip)
445
            $dump_buffer = $zipExtension->createFile($dump_buffer, $filename);
446
        } elseif ($compression == 'gzip' && $this->gzencodeNeeded()) {
447
            // without the optional parameter level because it bugs
448
            $dump_buffer = gzencode($dump_buffer);
0 ignored issues
show
Bug introduced by
It seems like $dump_buffer can also be of type array; however, parameter $data of gzencode() does only seem to accept string, 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

448
            $dump_buffer = gzencode(/** @scrutinizer ignore-type */ $dump_buffer);
Loading history...
449
        }
450
        return $dump_buffer;
451
    }
452
453
    /**
454
     * Saves the dump_buffer for a particular table in an array
455
     * Used in separate files export
456
     *
457
     * @param string  $object_name the name of current object to be stored
458
     * @param boolean $append      optional boolean to append to an existing index or not
459
     *
460
     * @return void
461
     */
462
    public function saveObjectInBuffer(string $object_name, bool $append = false): void
463
    {
464
        global $dump_buffer_objects, $dump_buffer, $dump_buffer_len;
465
466
        if (! empty($dump_buffer)) {
467
            if ($append && isset($dump_buffer_objects[$object_name])) {
468
                $dump_buffer_objects[$object_name] .= $dump_buffer;
469
            } else {
470
                $dump_buffer_objects[$object_name] = $dump_buffer;
471
            }
472
        }
473
474
        // Re - initialize
475
        $dump_buffer = '';
476
        $dump_buffer_len = 0;
477
    }
478
479
    /**
480
     * Returns HTML containing the header for a displayed export
481
     *
482
     * @param string $export_type the export type
483
     * @param string $db          the database name
484
     * @param string $table       the table name
485
     *
486
     * @return string[] the generated HTML and back button
487
     */
488
    public function getHtmlForDisplayedExportHeader(
489
        string $export_type,
490
        string $db,
491
        string $table
492
    ): array {
493
        $html = '<div>';
494
495
        /**
496
         * Displays a back button with all the $_POST data in the URL
497
         * (store in a variable to also display after the textarea)
498
         */
499
        $back_button = '<p id="export_back_button">[ <a href="';
500
        if ($export_type == 'server') {
501
            $back_button .= 'server_export.php" data-post="' . Url::getCommon([], '');
502
        } elseif ($export_type == 'database') {
503
            $back_button .= 'db_export.php" data-post="' . Url::getCommon(['db' => $db], '');
504
        } else {
505
            $back_button .= 'tbl_export.php" data-post="' . Url::getCommon(
506
                [
507
                    'db' => $db,
508
                    'table' => $table,
509
                ],
510
                ''
511
            );
512
        }
513
514
        // Convert the multiple select elements from an array to a string
515
        if ($export_type == 'database') {
516
            $structOrDataForced = empty($_POST['structure_or_data_forced']);
517
            if ($structOrDataForced && ! isset($_POST['table_structure'])) {
518
                $_POST['table_structure'] = [];
519
            }
520
            if ($structOrDataForced && ! isset($_POST['table_data'])) {
521
                $_POST['table_data'] = [];
522
            }
523
        }
524
525
        foreach ($_POST as $name => $value) {
526
            if (! is_array($value)) {
527
                $back_button .= '&amp;' . urlencode((string) $name) . '=' . urlencode((string) $value);
528
            }
529
        }
530
        $back_button .= '&amp;repopulate=1">' . __('Back') . '</a> ]</p>';
531
        $html .= '<br>';
532
        $html .= $back_button;
533
        $refreshButton = '<form id="export_refresh_form" method="POST" action="export.php" class="disableAjax">';
534
        $refreshButton .= '[ <a class="disableAjax" onclick="$(this).parent().submit()">' . __('Refresh') . '</a> ]';
535
        foreach ($_POST as $name => $value) {
536
            if (is_array($value)) {
537
                foreach ($value as $val) {
538
                    $refreshButton .= '<input type="hidden" name="' . htmlentities((string) $name) . '[]" value="' . htmlentities((string) $val) . '">';
539
                }
540
            } else {
541
                $refreshButton .= '<input type="hidden" name="' . htmlentities((string) $name) . '" value="' . htmlentities((string) $value) . '">';
542
            }
543
        }
544
        $refreshButton .= '</form>';
545
        $html .= $refreshButton
546
            . '<br>'
547
            . '<form name="nofunction">'
548
            . '<textarea name="sqldump" cols="50" rows="30" '
549
            . 'id="textSQLDUMP" wrap="OFF">';
550
551
        return [
552
            $html,
553
            $back_button,
554
            $refreshButton,
555
        ];
556
    }
557
558
    /**
559
     * Export at the server level
560
     *
561
     * @param string|array $db_select       the selected databases to export
562
     * @param string       $whatStrucOrData structure or data or both
563
     * @param ExportPlugin $export_plugin   the selected export plugin
564
     * @param string       $crlf            end of line character(s)
565
     * @param string       $err_url         the URL in case of error
566
     * @param string       $export_type     the export type
567
     * @param bool         $do_relation     whether to export relation info
568
     * @param bool         $do_comments     whether to add comments
569
     * @param bool         $do_mime         whether to add MIME info
570
     * @param bool         $do_dates        whether to add dates
571
     * @param array        $aliases         alias information for db/table/column
572
     * @param string       $separate_files  whether it is a separate-files export
573
     *
574
     * @return void
575
     */
576
    public function exportServer(
577
        $db_select,
578
        string $whatStrucOrData,
579
        ExportPlugin $export_plugin,
580
        string $crlf,
581
        string $err_url,
582
        string $export_type,
583
        bool $do_relation,
584
        bool $do_comments,
585
        bool $do_mime,
586
        bool $do_dates,
587
        array $aliases,
588
        string $separate_files
589
    ): void {
590
        if (! empty($db_select)) {
591
            $tmp_select = implode($db_select, '|');
0 ignored issues
show
Bug introduced by
It seems like $db_select can also be of type array; however, parameter $glue of implode() does only seem to accept string, 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

591
            $tmp_select = implode(/** @scrutinizer ignore-type */ $db_select, '|');
Loading history...
Bug introduced by
'|' of type string is incompatible with the type array expected by parameter $pieces of implode(). ( Ignorable by Annotation )

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

591
            $tmp_select = implode($db_select, /** @scrutinizer ignore-type */ '|');
Loading history...
592
            $tmp_select = '|' . $tmp_select . '|';
593
        }
594
        // Walk over databases
595
        foreach ($GLOBALS['dblist']->databases as $current_db) {
596
            if (isset($tmp_select)
597
                && mb_strpos(' ' . $tmp_select, '|' . $current_db . '|')
598
            ) {
599
                $tables = $this->dbi->getTables($current_db);
600
                $this->exportDatabase(
601
                    $current_db,
602
                    $tables,
603
                    $whatStrucOrData,
604
                    $tables,
605
                    $tables,
606
                    $export_plugin,
607
                    $crlf,
608
                    $err_url,
609
                    $export_type,
610
                    $do_relation,
611
                    $do_comments,
612
                    $do_mime,
613
                    $do_dates,
614
                    $aliases,
615
                    $separate_files == 'database' ? $separate_files : ''
616
                );
617
                if ($separate_files == 'server') {
618
                    $this->saveObjectInBuffer($current_db);
619
                }
620
            }
621
        } // end foreach database
622
    }
623
624
    /**
625
     * Export at the database level
626
     *
627
     * @param string       $db              the database to export
628
     * @param array        $tables          the tables to export
629
     * @param string       $whatStrucOrData structure or data or both
630
     * @param array        $table_structure whether to export structure for each table
631
     * @param array        $table_data      whether to export data for each table
632
     * @param ExportPlugin $export_plugin   the selected export plugin
633
     * @param string       $crlf            end of line character(s)
634
     * @param string       $err_url         the URL in case of error
635
     * @param string       $export_type     the export type
636
     * @param bool         $do_relation     whether to export relation info
637
     * @param bool         $do_comments     whether to add comments
638
     * @param bool         $do_mime         whether to add MIME info
639
     * @param bool         $do_dates        whether to add dates
640
     * @param array        $aliases         Alias information for db/table/column
641
     * @param string       $separate_files  whether it is a separate-files export
642
     *
643
     * @return void
644
     */
645
    public function exportDatabase(
646
        string $db,
647
        array $tables,
648
        string $whatStrucOrData,
649
        array $table_structure,
650
        array $table_data,
651
        ExportPlugin $export_plugin,
652
        string $crlf,
653
        string $err_url,
654
        string $export_type,
655
        bool $do_relation,
656
        bool $do_comments,
657
        bool $do_mime,
658
        bool $do_dates,
659
        array $aliases,
660
        string $separate_files
661
    ): void {
662
        $db_alias = ! empty($aliases[$db]['alias'])
663
            ? $aliases[$db]['alias'] : '';
664
665
        if (! $export_plugin->exportDBHeader($db, $db_alias)) {
666
            return;
667
        }
668
        if (! $export_plugin->exportDBCreate($db, $export_type, $db_alias)) {
669
            return;
670
        }
671
        if ($separate_files == 'database') {
672
            $this->saveObjectInBuffer('database', true);
673
        }
674
675
        if (($GLOBALS['sql_structure_or_data'] == 'structure'
676
            || $GLOBALS['sql_structure_or_data'] == 'structure_and_data')
677
            && isset($GLOBALS['sql_procedure_function'])
678
        ) {
679
            $export_plugin->exportRoutines($db, $aliases);
680
681
            if ($separate_files == 'database') {
682
                $this->saveObjectInBuffer('routines');
683
            }
684
        }
685
686
        $views = [];
687
688
        foreach ($tables as $table) {
689
            $_table = new Table($table, $db);
690
            // if this is a view, collect it for later;
691
            // views must be exported after the tables
692
            $is_view = $_table->isView();
693
            if ($is_view) {
694
                $views[] = $table;
695
            }
696
            if (($whatStrucOrData == 'structure'
697
                || $whatStrucOrData == 'structure_and_data')
698
                && in_array($table, $table_structure)
699
            ) {
700
                // for a view, export a stand-in definition of the table
701
                // to resolve view dependencies (only when it's a single-file export)
702
                if ($is_view) {
703
                    if ($separate_files == ''
704
                        && isset($GLOBALS['sql_create_view'])
705
                        && ! $export_plugin->exportStructure(
706
                            $db,
707
                            $table,
708
                            $crlf,
709
                            $err_url,
710
                            'stand_in',
711
                            $export_type,
712
                            $do_relation,
713
                            $do_comments,
714
                            $do_mime,
715
                            $do_dates,
716
                            $aliases
717
                        )
718
                    ) {
719
                        break;
720
                    }
721
                } elseif (isset($GLOBALS['sql_create_table'])) {
722
                    $table_size = $GLOBALS['maxsize'];
723
                    // Checking if the maximum table size constrain has been set
724
                    // And if that constrain is a valid number or not
725
                    if ($table_size !== '' && is_numeric($table_size)) {
726
                        // This obtains the current table's size
727
                        $query = 'SELECT data_length + index_length
728
                              from information_schema.TABLES
729
                              WHERE table_schema = "' . $this->dbi->escapeString($db) . '"
730
                              AND table_name = "' . $this->dbi->escapeString($table) . '"';
731
732
                        $size = $this->dbi->fetchValue($query);
733
                        //Converting the size to MB
734
                        $size = ($size / 1024) / 1024;
735
                        if ($size > $table_size) {
736
                            continue;
737
                        }
738
                    }
739
740
                    if (! $export_plugin->exportStructure(
741
                        $db,
742
                        $table,
743
                        $crlf,
744
                        $err_url,
745
                        'create_table',
746
                        $export_type,
747
                        $do_relation,
748
                        $do_comments,
749
                        $do_mime,
750
                        $do_dates,
751
                        $aliases
752
                    )) {
753
                        break;
754
                    }
755
                }
756
            }
757
            // if this is a view or a merge table, don't export data
758
            if (($whatStrucOrData == 'data' || $whatStrucOrData == 'structure_and_data')
759
                && in_array($table, $table_data)
760
                && ! $is_view
761
            ) {
762
                $tableObj = new Table($table, $db);
763
                $nonGeneratedCols = $tableObj->getNonGeneratedColumns(true);
764
765
                $local_query  = 'SELECT ' . implode(', ', $nonGeneratedCols)
766
                    . ' FROM ' . Util::backquote($db)
767
                    . '.' . Util::backquote($table);
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($table) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

767
                    . '.' . /** @scrutinizer ignore-type */ Util::backquote($table);
Loading history...
768
769
                if (! $export_plugin->exportData(
770
                    $db,
771
                    $table,
772
                    $crlf,
773
                    $err_url,
774
                    $local_query,
775
                    $aliases
776
                )) {
777
                    break;
778
                }
779
            }
780
781
            // this buffer was filled, we save it and go to the next one
782
            if ($separate_files == 'database') {
783
                $this->saveObjectInBuffer('table_' . $table);
784
            }
785
786
            // now export the triggers (needs to be done after the data because
787
            // triggers can modify already imported tables)
788
            if (isset($GLOBALS['sql_create_trigger']) && ($whatStrucOrData == 'structure'
789
                || $whatStrucOrData == 'structure_and_data')
790
                && in_array($table, $table_structure)
791
            ) {
792
                if (! $export_plugin->exportStructure(
793
                    $db,
794
                    $table,
795
                    $crlf,
796
                    $err_url,
797
                    'triggers',
798
                    $export_type,
799
                    $do_relation,
800
                    $do_comments,
801
                    $do_mime,
802
                    $do_dates,
803
                    $aliases
804
                )) {
805
                    break;
806
                }
807
808
                if ($separate_files == 'database') {
809
                    $this->saveObjectInBuffer('table_' . $table, true);
810
                }
811
            }
812
        }
813
814
        if (isset($GLOBALS['sql_create_view'])) {
815
            foreach ($views as $view) {
816
                // no data export for a view
817
                if ($whatStrucOrData == 'structure'
818
                    || $whatStrucOrData == 'structure_and_data'
819
                ) {
820
                    if (! $export_plugin->exportStructure(
821
                        $db,
822
                        $view,
823
                        $crlf,
824
                        $err_url,
825
                        'create_view',
826
                        $export_type,
827
                        $do_relation,
828
                        $do_comments,
829
                        $do_mime,
830
                        $do_dates,
831
                        $aliases
832
                    )) {
833
                        break;
834
                    }
835
836
                    if ($separate_files == 'database') {
837
                        $this->saveObjectInBuffer('view_' . $view);
838
                    }
839
                }
840
            }
841
        }
842
843
        if (! $export_plugin->exportDBFooter($db)) {
844
            return;
845
        }
846
847
        // export metadata related to this db
848
        if (isset($GLOBALS['sql_metadata'])) {
849
            // Types of metadata to export.
850
            // In the future these can be allowed to be selected by the user
851
            $metadataTypes = $this->getMetadataTypes();
852
            $export_plugin->exportMetadata($db, $tables, $metadataTypes);
853
854
            if ($separate_files == 'database') {
855
                $this->saveObjectInBuffer('metadata');
856
            }
857
        }
858
859
        if ($separate_files == 'database') {
860
            $this->saveObjectInBuffer('extra');
861
        }
862
863
        if (($GLOBALS['sql_structure_or_data'] == 'structure'
864
            || $GLOBALS['sql_structure_or_data'] == 'structure_and_data')
865
            && isset($GLOBALS['sql_procedure_function'])
866
        ) {
867
            $export_plugin->exportEvents($db);
868
869
            if ($separate_files == 'database') {
870
                $this->saveObjectInBuffer('events');
871
            }
872
        }
873
    }
874
875
    /**
876
     * Export at the table level
877
     *
878
     * @param string       $db              the database to export
879
     * @param string       $table           the table to export
880
     * @param string       $whatStrucOrData structure or data or both
881
     * @param ExportPlugin $export_plugin   the selected export plugin
882
     * @param string       $crlf            end of line character(s)
883
     * @param string       $err_url         the URL in case of error
884
     * @param string       $export_type     the export type
885
     * @param bool         $do_relation     whether to export relation info
886
     * @param bool         $do_comments     whether to add comments
887
     * @param bool         $do_mime         whether to add MIME info
888
     * @param bool         $do_dates        whether to add dates
889
     * @param string|null  $allrows         whether "dump all rows" was ticked
890
     * @param string       $limit_to        upper limit
891
     * @param string       $limit_from      starting limit
892
     * @param string       $sql_query       query for which exporting is requested
893
     * @param array        $aliases         Alias information for db/table/column
894
     *
895
     * @return void
896
     */
897
    public function exportTable(
898
        string $db,
899
        string $table,
900
        string $whatStrucOrData,
901
        ExportPlugin $export_plugin,
902
        string $crlf,
903
        string $err_url,
904
        string $export_type,
905
        bool $do_relation,
906
        bool $do_comments,
907
        bool $do_mime,
908
        bool $do_dates,
909
        ?string $allrows,
910
        string $limit_to,
911
        string $limit_from,
912
        string $sql_query,
913
        array $aliases
914
    ): void {
915
        $db_alias = ! empty($aliases[$db]['alias'])
916
            ? $aliases[$db]['alias'] : '';
917
        if (! $export_plugin->exportDBHeader($db, $db_alias)) {
918
            return;
919
        }
920
        if (isset($allrows)
921
            && $allrows == '0'
922
            && $limit_to > 0
923
            && $limit_from >= 0
924
        ) {
925
            $add_query  = ' LIMIT '
926
                        . ($limit_from > 0 ? $limit_from . ', ' : '')
927
                        . $limit_to;
928
        } else {
929
            $add_query  = '';
930
        }
931
932
        $_table = new Table($table, $db);
933
        $is_view = $_table->isView();
934
        if ($whatStrucOrData == 'structure'
935
            || $whatStrucOrData == 'structure_and_data'
936
        ) {
937
            if ($is_view) {
938
                if (isset($GLOBALS['sql_create_view'])) {
939
                    if (! $export_plugin->exportStructure(
940
                        $db,
941
                        $table,
942
                        $crlf,
943
                        $err_url,
944
                        'create_view',
945
                        $export_type,
946
                        $do_relation,
947
                        $do_comments,
948
                        $do_mime,
949
                        $do_dates,
950
                        $aliases
951
                    )) {
952
                        return;
953
                    }
954
                }
955
            } elseif (isset($GLOBALS['sql_create_table'])) {
956
                if (! $export_plugin->exportStructure(
957
                    $db,
958
                    $table,
959
                    $crlf,
960
                    $err_url,
961
                    'create_table',
962
                    $export_type,
963
                    $do_relation,
964
                    $do_comments,
965
                    $do_mime,
966
                    $do_dates,
967
                    $aliases
968
                )) {
969
                    return;
970
                }
971
            }
972
        }
973
        // If this is an export of a single view, we have to export data;
974
        // for example, a PDF report
975
        // if it is a merge table, no data is exported
976
        if ($whatStrucOrData == 'data'
977
            || $whatStrucOrData == 'structure_and_data'
978
        ) {
979
            if (! empty($sql_query)) {
980
                // only preg_replace if needed
981
                if (! empty($add_query)) {
982
                    // remove trailing semicolon before adding a LIMIT
983
                    $sql_query = preg_replace('%;\s*$%', '', $sql_query);
984
                }
985
                $local_query = $sql_query . $add_query;
986
                $this->dbi->selectDb($db);
987
            } else {
988
                // Data is exported only for Non-generated columns
989
                $tableObj = new Table($table, $db);
990
                $nonGeneratedCols = $tableObj->getNonGeneratedColumns(true);
991
992
                $local_query  = 'SELECT ' . implode(', ', $nonGeneratedCols)
993
                    . ' FROM ' . Util::backquote($db)
994
                    . '.' . Util::backquote($table) . $add_query;
995
            }
996
            if (! $export_plugin->exportData(
997
                $db,
998
                $table,
999
                $crlf,
1000
                $err_url,
1001
                $local_query,
1002
                $aliases
1003
            )) {
1004
                return;
1005
            }
1006
        }
1007
        // now export the triggers (needs to be done after the data because
1008
        // triggers can modify already imported tables)
1009
        if (isset($GLOBALS['sql_create_trigger']) && ($whatStrucOrData == 'structure'
1010
            || $whatStrucOrData == 'structure_and_data')
1011
        ) {
1012
            if (! $export_plugin->exportStructure(
1013
                $db,
1014
                $table,
1015
                $crlf,
1016
                $err_url,
1017
                'triggers',
1018
                $export_type,
1019
                $do_relation,
1020
                $do_comments,
1021
                $do_mime,
1022
                $do_dates,
1023
                $aliases
1024
            )) {
1025
                return;
1026
            }
1027
        }
1028
        if (! $export_plugin->exportDBFooter($db)) {
1029
            return;
1030
        }
1031
1032
        if (isset($GLOBALS['sql_metadata'])) {
1033
            // Types of metadata to export.
1034
            // In the future these can be allowed to be selected by the user
1035
            $metadataTypes = $this->getMetadataTypes();
1036
            $export_plugin->exportMetadata($db, $table, $metadataTypes);
1037
        }
1038
    }
1039
1040
    /**
1041
     * Loads correct page after doing export
1042
     *
1043
     * @param string $db          the database name
1044
     * @param string $table       the table name
1045
     * @param string $export_type Export type
1046
     *
1047
     * @return void
1048
     */
1049
    public function showPage(string $db, string $table, string $export_type): void
0 ignored issues
show
Unused Code introduced by
The parameter $db is not used and could be removed. ( Ignorable by Annotation )

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

1049
    public function showPage(/** @scrutinizer ignore-unused */ string $db, string $table, string $export_type): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $table is not used and could be removed. ( Ignorable by Annotation )

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

1049
    public function showPage(string $db, /** @scrutinizer ignore-unused */ string $table, string $export_type): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1050
    {
1051
        global $cfg;
1052
        if ($export_type == 'server') {
1053
            $active_page = 'server_export.php';
0 ignored issues
show
Unused Code introduced by
The assignment to $active_page is dead and can be removed.
Loading history...
1054
            include_once ROOT_PATH . 'server_export.php';
1055
        } elseif ($export_type == 'database') {
1056
            $active_page = 'db_export.php';
1057
            include_once ROOT_PATH . 'db_export.php';
1058
        } else {
1059
            $active_page = 'tbl_export.php';
1060
            include_once ROOT_PATH . 'tbl_export.php';
1061
        }
1062
        exit();
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1063
    }
1064
1065
    /**
1066
     * Merge two alias arrays, if array1 and array2 have
1067
     * conflicting alias then array2 value is used if it
1068
     * is non empty otherwise array1 value.
1069
     *
1070
     * @param array $aliases1 first array of aliases
1071
     * @param array $aliases2 second array of aliases
1072
     *
1073
     * @return array resultant merged aliases info
1074
     */
1075
    public function mergeAliases(array $aliases1, array $aliases2): array
1076
    {
1077
        // First do a recursive array merge
1078
        // on aliases arrays.
1079
        $aliases = array_merge_recursive($aliases1, $aliases2);
1080
        // Now, resolve conflicts in aliases, if any
1081
        foreach ($aliases as $db_name => $db) {
1082
            // If alias key is an array then
1083
            // it is a merge conflict.
1084
            if (isset($db['alias']) && is_array($db['alias'])) {
1085
                $val1 = $db['alias'][0];
1086
                $val2 = $db['alias'][1];
1087
                // Use aliases2 alias if non empty
1088
                $aliases[$db_name]['alias']
1089
                    = empty($val2) ? $val1 : $val2;
1090
            }
1091
            if (! isset($db['tables'])) {
1092
                continue;
1093
            }
1094
            foreach ($db['tables'] as $tbl_name => $tbl) {
1095
                if (isset($tbl['alias']) && is_array($tbl['alias'])) {
1096
                    $val1 = $tbl['alias'][0];
1097
                    $val2 = $tbl['alias'][1];
1098
                    // Use aliases2 alias if non empty
1099
                    $aliases[$db_name]['tables'][$tbl_name]['alias']
1100
                        = empty($val2) ? $val1 : $val2;
1101
                }
1102
                if (! isset($tbl['columns'])) {
1103
                    continue;
1104
                }
1105
                foreach ($tbl['columns'] as $col => $col_as) {
1106
                    if (isset($col_as) && is_array($col_as)) {
1107
                        $val1 = $col_as[0];
1108
                        $val2 = $col_as[1];
1109
                        // Use aliases2 alias if non empty
1110
                        $aliases[$db_name]['tables'][$tbl_name]['columns'][$col]
1111
                            = empty($val2) ? $val1 : $val2;
1112
                    }
1113
                }
1114
            }
1115
        }
1116
        return $aliases;
1117
    }
1118
1119
    /**
1120
     * Locks tables
1121
     *
1122
     * @param string $db       database name
1123
     * @param array  $tables   list of table names
1124
     * @param string $lockType lock type; "[LOW_PRIORITY] WRITE" or "READ [LOCAL]"
1125
     *
1126
     * @return mixed result of the query
1127
     */
1128
    public function lockTables(string $db, array $tables, string $lockType = "WRITE")
1129
    {
1130
        $locks = [];
1131
        foreach ($tables as $table) {
1132
            $locks[] = Util::backquote($db) . "."
1133
                . Util::backquote($table) . " " . $lockType;
0 ignored issues
show
Bug introduced by
Are you sure PhpMyAdmin\Util::backquote($table) of type array|mixed|string can be used in concatenation? ( Ignorable by Annotation )

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

1133
                . /** @scrutinizer ignore-type */ Util::backquote($table) . " " . $lockType;
Loading history...
1134
        }
1135
1136
        $sql = "LOCK TABLES " . implode(", ", $locks);
1137
        return $this->dbi->tryQuery($sql);
1138
    }
1139
1140
    /**
1141
     * Releases table locks
1142
     *
1143
     * @return mixed result of the query
1144
     */
1145
    public function unlockTables()
1146
    {
1147
        return $this->dbi->tryQuery("UNLOCK TABLES");
1148
    }
1149
1150
    /**
1151
     * Returns all the metadata types that can be exported with a database or a table
1152
     *
1153
     * @return string[] metadata types.
1154
     */
1155
    public function getMetadataTypes(): array
1156
    {
1157
        return [
1158
            'column_info',
1159
            'table_uiprefs',
1160
            'tracking',
1161
            'bookmark',
1162
            'relation',
1163
            'table_coords',
1164
            'pdf_pages',
1165
            'savedsearches',
1166
            'central_columns',
1167
            'export_templates',
1168
        ];
1169
    }
1170
1171
    /**
1172
     * Returns the checked clause, depending on the presence of key in array
1173
     *
1174
     * @param string $key   the key to look for
1175
     * @param array  $array array to verify
1176
     *
1177
     * @return string the checked clause
1178
     */
1179
    public function getCheckedClause(string $key, array $array): string
1180
    {
1181
        if (in_array($key, $array)) {
1182
            return ' checked="checked"';
1183
        }
1184
1185
        return '';
1186
    }
1187
1188
    /**
1189
     * get all the export options and verify
1190
     * call and include the appropriate Schema Class depending on $export_type
1191
     *
1192
     * @param string|null $export_type format of the export
1193
     *
1194
     * @return void
1195
     */
1196
    public function processExportSchema(?string $export_type): void
1197
    {
1198
        /**
1199
         * default is PDF, otherwise validate it's only letters a-z
1200
         */
1201
        if (! isset($export_type) || ! preg_match('/^[a-zA-Z]+$/', $export_type)) {
1202
            $export_type = 'pdf';
1203
        }
1204
1205
        // sanitize this parameter which will be used below in a file inclusion
1206
        $export_type = Core::securePath($export_type);
1207
1208
        // get the specific plugin
1209
        /** @var \PhpMyAdmin\Plugins\SchemaPlugin $export_plugin */
1210
        $export_plugin = Plugins::getPlugin(
1211
            "schema",
1212
            $export_type,
1213
            'libraries/classes/Plugins/Schema/'
1214
        );
1215
1216
        // Check schema export type
1217
        if (is_null($export_plugin) || ! is_object($export_plugin)) {
1218
            Core::fatalError(__('Bad type!'));
1219
        }
1220
1221
        $this->dbi->selectDb($GLOBALS['db']);
1222
        $export_plugin->exportSchema($GLOBALS['db']);
1223
    }
1224
}
1225