Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / Export / Export.php
blobe2e431a0326e68f6ee217fdb4f0a17e1d3d7f423
1 <?php
2 /**
3 * function for the main export logic
4 */
6 declare(strict_types=1);
8 namespace PhpMyAdmin\Export;
10 use PhpMyAdmin\Config;
11 use PhpMyAdmin\Current;
12 use PhpMyAdmin\DatabaseInterface;
13 use PhpMyAdmin\Encoding;
14 use PhpMyAdmin\Exceptions\ExportException;
15 use PhpMyAdmin\FlashMessages;
16 use PhpMyAdmin\Identifiers\DatabaseName;
17 use PhpMyAdmin\Message;
18 use PhpMyAdmin\Plugins;
19 use PhpMyAdmin\Plugins\ExportPlugin;
20 use PhpMyAdmin\Plugins\SchemaPlugin;
21 use PhpMyAdmin\Sanitize;
22 use PhpMyAdmin\Table\Table;
23 use PhpMyAdmin\Url;
24 use PhpMyAdmin\Util;
25 use PhpMyAdmin\ZipExtension;
27 use function __;
28 use function array_filter;
29 use function array_merge_recursive;
30 use function error_get_last;
31 use function fclose;
32 use function file_exists;
33 use function fopen;
34 use function function_exists;
35 use function fwrite;
36 use function gzencode;
37 use function header;
38 use function htmlentities;
39 use function htmlspecialchars;
40 use function http_build_query;
41 use function implode;
42 use function in_array;
43 use function ini_get;
44 use function ini_parse_quantity;
45 use function is_array;
46 use function is_file;
47 use function is_numeric;
48 use function is_string;
49 use function is_writable;
50 use function mb_strlen;
51 use function mb_strtolower;
52 use function mb_substr;
53 use function ob_list_handlers;
54 use function preg_match;
55 use function preg_replace;
56 use function str_contains;
57 use function strlen;
58 use function substr;
59 use function time;
61 use const ENT_COMPAT;
63 /**
64 * PhpMyAdmin\Export\Export class
66 class Export
68 public string $dumpBuffer = '';
70 public int $dumpBufferLength = 0;
72 /** @var mixed[] */
73 public array $dumpBufferObjects = [];
75 public function __construct(private DatabaseInterface $dbi)
79 /**
80 * Sets a session variable upon a possible fatal error during export
82 public function shutdown(): void
84 $error = error_get_last();
85 if ($error === null || ! str_contains($error['message'], 'execution time')) {
86 return;
89 //set session variable to check if there was error while exporting
90 $_SESSION['pma_export_error'] = $error['message'];
93 /**
94 * Detect ob_gzhandler
96 public function isGzHandlerEnabled(): bool
98 /** @var string[] $handlers */
99 $handlers = ob_list_handlers();
101 return in_array('ob_gzhandler', $handlers, true);
105 * Detect whether gzencode is needed; it might not be needed if
106 * the server is already compressing by itself
108 public function gzencodeNeeded(): bool
111 * We should gzencode only if the function exists
112 * but we don't want to compress twice, therefore
113 * gzencode only if transparent compression is not enabled
114 * and gz compression was not asked via $cfg['OBGzip']
115 * but transparent compression does not apply when saving to server
117 return function_exists('gzencode')
118 && ((! ini_get('zlib.output_compression')
119 && ! $this->isGzHandlerEnabled())
120 || $GLOBALS['save_on_server']
121 || Config::getInstance()->get('PMA_USR_BROWSER_AGENT') === 'CHROME');
125 * Output handler for all exports, if needed buffering, it stores data into
126 * $this->dumpBuffer, otherwise it prints them out.
128 * @param string $line the insert statement
130 public function outputHandler(string $line): bool
132 $GLOBALS['time_start'] ??= null;
133 $GLOBALS['save_filename'] ??= null;
135 // Kanji encoding convert feature
136 if ($GLOBALS['output_kanji_conversion']) {
137 $line = Encoding::kanjiStrConv($line, $GLOBALS['knjenc'], $GLOBALS['xkana'] ?? '');
140 // If we have to buffer data, we will perform everything at once at the end
141 if ($GLOBALS['buffer_needed']) {
142 $this->dumpBuffer .= $line;
143 if ($GLOBALS['onfly_compression']) {
144 $this->dumpBufferLength += strlen($line);
146 if ($this->dumpBufferLength > $GLOBALS['memory_limit']) {
147 if ($GLOBALS['output_charset_conversion']) {
148 $this->dumpBuffer = Encoding::convertString('utf-8', $GLOBALS['charset'], $this->dumpBuffer);
151 if ($GLOBALS['compression'] === 'gzip' && $this->gzencodeNeeded()) {
152 // as a gzipped file
153 // without the optional parameter level because it bugs
154 $this->dumpBuffer = (string) gzencode($this->dumpBuffer);
157 if ($GLOBALS['save_on_server']) {
158 $writeResult = @fwrite($GLOBALS['file_handle'], $this->dumpBuffer);
159 // Here, use strlen rather than mb_strlen to get the length
160 // in bytes to compare against the number of bytes written.
161 if ($writeResult != strlen($this->dumpBuffer)) {
162 $GLOBALS['message'] = Message::error(
163 __('Insufficient space to save the file %s.'),
165 $GLOBALS['message']->addParam($GLOBALS['save_filename']);
167 return false;
169 } else {
170 echo $this->dumpBuffer;
173 $this->dumpBuffer = '';
174 $this->dumpBufferLength = 0;
176 } else {
177 $timeNow = time();
178 if ($GLOBALS['time_start'] >= $timeNow + 30) {
179 $GLOBALS['time_start'] = $timeNow;
180 header('X-pmaPing: Pong');
183 } elseif ($GLOBALS['asfile']) {
184 if ($GLOBALS['output_charset_conversion']) {
185 $line = Encoding::convertString('utf-8', $GLOBALS['charset'], $line);
188 if ($GLOBALS['save_on_server'] && $line !== '') {
189 if ($GLOBALS['file_handle'] !== null) {
190 $writeResult = @fwrite($GLOBALS['file_handle'], $line);
191 } else {
192 $writeResult = false;
195 // Here, use strlen rather than mb_strlen to get the length
196 // in bytes to compare against the number of bytes written.
197 if ($writeResult === 0 || $writeResult === false || $writeResult != strlen($line)) {
198 $GLOBALS['message'] = Message::error(
199 __('Insufficient space to save the file %s.'),
201 $GLOBALS['message']->addParam($GLOBALS['save_filename']);
203 return false;
206 $timeNow = time();
207 if ($GLOBALS['time_start'] >= $timeNow + 30) {
208 $GLOBALS['time_start'] = $timeNow;
209 header('X-pmaPing: Pong');
211 } else {
212 // We export as file - output normally
213 echo $line;
215 } else {
216 // We export as html - replace special chars
217 echo htmlspecialchars($line, ENT_COMPAT);
220 return true;
224 * Returns HTML containing the footer for a displayed export
226 * @param string $exportType the export type
227 * @param string $db the database name
228 * @param string $table the table name
230 * @return string the HTML output
232 public function getHtmlForDisplayedExportFooter(
233 string $exportType,
234 string $db,
235 string $table,
236 ): string {
238 * Close the html tags and add the footers for on-screen export
240 return '</textarea>'
241 . ' </form>'
242 . '<br>'
243 // bottom back button
244 . $this->getHTMLForBackButton($exportType, $db, $table)
245 . $this->getHTMLForRefreshButton($exportType)
246 . '</div>'
247 . '<script>' . "\n"
248 . '//<![CDATA[' . "\n"
249 . 'var $body = $("body");' . "\n"
250 . '$("#textSQLDUMP")' . "\n"
251 . '.width($body.width() - 50)' . "\n"
252 . '.height($body.height() - 100);' . "\n"
253 . '//]]>' . "\n"
254 . '</script>' . "\n";
258 * Computes the memory limit for export
260 * @return int the memory limit
262 public function getMemoryLimit(): int
264 $memoryLimit = ini_parse_quantity((string) ini_get('memory_limit'));
266 // Some of memory is needed for other things and as threshold.
267 // During export I had allocated (see memory_get_usage function)
268 // approx 1.2MB so this comes from that.
269 if ($memoryLimit > 1500000) {
270 $memoryLimit -= 1500000;
273 // Some memory is needed for compression, assume 1/3
274 $memoryLimit /= 8;
276 return $memoryLimit;
280 * Returns the filename and MIME type for a compression and an export plugin
282 * @param ExportPlugin $exportPlugin the export plugin
283 * @param string $compression compression asked
284 * @param string $filename the filename
286 * @return string[] the filename and mime type
288 public function getFinalFilenameAndMimetypeForFilename(
289 ExportPlugin $exportPlugin,
290 string $compression,
291 string $filename,
292 ): array {
293 // Grab basic dump extension and mime type
294 // Check if the user already added extension;
295 // get the substring where the extension would be if it was included
296 $requiredExtension = '.' . $exportPlugin->getProperties()->getExtension();
297 $extensionLength = mb_strlen($requiredExtension);
298 $userExtension = mb_substr($filename, -$extensionLength);
299 if (mb_strtolower($userExtension) !== $requiredExtension) {
300 $filename .= $requiredExtension;
303 $mediaType = $exportPlugin->getProperties()->getMimeType();
305 // If dump is going to be compressed, set correct mime_type and add
306 // compression to extension
307 if ($compression === 'gzip') {
308 $filename .= '.gz';
309 $mediaType = 'application/x-gzip';
310 } elseif ($compression === 'zip') {
311 $filename .= '.zip';
312 $mediaType = 'application/zip';
315 return [$filename, $mediaType];
319 * Return the filename and MIME type for export file
321 * @param string $exportType type of export
322 * @param string $rememberTemplate whether to remember template
323 * @param ExportPlugin $exportPlugin the export plugin
324 * @param string $compression compression asked
325 * @param string $filenameTemplate the filename template
327 * @return string[] the filename template and mime type
329 public function getFilenameAndMimetype(
330 string $exportType,
331 string $rememberTemplate,
332 ExportPlugin $exportPlugin,
333 string $compression,
334 string $filenameTemplate,
335 ): array {
336 $config = Config::getInstance();
337 if ($exportType === 'server') {
338 if ($rememberTemplate !== '' && $rememberTemplate !== '0') {
339 $config->setUserValue('pma_server_filename_template', 'Export/file_template_server', $filenameTemplate);
341 } elseif ($exportType === 'database') {
342 if ($rememberTemplate !== '' && $rememberTemplate !== '0') {
343 $config->setUserValue('pma_db_filename_template', 'Export/file_template_database', $filenameTemplate);
345 } elseif ($exportType === 'raw') {
346 if ($rememberTemplate !== '' && $rememberTemplate !== '0') {
347 $config->setUserValue('pma_raw_filename_template', 'Export/file_template_raw', $filenameTemplate);
349 } elseif ($rememberTemplate !== '' && $rememberTemplate !== '0') {
350 $config->setUserValue('pma_table_filename_template', 'Export/file_template_table', $filenameTemplate);
353 $filename = Util::expandUserString($filenameTemplate);
354 // remove dots in filename (coming from either the template or already
355 // part of the filename) to avoid a remote code execution vulnerability
356 $filename = Sanitize::sanitizeFilename($filename, true);
358 return $this->getFinalFilenameAndMimetypeForFilename($exportPlugin, $compression, $filename);
362 * Open the export file
364 * @param string $filename the export filename
365 * @param bool $quickExport whether it's a quick export or not
367 * @psalm-return array{string, Message|null, resource|null}
369 public function openFile(string $filename, bool $quickExport): array
371 $fileHandle = null;
372 $message = null;
373 $doNotSaveItOver = true;
375 if (isset($_POST['quick_export_onserver_overwrite'])) {
376 $doNotSaveItOver = $_POST['quick_export_onserver_overwrite'] !== 'saveitover';
379 $saveFilename = Util::userDir(Config::getInstance()->settings['SaveDir'] ?? '')
380 . preg_replace('@[/\\\\]@', '_', $filename);
382 if (
383 @file_exists($saveFilename)
384 && ((! $quickExport && empty($_POST['onserver_overwrite']))
385 || ($quickExport
386 && $doNotSaveItOver))
388 $message = Message::error(
390 'File %s already exists on server, change filename or check overwrite option.',
393 $message->addParam($saveFilename);
394 } elseif (@is_file($saveFilename) && ! @is_writable($saveFilename)) {
395 $message = Message::error(
397 'The web server does not have permission to save the file %s.',
400 $message->addParam($saveFilename);
401 } else {
402 $fileHandle = @fopen($saveFilename, 'w');
403 if ($fileHandle === false) {
404 $fileHandle = null;
405 $message = Message::error(
407 'The web server does not have permission to save the file %s.',
410 $message->addParam($saveFilename);
414 return [$saveFilename, $message, $fileHandle];
418 * Close the export file
420 * @param resource $fileHandle the export file handle
421 * @param string $dumpBuffer the current dump buffer
422 * @param string $saveFilename the export filename
424 * @return Message a message object (or empty string)
426 public function closeFile(
427 $fileHandle,
428 string $dumpBuffer,
429 string $saveFilename,
430 ): Message {
431 $writeResult = @fwrite($fileHandle, $dumpBuffer);
432 fclose($fileHandle);
433 // Here, use strlen rather than mb_strlen to get the length
434 // in bytes to compare against the number of bytes written.
435 if ($dumpBuffer !== '' && $writeResult !== strlen($dumpBuffer)) {
436 return new Message(
437 __('Insufficient space to save the file %s.'),
438 Message::ERROR,
439 [$saveFilename],
443 return new Message(
444 __('Dump has been saved to file %s.'),
445 Message::SUCCESS,
446 [$saveFilename],
451 * Compress the export buffer
453 * @param mixed[]|string $dumpBuffer the current dump buffer
454 * @param string $compression the compression mode
455 * @param string $filename the filename
457 public function compress(array|string $dumpBuffer, string $compression, string $filename): array|string|bool
459 if ($compression === 'zip' && function_exists('gzcompress')) {
460 $zipExtension = new ZipExtension();
461 $filename = substr($filename, 0, -4); // remove extension (.zip)
462 $dumpBuffer = $zipExtension->createFile($dumpBuffer, $filename);
463 } elseif ($compression === 'gzip' && $this->gzencodeNeeded() && is_string($dumpBuffer)) {
464 // without the optional parameter level because it bugs
465 $dumpBuffer = gzencode($dumpBuffer);
468 return $dumpBuffer;
472 * Saves the dump buffer for a particular table in an array
473 * Used in separate files export
475 * @param string $objectName the name of current object to be stored
476 * @param bool $append optional boolean to append to an existing index or not
478 public function saveObjectInBuffer(string $objectName, bool $append = false): void
480 if ($this->dumpBuffer !== '') {
481 if ($append && isset($this->dumpBufferObjects[$objectName])) {
482 $this->dumpBufferObjects[$objectName] .= $this->dumpBuffer;
483 } else {
484 $this->dumpBufferObjects[$objectName] = $this->dumpBuffer;
488 // Re - initialize
489 $this->dumpBuffer = '';
490 $this->dumpBufferLength = 0;
494 * Returns HTML containing the header for a displayed export
496 * @param string $exportType the export type
497 * @param string $db the database name
498 * @param string $table the table name
500 * @return string the generated HTML and back button
502 public function getHtmlForDisplayedExportHeader(
503 string $exportType,
504 string $db,
505 string $table,
506 ): string {
508 * Displays a back button with all the $_POST data in the URL
510 return '<div>'
511 . '<br>'
512 . $this->getHTMLForBackButton($exportType, $db, $table)
513 . $this->getHTMLForRefreshButton($exportType)
514 . '<br>'
515 . '<form name="nofunction">'
516 . '<textarea name="sqldump" cols="50" rows="30" '
517 . 'id="textSQLDUMP" wrap="OFF">';
521 * Export at the server level
523 * @param string|mixed[] $dbSelect the selected databases to export
524 * @param string $whatStrucOrData structure or data or both
525 * @param ExportPlugin $exportPlugin the selected export plugin
526 * @param string $errorUrl the URL in case of error
527 * @param string $exportType the export type
528 * @param bool $doRelation whether to export relation info
529 * @param bool $doComments whether to add comments
530 * @param bool $doMime whether to add MIME info
531 * @param bool $doDates whether to add dates
532 * @param mixed[] $aliases alias information for db/table/column
533 * @param string $separateFiles whether it is a separate-files export
535 public function exportServer(
536 string|array $dbSelect,
537 string $whatStrucOrData,
538 ExportPlugin $exportPlugin,
539 string $errorUrl,
540 string $exportType,
541 bool $doRelation,
542 bool $doComments,
543 bool $doMime,
544 bool $doDates,
545 array $aliases,
546 string $separateFiles,
547 ): void {
548 if (is_array($dbSelect) && $dbSelect !== []) {
549 $tmpSelect = implode('|', $dbSelect);
550 $tmpSelect = '|' . $tmpSelect . '|';
553 // Walk over databases
554 foreach ($this->dbi->getDatabaseList() as $currentDb) {
555 if (! isset($tmpSelect) || ! str_contains(' ' . $tmpSelect, '|' . $currentDb . '|')) {
556 continue;
559 $tables = $this->dbi->getTables($currentDb);
560 $this->exportDatabase(
561 DatabaseName::from($currentDb),
562 $tables,
563 $whatStrucOrData,
564 $tables,
565 $tables,
566 $exportPlugin,
567 $errorUrl,
568 $exportType,
569 $doRelation,
570 $doComments,
571 $doMime,
572 $doDates,
573 $aliases,
574 $separateFiles === 'database' ? $separateFiles : '',
576 if ($separateFiles !== 'server') {
577 continue;
580 $this->saveObjectInBuffer($currentDb);
585 * Export at the database level
587 * @param DatabaseName $db the database to export
588 * @param string[] $tables the tables to export
589 * @param string $whatStrucOrData structure or data or both
590 * @param string[] $tableStructure whether to export structure for each table
591 * @param string[] $tableData whether to export data for each table
592 * @param ExportPlugin $exportPlugin the selected export plugin
593 * @param string $errorUrl the URL in case of error
594 * @param string $exportType the export type
595 * @param bool $doRelation whether to export relation info
596 * @param bool $doComments whether to add comments
597 * @param bool $doMime whether to add MIME info
598 * @param bool $doDates whether to add dates
599 * @param mixed[] $aliases Alias information for db/table/column
600 * @param string $separateFiles whether it is a separate-files export
602 public function exportDatabase(
603 DatabaseName $db,
604 array $tables,
605 string $whatStrucOrData,
606 array $tableStructure,
607 array $tableData,
608 ExportPlugin $exportPlugin,
609 string $errorUrl,
610 string $exportType,
611 bool $doRelation,
612 bool $doComments,
613 bool $doMime,
614 bool $doDates,
615 array $aliases,
616 string $separateFiles,
617 ): void {
618 $dbAlias = ! empty($aliases[$db->getName()]['alias'])
619 ? $aliases[$db->getName()]['alias'] : '';
621 if (! $exportPlugin->exportDBHeader($db->getName(), $dbAlias)) {
622 return;
625 if (! $exportPlugin->exportDBCreate($db->getName(), $exportType, $dbAlias)) {
626 return;
629 if ($separateFiles === 'database') {
630 $this->saveObjectInBuffer('database', true);
633 if (
634 ($GLOBALS['sql_structure_or_data'] === 'structure'
635 || $GLOBALS['sql_structure_or_data'] === 'structure_and_data')
636 && isset($GLOBALS['sql_procedure_function'])
638 $exportPlugin->exportRoutines($db->getName(), $aliases);
640 if ($separateFiles === 'database') {
641 $this->saveObjectInBuffer('routines');
645 $views = [];
647 if ($tables !== []) {
648 // Prefetch table information to improve performance.
649 // Table status will get saved in Query Cache,
650 // and all instantiations of Table below should be much faster.
651 $this->dbi->getTablesFull($db->getName(), $tables);
654 foreach ($tables as $table) {
655 $tableObject = new Table($table, $db->getName(), $this->dbi);
656 // if this is a view, collect it for later;
657 // views must be exported after the tables
658 $isView = $tableObject->isView();
659 if ($isView) {
660 $views[] = $table;
663 if (
664 ($whatStrucOrData === 'structure'
665 || $whatStrucOrData === 'structure_and_data')
666 && in_array($table, $tableStructure, true)
668 // for a view, export a stand-in definition of the table
669 // to resolve view dependencies (only when it's a single-file export)
670 if ($isView) {
671 if (
672 $separateFiles === ''
673 && isset($GLOBALS['sql_create_view'])
674 && ! $exportPlugin->exportStructure(
675 $db->getName(),
676 $table,
677 'stand_in',
678 $exportType,
679 $doRelation,
680 $doComments,
681 $doMime,
682 $doDates,
683 $aliases,
686 break;
688 } elseif (isset($GLOBALS['sql_create_table'])) {
689 $tableSize = $GLOBALS['maxsize'];
690 // Checking if the maximum table size constrain has been set
691 // And if that constrain is a valid number or not
692 if ($tableSize !== '' && is_numeric($tableSize)) {
693 // This obtains the current table's size
694 $query = 'SELECT data_length + index_length
695 from information_schema.TABLES
696 WHERE table_schema = ' . $this->dbi->quoteString($db->getName()) . '
697 AND table_name = ' . $this->dbi->quoteString($table);
699 $size = (int) $this->dbi->fetchValue($query);
700 //Converting the size to MB
701 $size /= 1024 * 1024;
702 if ($size > $tableSize) {
703 continue;
707 if (
708 ! $exportPlugin->exportStructure(
709 $db->getName(),
710 $table,
711 'create_table',
712 $exportType,
713 $doRelation,
714 $doComments,
715 $doMime,
716 $doDates,
717 $aliases,
720 break;
725 // if this is a view or a merge table, don't export data
726 if (
727 ($whatStrucOrData === 'data' || $whatStrucOrData === 'structure_and_data')
728 && in_array($table, $tableData, true)
729 && ! $isView
731 $tableObj = new Table($table, $db->getName(), $this->dbi);
732 $nonGeneratedCols = $tableObj->getNonGeneratedColumns();
734 $localQuery = 'SELECT ' . implode(', ', $nonGeneratedCols)
735 . ' FROM ' . Util::backquote($db->getName())
736 . '.' . Util::backquote($table);
738 if (! $exportPlugin->exportData($db->getName(), $table, $errorUrl, $localQuery, $aliases)) {
739 break;
743 // this buffer was filled, we save it and go to the next one
744 if ($separateFiles === 'database') {
745 $this->saveObjectInBuffer('table_' . $table);
748 // now export the triggers (needs to be done after the data because
749 // triggers can modify already imported tables)
750 if (
751 ! isset($GLOBALS['sql_create_trigger']) || ($whatStrucOrData !== 'structure'
752 && $whatStrucOrData !== 'structure_and_data')
753 || ! in_array($table, $tableStructure, true)
755 continue;
758 if (
759 ! $exportPlugin->exportStructure(
760 $db->getName(),
761 $table,
762 'triggers',
763 $exportType,
764 $doRelation,
765 $doComments,
766 $doMime,
767 $doDates,
768 $aliases,
771 break;
774 if ($separateFiles !== 'database') {
775 continue;
778 $this->saveObjectInBuffer('table_' . $table, true);
781 if (isset($GLOBALS['sql_create_view'])) {
782 foreach ($views as $view) {
783 // no data export for a view
784 if ($whatStrucOrData !== 'structure' && $whatStrucOrData !== 'structure_and_data') {
785 continue;
788 if (
789 ! $exportPlugin->exportStructure(
790 $db->getName(),
791 $view,
792 'create_view',
793 $exportType,
794 $doRelation,
795 $doComments,
796 $doMime,
797 $doDates,
798 $aliases,
801 break;
804 if ($separateFiles !== 'database') {
805 continue;
808 $this->saveObjectInBuffer('view_' . $view);
812 if (! $exportPlugin->exportDBFooter($db->getName())) {
813 return;
816 // export metadata related to this db
817 if (isset($GLOBALS['sql_metadata'])) {
818 // Types of metadata to export.
819 // In the future these can be allowed to be selected by the user
820 $metadataTypes = $this->getMetadataTypes();
821 $exportPlugin->exportMetadata($db->getName(), $tables, $metadataTypes);
823 if ($separateFiles === 'database') {
824 $this->saveObjectInBuffer('metadata');
828 if ($separateFiles === 'database') {
829 $this->saveObjectInBuffer('extra');
832 if (
833 ($GLOBALS['sql_structure_or_data'] !== 'structure'
834 && $GLOBALS['sql_structure_or_data'] !== 'structure_and_data')
835 || ! isset($GLOBALS['sql_procedure_function'])
837 return;
840 $exportPlugin->exportEvents($db->getName());
842 if ($separateFiles !== 'database') {
843 return;
846 $this->saveObjectInBuffer('events');
850 * Export a raw query
852 * @param string $whatStrucOrData whether to export structure for each table or raw
853 * @param ExportPlugin $exportPlugin the selected export plugin
854 * @param string $errorUrl the URL in case of error
855 * @param string|null $db the database where the query is executed
856 * @param string $sqlQuery the query to be executed
858 public static function exportRaw(
859 string $whatStrucOrData,
860 ExportPlugin $exportPlugin,
861 string $errorUrl,
862 string|null $db,
863 string $sqlQuery,
864 ): void {
865 // In case the we need to dump just the raw query
866 if ($whatStrucOrData !== 'raw') {
867 return;
870 if ($exportPlugin->exportRawQuery($errorUrl, $db, $sqlQuery)) {
871 return;
874 $GLOBALS['message'] = Message::error(
875 // phpcs:disable Generic.Files.LineLength.TooLong
876 /* l10n: A query written by the user is a "raw query" that could be using no tables or databases in particular */
877 __('Exporting a raw query is not supported for this export method.'),
882 * Export at the table level
884 * @param string $db the database to export
885 * @param string $table the table to export
886 * @param string $whatStrucOrData structure or data or both
887 * @param ExportPlugin $exportPlugin the selected export plugin
888 * @param string $errorUrl the URL in case of error
889 * @param string $exportType the export type
890 * @param bool $doRelation whether to export relation info
891 * @param bool $doComments whether to add comments
892 * @param bool $doMime whether to add MIME info
893 * @param bool $doDates whether to add dates
894 * @param string|null $allrows whether "dump all rows" was ticked
895 * @param string $limitTo upper limit
896 * @param string $limitFrom starting limit
897 * @param string $sqlQuery query for which exporting is requested
898 * @param mixed[] $aliases Alias information for db/table/column
900 public function exportTable(
901 string $db,
902 string $table,
903 string $whatStrucOrData,
904 ExportPlugin $exportPlugin,
905 string $errorUrl,
906 string $exportType,
907 bool $doRelation,
908 bool $doComments,
909 bool $doMime,
910 bool $doDates,
911 string|null $allrows,
912 string $limitTo,
913 string $limitFrom,
914 string $sqlQuery,
915 array $aliases,
916 ): void {
917 $dbAlias = ! empty($aliases[$db]['alias'])
918 ? $aliases[$db]['alias'] : '';
919 if (! $exportPlugin->exportDBHeader($db, $dbAlias)) {
920 return;
923 if ($allrows === '0' && $limitTo > 0 && $limitFrom >= 0) {
924 $addQuery = ' LIMIT '
925 . ($limitFrom > 0 ? $limitFrom . ', ' : '')
926 . $limitTo;
927 } else {
928 $addQuery = '';
931 $tableObject = new Table($table, $db, $this->dbi);
932 $isView = $tableObject->isView();
933 if ($whatStrucOrData === 'structure' || $whatStrucOrData === 'structure_and_data') {
934 if ($isView) {
935 if (isset($GLOBALS['sql_create_view'])) {
936 if (
937 ! $exportPlugin->exportStructure(
938 $db,
939 $table,
940 'create_view',
941 $exportType,
942 $doRelation,
943 $doComments,
944 $doMime,
945 $doDates,
946 $aliases,
949 return;
952 } elseif (isset($GLOBALS['sql_create_table'])) {
953 if (
954 ! $exportPlugin->exportStructure(
955 $db,
956 $table,
957 'create_table',
958 $exportType,
959 $doRelation,
960 $doComments,
961 $doMime,
962 $doDates,
963 $aliases,
966 return;
971 // If this is an export of a single view, we have to export data;
972 // for example, a PDF report
973 // if it is a merge table, no data is exported
974 if ($whatStrucOrData === 'data' || $whatStrucOrData === 'structure_and_data') {
975 if ($sqlQuery !== '') {
976 // only preg_replace if needed
977 if ($addQuery !== '') {
978 // remove trailing semicolon before adding a LIMIT
979 $sqlQuery = preg_replace('%;\s*$%', '', $sqlQuery);
982 $localQuery = $sqlQuery . $addQuery;
983 $this->dbi->selectDb($db);
984 } else {
985 // Data is exported only for Non-generated columns
986 $tableObj = new Table($table, $db, $this->dbi);
987 $nonGeneratedCols = $tableObj->getNonGeneratedColumns();
989 $localQuery = 'SELECT ' . implode(', ', $nonGeneratedCols)
990 . ' FROM ' . Util::backquote($db)
991 . '.' . Util::backquote($table) . $addQuery;
994 if (! $exportPlugin->exportData($db, $table, $errorUrl, $localQuery, $aliases)) {
995 return;
999 // now export the triggers (needs to be done after the data because
1000 // triggers can modify already imported tables)
1001 if (
1002 isset($GLOBALS['sql_create_trigger']) && ($whatStrucOrData === 'structure'
1003 || $whatStrucOrData === 'structure_and_data')
1005 if (
1006 ! $exportPlugin->exportStructure(
1007 $db,
1008 $table,
1009 'triggers',
1010 $exportType,
1011 $doRelation,
1012 $doComments,
1013 $doMime,
1014 $doDates,
1015 $aliases,
1018 return;
1022 if (! $exportPlugin->exportDBFooter($db)) {
1023 return;
1026 if (! isset($GLOBALS['sql_metadata'])) {
1027 return;
1030 // Types of metadata to export.
1031 // In the future these can be allowed to be selected by the user
1032 $metadataTypes = $this->getMetadataTypes();
1033 $exportPlugin->exportMetadata($db, $table, $metadataTypes);
1037 * Loads correct page after doing export
1039 * @psalm-return non-empty-string
1041 public function getPageLocationAndSaveMessage(string $exportType, Message $message): string
1043 (new FlashMessages())->addMessage($message->isError() ? 'danger' : 'success', $message->getMessage());
1045 if ($exportType === 'server') {
1046 return 'index.php?route=/server/export' . Url::getCommonRaw([], '&');
1049 if ($exportType === 'database') {
1050 $params = ['db' => Current::$database];
1052 return 'index.php?route=/database/export' . Url::getCommonRaw($params, '&');
1055 $params = ['db' => Current::$database, 'table' => Current::$table, 'single_table' => 'true'];
1057 return 'index.php?route=/table/export' . Url::getCommonRaw($params, '&');
1061 * Merge two alias arrays, if array1 and array2 have
1062 * conflicting alias then array2 value is used if it
1063 * is non empty otherwise array1 value.
1065 * @param mixed[] $aliases1 first array of aliases
1066 * @param mixed[] $aliases2 second array of aliases
1068 * @return mixed[] resultant merged aliases info
1070 public function mergeAliases(array $aliases1, array $aliases2): array
1072 // First do a recursive array merge
1073 // on aliases arrays.
1074 $aliases = array_merge_recursive($aliases1, $aliases2);
1075 // Now, resolve conflicts in aliases, if any
1076 foreach ($aliases as $dbName => $db) {
1077 // If alias key is an array then
1078 // it is a merge conflict.
1079 if (isset($db['alias']) && is_array($db['alias'])) {
1080 $val1 = $db['alias'][0];
1081 $val2 = $db['alias'][1];
1082 // Use aliases2 alias if non empty
1083 $aliases[$dbName]['alias'] = $val2 !== '' && $val2 !== null ? $val2 : $val1;
1086 if (! isset($db['tables'])) {
1087 continue;
1090 foreach ($db['tables'] as $tableName => $tbl) {
1091 if (isset($tbl['alias']) && is_array($tbl['alias'])) {
1092 $val1 = $tbl['alias'][0];
1093 $val2 = $tbl['alias'][1];
1094 // Use aliases2 alias if non empty
1095 $aliases[$dbName]['tables'][$tableName]['alias'] = $val2 !== '' && $val2 !== null ? $val2 : $val1;
1098 if (! isset($tbl['columns'])) {
1099 continue;
1102 foreach ($tbl['columns'] as $col => $colAs) {
1103 if (! isset($colAs) || ! is_array($colAs)) {
1104 continue;
1107 $val1 = $colAs[0];
1108 $val2 = $colAs[1];
1109 // Use aliases2 alias if non empty
1110 $aliases[$dbName]['tables'][$tableName]['columns'][$col] = $val2 !== '' && $val2 !== null ? $val2 : $val1;
1115 return $aliases;
1119 * Locks tables
1121 * @param DatabaseName $db database name
1122 * @param mixed[] $tables list of table names
1123 * @param string $lockType lock type; "[LOW_PRIORITY] WRITE" or "READ [LOCAL]"
1125 public function lockTables(DatabaseName $db, array $tables, string $lockType = 'WRITE'): void
1127 $locks = [];
1128 foreach ($tables as $table) {
1129 $locks[] = Util::backquote($db->getName()) . '.'
1130 . Util::backquote($table) . ' ' . $lockType;
1133 $sql = 'LOCK TABLES ' . implode(', ', $locks);
1135 $this->dbi->tryQuery($sql);
1139 * Releases table locks
1141 public function unlockTables(): void
1143 $this->dbi->tryQuery('UNLOCK TABLES');
1147 * Returns all the metadata types that can be exported with a database or a table
1149 * @return string[] metadata types.
1151 public function getMetadataTypes(): array
1153 return [
1154 'column_info',
1155 'table_uiprefs',
1156 'tracking',
1157 'bookmark',
1158 'relation',
1159 'table_coords',
1160 'pdf_pages',
1161 'savedsearches',
1162 'central_columns',
1163 'export_templates',
1168 * Returns the checked clause, depending on the presence of key in array
1170 * @param string $key the key to look for
1171 * @param mixed[] $array array to verify
1173 * @return string the checked clause
1175 public function getCheckedClause(string $key, array $array): string
1177 if (in_array($key, $array)) {
1178 return ' checked="checked"';
1181 return '';
1185 * get all the export options and verify
1186 * call and include the appropriate Schema Class depending on $export_type
1188 * @param non-empty-string $exportType
1190 * @return array{fileName: non-empty-string, mediaType: non-empty-string, fileData: string}
1192 * @throws ExportException
1194 public function getExportSchemaInfo(DatabaseName $db, string $exportType): array
1197 * default is PDF, otherwise validate it's only letters a-z
1199 if (! preg_match('/^[a-zA-Z]+$/', $exportType)) {
1200 $exportType = 'pdf';
1203 // get the specific plugin
1204 /** @var SchemaPlugin|null $exportPlugin */
1205 $exportPlugin = Plugins::getPlugin('schema', $exportType);
1207 // Check schema export type
1208 if ($exportPlugin === null) {
1209 throw new ExportException(__('Bad type!'));
1212 $this->dbi->selectDb($db);
1214 return $exportPlugin->getExportInfo($db);
1217 /** @return string[] */
1218 public function getTableNames(string $database): array
1220 return $this->dbi->getTables($database);
1223 private function getHTMLForRefreshButton(string $exportType): string
1225 $postParams = $this->getPostParams($exportType);
1227 $refreshButton = '<form id="export_refresh_form" method="POST" action="'
1228 . Url::getFromRoute('/export') . '" class="disableAjax">';
1229 $refreshButton .= '[ <a class="disableAjax export_refresh_btn">' . __('Refresh') . '</a> ]';
1230 foreach ($postParams as $name => $value) {
1231 if (is_array($value)) {
1232 foreach ($value as $val) {
1233 $refreshButton .= '<input type="hidden" name="' . htmlentities((string) $name)
1234 . '[]" value="' . htmlentities((string) $val) . '">';
1236 } else {
1237 $refreshButton .= '<input type="hidden" name="' . htmlentities((string) $name)
1238 . '" value="' . htmlentities((string) $value) . '">';
1242 return $refreshButton . '</form>';
1245 private function getHTMLForBackButton(string $exportType, string $db, string $table): string
1247 $backButton = '<p>[ <a href="';
1248 $backButton .= match ($exportType) {
1249 'server' => Url::getFromRoute('/server/export') . '" data-post="' . Url::getCommon([], '', false),
1250 'database' => Url::getFromRoute('/database/export') . '" data-post="' . Url::getCommon(
1251 ['db' => $db],
1253 false,
1255 default => Url::getFromRoute('/table/export') . '" data-post="' . Url::getCommon(
1256 ['db' => $db, 'table' => $table],
1258 false,
1262 $postParams = array_filter($this->getPostParams($exportType), static fn ($value): bool => ! is_array($value));
1263 $backButton .= '&amp;' . http_build_query($postParams);
1265 $backButton .= '&amp;repopulate=1">' . __('Back') . '</a> ]</p>';
1267 return $backButton;
1270 /** @return mixed[] */
1271 private function getPostParams(string $exportType): array
1273 $postParams = $_POST;
1275 // Convert the multiple select elements from an array to a string
1276 if ($exportType === 'database') {
1277 $structOrDataForced = empty($postParams['structure_or_data_forced']);
1278 if ($structOrDataForced && ! isset($postParams['table_structure'])) {
1279 $postParams['table_structure'] = [];
1282 if ($structOrDataForced && ! isset($postParams['table_data'])) {
1283 $postParams['table_data'] = [];
1287 return $postParams;