PageRenderTime 55ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/libraries/classes/Export.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 1342 lines | 899 code | 136 blank | 307 comment | 171 complexity | c817e1f55d799279d2d889f6badff0a2 MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0
  1. <?php
  2. /**
  3. * function for the main export logic
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin;
  7. use PhpMyAdmin\Controllers\Database\ExportController as DatabaseExportController;
  8. use PhpMyAdmin\Controllers\Server\ExportController as ServerExportController;
  9. use PhpMyAdmin\Controllers\Table\ExportController as TableExportController;
  10. use PhpMyAdmin\Plugins\ExportPlugin;
  11. use PhpMyAdmin\Plugins\SchemaPlugin;
  12. use function __;
  13. use function array_merge_recursive;
  14. use function error_get_last;
  15. use function fclose;
  16. use function file_exists;
  17. use function fopen;
  18. use function function_exists;
  19. use function fwrite;
  20. use function gzencode;
  21. use function header;
  22. use function htmlentities;
  23. use function htmlspecialchars;
  24. use function implode;
  25. use function in_array;
  26. use function ini_get;
  27. use function is_array;
  28. use function is_file;
  29. use function is_numeric;
  30. use function is_object;
  31. use function is_string;
  32. use function is_writable;
  33. use function mb_strlen;
  34. use function mb_strpos;
  35. use function mb_strtolower;
  36. use function mb_substr;
  37. use function ob_list_handlers;
  38. use function preg_match;
  39. use function preg_replace;
  40. use function strlen;
  41. use function strtolower;
  42. use function substr;
  43. use function time;
  44. use function trim;
  45. use function urlencode;
  46. /**
  47. * PhpMyAdmin\Export class
  48. */
  49. class Export
  50. {
  51. /** @var DatabaseInterface */
  52. private $dbi;
  53. /** @var mixed */
  54. public $dumpBuffer = '';
  55. /** @var int */
  56. public $dumpBufferLength = 0;
  57. /** @var array */
  58. public $dumpBufferObjects = [];
  59. /**
  60. * @param DatabaseInterface $dbi DatabaseInterface instance
  61. */
  62. public function __construct($dbi)
  63. {
  64. $this->dbi = $dbi;
  65. }
  66. /**
  67. * Sets a session variable upon a possible fatal error during export
  68. */
  69. public function shutdown(): void
  70. {
  71. $error = error_get_last();
  72. if ($error == null || ! mb_strpos($error['message'], 'execution time')) {
  73. return;
  74. }
  75. //set session variable to check if there was error while exporting
  76. $_SESSION['pma_export_error'] = $error['message'];
  77. }
  78. /**
  79. * Detect ob_gzhandler
  80. */
  81. public function isGzHandlerEnabled(): bool
  82. {
  83. /** @var string[] $handlers */
  84. $handlers = ob_list_handlers();
  85. return in_array('ob_gzhandler', $handlers);
  86. }
  87. /**
  88. * Detect whether gzencode is needed; it might not be needed if
  89. * the server is already compressing by itself
  90. */
  91. public function gzencodeNeeded(): bool
  92. {
  93. /*
  94. * We should gzencode only if the function exists
  95. * but we don't want to compress twice, therefore
  96. * gzencode only if transparent compression is not enabled
  97. * and gz compression was not asked via $cfg['OBGzip']
  98. * but transparent compression does not apply when saving to server
  99. */
  100. $chromeAndGreaterThan43 = $GLOBALS['config']->get('PMA_USR_BROWSER_AGENT') == 'CHROME'
  101. && $GLOBALS['config']->get('PMA_USR_BROWSER_VER') >= 43; // see bug #4942
  102. return function_exists('gzencode')
  103. && ((! ini_get('zlib.output_compression')
  104. && ! $this->isGzHandlerEnabled())
  105. || $GLOBALS['save_on_server']
  106. || $chromeAndGreaterThan43);
  107. }
  108. /**
  109. * Output handler for all exports, if needed buffering, it stores data into
  110. * $this->dumpBuffer, otherwise it prints them out.
  111. *
  112. * @param string $line the insert statement
  113. */
  114. public function outputHandler(?string $line): bool
  115. {
  116. global $time_start, $save_filename;
  117. // Kanji encoding convert feature
  118. if ($GLOBALS['output_kanji_conversion']) {
  119. $line = Encoding::kanjiStrConv($line, $GLOBALS['knjenc'], $GLOBALS['xkana'] ?? '');
  120. }
  121. // If we have to buffer data, we will perform everything at once at the end
  122. if ($GLOBALS['buffer_needed']) {
  123. $this->dumpBuffer .= $line;
  124. if ($GLOBALS['onfly_compression']) {
  125. $this->dumpBufferLength += strlen((string) $line);
  126. if ($this->dumpBufferLength > $GLOBALS['memory_limit']) {
  127. if ($GLOBALS['output_charset_conversion']) {
  128. $this->dumpBuffer = Encoding::convertString('utf-8', $GLOBALS['charset'], $this->dumpBuffer);
  129. }
  130. if ($GLOBALS['compression'] === 'gzip' && $this->gzencodeNeeded()) {
  131. // as a gzipped file
  132. // without the optional parameter level because it bugs
  133. $this->dumpBuffer = gzencode($this->dumpBuffer);
  134. }
  135. if ($GLOBALS['save_on_server']) {
  136. $writeResult = @fwrite($GLOBALS['file_handle'], (string) $this->dumpBuffer);
  137. // Here, use strlen rather than mb_strlen to get the length
  138. // in bytes to compare against the number of bytes written.
  139. if ($writeResult != strlen((string) $this->dumpBuffer)) {
  140. $GLOBALS['message'] = Message::error(
  141. __('Insufficient space to save the file %s.')
  142. );
  143. $GLOBALS['message']->addParam($save_filename);
  144. return false;
  145. }
  146. } else {
  147. echo $this->dumpBuffer;
  148. }
  149. $this->dumpBuffer = '';
  150. $this->dumpBufferLength = 0;
  151. }
  152. } else {
  153. $timeNow = time();
  154. if ($time_start >= $timeNow + 30) {
  155. $time_start = $timeNow;
  156. header('X-pmaPing: Pong');
  157. }
  158. }
  159. } elseif ($GLOBALS['asfile']) {
  160. if ($GLOBALS['output_charset_conversion']) {
  161. $line = Encoding::convertString('utf-8', $GLOBALS['charset'], $line);
  162. }
  163. if ($GLOBALS['save_on_server'] && mb_strlen((string) $line) > 0) {
  164. if ($GLOBALS['file_handle'] !== null) {
  165. $writeResult = @fwrite($GLOBALS['file_handle'], (string) $line);
  166. } else {
  167. $writeResult = false;
  168. }
  169. // Here, use strlen rather than mb_strlen to get the length
  170. // in bytes to compare against the number of bytes written.
  171. if (! $writeResult || $writeResult != strlen((string) $line)) {
  172. $GLOBALS['message'] = Message::error(
  173. __('Insufficient space to save the file %s.')
  174. );
  175. $GLOBALS['message']->addParam($save_filename);
  176. return false;
  177. }
  178. $timeNow = time();
  179. if ($time_start >= $timeNow + 30) {
  180. $time_start = $timeNow;
  181. header('X-pmaPing: Pong');
  182. }
  183. } else {
  184. // We export as file - output normally
  185. echo $line;
  186. }
  187. } else {
  188. // We export as html - replace special chars
  189. echo htmlspecialchars((string) $line);
  190. }
  191. return true;
  192. }
  193. /**
  194. * Returns HTML containing the footer for a displayed export
  195. *
  196. * @param string $backButton the link for going Back
  197. * @param string $refreshButton the link for refreshing page
  198. *
  199. * @return string the HTML output
  200. */
  201. public function getHtmlForDisplayedExportFooter(
  202. string $backButton,
  203. string $refreshButton
  204. ): string {
  205. /**
  206. * Close the html tags and add the footers for on-screen export
  207. */
  208. return '</textarea>'
  209. . ' </form>'
  210. . '<br>'
  211. // bottom back button
  212. . $backButton
  213. . $refreshButton
  214. . '</div>'
  215. . '<script type="text/javascript">' . "\n"
  216. . '//<![CDATA[' . "\n"
  217. . 'var $body = $("body");' . "\n"
  218. . '$("#textSQLDUMP")' . "\n"
  219. . '.width($body.width() - 50)' . "\n"
  220. . '.height($body.height() - 100);' . "\n"
  221. . '//]]>' . "\n"
  222. . '</script>' . "\n";
  223. }
  224. /**
  225. * Computes the memory limit for export
  226. *
  227. * @return int the memory limit
  228. */
  229. public function getMemoryLimit(): int
  230. {
  231. $memoryLimit = trim((string) ini_get('memory_limit'));
  232. $memoryLimitNumber = (int) substr($memoryLimit, 0, -1);
  233. $lowerLastChar = strtolower(substr($memoryLimit, -1));
  234. // 2 MB as default
  235. if (empty($memoryLimit) || $memoryLimit == '-1') {
  236. $memoryLimit = 2 * 1024 * 1024;
  237. } elseif ($lowerLastChar === 'm') {
  238. $memoryLimit = $memoryLimitNumber * 1024 * 1024;
  239. } elseif ($lowerLastChar === 'k') {
  240. $memoryLimit = $memoryLimitNumber * 1024;
  241. } elseif ($lowerLastChar === 'g') {
  242. $memoryLimit = $memoryLimitNumber * 1024 * 1024 * 1024;
  243. } else {
  244. $memoryLimit = (int) $memoryLimit;
  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 ($memoryLimit > 1500000) {
  250. $memoryLimit -= 1500000;
  251. }
  252. // Some memory is needed for compression, assume 1/3
  253. $memoryLimit /= 8;
  254. return $memoryLimit;
  255. }
  256. /**
  257. * Returns the filename and MIME type for a compression and an export plugin
  258. *
  259. * @param ExportPlugin $exportPlugin the export plugin
  260. * @param string $compression compression asked
  261. * @param string $filename the filename
  262. *
  263. * @return string[] the filename and mime type
  264. */
  265. public function getFinalFilenameAndMimetypeForFilename(
  266. ExportPlugin $exportPlugin,
  267. string $compression,
  268. string $filename
  269. ): array {
  270. // Grab basic dump extension and mime type
  271. // Check if the user already added extension;
  272. // get the substring where the extension would be if it was included
  273. $requiredExtension = '.' . $exportPlugin->getProperties()->getExtension();
  274. $extensionLength = mb_strlen($requiredExtension);
  275. $userExtension = mb_substr($filename, -$extensionLength);
  276. if (mb_strtolower($userExtension) != $requiredExtension) {
  277. $filename .= $requiredExtension;
  278. }
  279. $mediaType = $exportPlugin->getProperties()->getMimeType();
  280. // If dump is going to be compressed, set correct mime_type and add
  281. // compression to extension
  282. if ($compression === 'gzip') {
  283. $filename .= '.gz';
  284. $mediaType = 'application/x-gzip';
  285. } elseif ($compression === 'zip') {
  286. $filename .= '.zip';
  287. $mediaType = 'application/zip';
  288. }
  289. return [
  290. $filename,
  291. $mediaType,
  292. ];
  293. }
  294. /**
  295. * Return the filename and MIME type for export file
  296. *
  297. * @param string $exportType type of export
  298. * @param string $rememberTemplate whether to remember template
  299. * @param ExportPlugin $exportPlugin the export plugin
  300. * @param string $compression compression asked
  301. * @param string $filenameTemplate the filename template
  302. *
  303. * @return string[] the filename template and mime type
  304. */
  305. public function getFilenameAndMimetype(
  306. string $exportType,
  307. string $rememberTemplate,
  308. ExportPlugin $exportPlugin,
  309. string $compression,
  310. string $filenameTemplate
  311. ): array {
  312. if ($exportType === 'server') {
  313. if (! empty($rememberTemplate)) {
  314. $GLOBALS['config']->setUserValue(
  315. 'pma_server_filename_template',
  316. 'Export/file_template_server',
  317. $filenameTemplate
  318. );
  319. }
  320. } elseif ($exportType === 'database') {
  321. if (! empty($rememberTemplate)) {
  322. $GLOBALS['config']->setUserValue(
  323. 'pma_db_filename_template',
  324. 'Export/file_template_database',
  325. $filenameTemplate
  326. );
  327. }
  328. } elseif ($exportType === 'raw') {
  329. if (! empty($rememberTemplate)) {
  330. $GLOBALS['config']->setUserValue(
  331. 'pma_raw_filename_template',
  332. 'Export/file_template_raw',
  333. $filenameTemplate
  334. );
  335. }
  336. } else {
  337. if (! empty($rememberTemplate)) {
  338. $GLOBALS['config']->setUserValue(
  339. 'pma_table_filename_template',
  340. 'Export/file_template_table',
  341. $filenameTemplate
  342. );
  343. }
  344. }
  345. $filename = Util::expandUserString($filenameTemplate);
  346. // remove dots in filename (coming from either the template or already
  347. // part of the filename) to avoid a remote code execution vulnerability
  348. $filename = Sanitize::sanitizeFilename($filename, true);
  349. return $this->getFinalFilenameAndMimetypeForFilename($exportPlugin, $compression, $filename);
  350. }
  351. /**
  352. * Open the export file
  353. *
  354. * @param string $filename the export filename
  355. * @param bool $quickExport whether it's a quick export or not
  356. *
  357. * @return array the full save filename, possible message and the file handle
  358. */
  359. public function openFile(string $filename, bool $quickExport): array
  360. {
  361. $fileHandle = null;
  362. $message = '';
  363. $doNotSaveItOver = true;
  364. if (isset($_POST['quick_export_onserver_overwrite'])) {
  365. $doNotSaveItOver = $_POST['quick_export_onserver_overwrite'] !== 'saveitover';
  366. }
  367. $saveFilename = Util::userDir((string) ($GLOBALS['cfg']['SaveDir'] ?? ''))
  368. . preg_replace('@[/\\\\]@', '_', $filename);
  369. if (
  370. @file_exists($saveFilename)
  371. && ((! $quickExport && empty($_POST['onserver_overwrite']))
  372. || ($quickExport
  373. && $doNotSaveItOver))
  374. ) {
  375. $message = Message::error(
  376. __(
  377. 'File %s already exists on server, change filename or check overwrite option.'
  378. )
  379. );
  380. $message->addParam($saveFilename);
  381. } elseif (@is_file($saveFilename) && ! @is_writable($saveFilename)) {
  382. $message = Message::error(
  383. __(
  384. 'The web server does not have permission to save the file %s.'
  385. )
  386. );
  387. $message->addParam($saveFilename);
  388. } else {
  389. $fileHandle = @fopen($saveFilename, 'w');
  390. if ($fileHandle === false) {
  391. $message = Message::error(
  392. __(
  393. 'The web server does not have permission to save the file %s.'
  394. )
  395. );
  396. $message->addParam($saveFilename);
  397. }
  398. }
  399. return [
  400. $saveFilename,
  401. $message,
  402. $fileHandle,
  403. ];
  404. }
  405. /**
  406. * Close the export file
  407. *
  408. * @param resource $fileHandle the export file handle
  409. * @param string $dumpBuffer the current dump buffer
  410. * @param string $saveFilename the export filename
  411. *
  412. * @return Message a message object (or empty string)
  413. */
  414. public function closeFile(
  415. $fileHandle,
  416. string $dumpBuffer,
  417. string $saveFilename
  418. ): Message {
  419. $writeResult = @fwrite($fileHandle, $dumpBuffer);
  420. fclose($fileHandle);
  421. // Here, use strlen rather than mb_strlen to get the length
  422. // in bytes to compare against the number of bytes written.
  423. if (strlen($dumpBuffer) > 0 && (! $writeResult || $writeResult != strlen($dumpBuffer))) {
  424. $message = new Message(
  425. __('Insufficient space to save the file %s.'),
  426. Message::ERROR,
  427. [$saveFilename]
  428. );
  429. } else {
  430. $message = new Message(
  431. __('Dump has been saved to file %s.'),
  432. Message::SUCCESS,
  433. [$saveFilename]
  434. );
  435. }
  436. return $message;
  437. }
  438. /**
  439. * Compress the export buffer
  440. *
  441. * @param array|string $dumpBuffer the current dump buffer
  442. * @param string $compression the compression mode
  443. * @param string $filename the filename
  444. *
  445. * @return array|string|bool
  446. */
  447. public function compress($dumpBuffer, string $compression, string $filename)
  448. {
  449. if ($compression === 'zip' && function_exists('gzcompress')) {
  450. $zipExtension = new ZipExtension();
  451. $filename = substr($filename, 0, -4); // remove extension (.zip)
  452. $dumpBuffer = $zipExtension->createFile($dumpBuffer, $filename);
  453. } elseif ($compression === 'gzip' && $this->gzencodeNeeded() && is_string($dumpBuffer)) {
  454. // without the optional parameter level because it bugs
  455. $dumpBuffer = gzencode($dumpBuffer);
  456. }
  457. return $dumpBuffer;
  458. }
  459. /**
  460. * Saves the dump buffer for a particular table in an array
  461. * Used in separate files export
  462. *
  463. * @param string $objectName the name of current object to be stored
  464. * @param bool $append optional boolean to append to an existing index or not
  465. */
  466. public function saveObjectInBuffer(string $objectName, bool $append = false): void
  467. {
  468. if (! empty($this->dumpBuffer)) {
  469. if ($append && isset($this->dumpBufferObjects[$objectName])) {
  470. $this->dumpBufferObjects[$objectName] .= $this->dumpBuffer;
  471. } else {
  472. $this->dumpBufferObjects[$objectName] = $this->dumpBuffer;
  473. }
  474. }
  475. // Re - initialize
  476. $this->dumpBuffer = '';
  477. $this->dumpBufferLength = 0;
  478. }
  479. /**
  480. * Returns HTML containing the header for a displayed export
  481. *
  482. * @param string $exportType 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 $exportType,
  490. string $db,
  491. string $table
  492. ): array {
  493. $html = '<div>';
  494. /**
  495. * Displays a back button with all the $_POST data in the URL
  496. * (store in a variable to also display after the textarea)
  497. */
  498. $backButton = '<p id="export_back_button">[ <a href="';
  499. if ($exportType === 'server') {
  500. $backButton .= Url::getFromRoute('/server/export') . '" data-post="' . Url::getCommon([], '');
  501. } elseif ($exportType === 'database') {
  502. $backButton .= Url::getFromRoute('/database/export') . '" data-post="' . Url::getCommon(['db' => $db], '');
  503. } else {
  504. $backButton .= Url::getFromRoute('/table/export') . '" data-post="' . Url::getCommon(
  505. [
  506. 'db' => $db,
  507. 'table' => $table,
  508. ],
  509. ''
  510. );
  511. }
  512. $postParams = $_POST;
  513. // Convert the multiple select elements from an array to a string
  514. if ($exportType === 'database') {
  515. $structOrDataForced = empty($postParams['structure_or_data_forced']);
  516. if ($structOrDataForced && ! isset($postParams['table_structure'])) {
  517. $postParams['table_structure'] = [];
  518. }
  519. if ($structOrDataForced && ! isset($postParams['table_data'])) {
  520. $postParams['table_data'] = [];
  521. }
  522. }
  523. foreach ($postParams as $name => $value) {
  524. if (is_array($value)) {
  525. continue;
  526. }
  527. $backButton .= '&amp;' . urlencode((string) $name) . '=' . urlencode((string) $value);
  528. }
  529. $backButton .= '&amp;repopulate=1">' . __('Back') . '</a> ]</p>';
  530. $html .= '<br>';
  531. $html .= $backButton;
  532. $refreshButton = '<form id="export_refresh_form" method="POST" action="'
  533. . Url::getFromRoute('/export') . '" class="disableAjax">';
  534. $refreshButton .= '[ <a class="disableAjax export_refresh_btn">' . __('Refresh') . '</a> ]';
  535. foreach ($postParams as $name => $value) {
  536. if (is_array($value)) {
  537. foreach ($value as $val) {
  538. $refreshButton .= '<input type="hidden" name="' . htmlentities((string) $name)
  539. . '[]" value="' . htmlentities((string) $val) . '">';
  540. }
  541. } else {
  542. $refreshButton .= '<input type="hidden" name="' . htmlentities((string) $name)
  543. . '" value="' . htmlentities((string) $value) . '">';
  544. }
  545. }
  546. $refreshButton .= '</form>';
  547. $html .= $refreshButton
  548. . '<br>'
  549. . '<form name="nofunction">'
  550. . '<textarea name="sqldump" cols="50" rows="30" '
  551. . 'id="textSQLDUMP" wrap="OFF">';
  552. return [
  553. $html,
  554. $backButton,
  555. $refreshButton,
  556. ];
  557. }
  558. /**
  559. * Export at the server level
  560. *
  561. * @param string|array $dbSelect the selected databases to export
  562. * @param string $whatStrucOrData structure or data or both
  563. * @param ExportPlugin $exportPlugin the selected export plugin
  564. * @param string $crlf end of line character(s)
  565. * @param string $errorUrl the URL in case of error
  566. * @param string $exportType the export type
  567. * @param bool $doRelation whether to export relation info
  568. * @param bool $doComments whether to add comments
  569. * @param bool $doMime whether to add MIME info
  570. * @param bool $doDates whether to add dates
  571. * @param array $aliases alias information for db/table/column
  572. * @param string $separateFiles whether it is a separate-files export
  573. */
  574. public function exportServer(
  575. $dbSelect,
  576. string $whatStrucOrData,
  577. ExportPlugin $exportPlugin,
  578. string $crlf,
  579. string $errorUrl,
  580. string $exportType,
  581. bool $doRelation,
  582. bool $doComments,
  583. bool $doMime,
  584. bool $doDates,
  585. array $aliases,
  586. string $separateFiles
  587. ): void {
  588. if (! empty($dbSelect) && is_array($dbSelect)) {
  589. $tmpSelect = implode('|', $dbSelect);
  590. $tmpSelect = '|' . $tmpSelect . '|';
  591. }
  592. // Walk over databases
  593. foreach ($GLOBALS['dblist']->databases as $currentDb) {
  594. if (! isset($tmpSelect) || ! mb_strpos(' ' . $tmpSelect, '|' . $currentDb . '|')) {
  595. continue;
  596. }
  597. $tables = $this->dbi->getTables($currentDb);
  598. $this->exportDatabase(
  599. $currentDb,
  600. $tables,
  601. $whatStrucOrData,
  602. $tables,
  603. $tables,
  604. $exportPlugin,
  605. $crlf,
  606. $errorUrl,
  607. $exportType,
  608. $doRelation,
  609. $doComments,
  610. $doMime,
  611. $doDates,
  612. $aliases,
  613. $separateFiles === 'database' ? $separateFiles : ''
  614. );
  615. if ($separateFiles !== 'server') {
  616. continue;
  617. }
  618. $this->saveObjectInBuffer($currentDb);
  619. }
  620. }
  621. /**
  622. * Export at the database level
  623. *
  624. * @param string $db the database to export
  625. * @param array $tables the tables to export
  626. * @param string $whatStrucOrData structure or data or both
  627. * @param array $tableStructure whether to export structure for each table
  628. * @param array $tableData whether to export data for each table
  629. * @param ExportPlugin $exportPlugin the selected export plugin
  630. * @param string $crlf end of line character(s)
  631. * @param string $errorUrl the URL in case of error
  632. * @param string $exportType the export type
  633. * @param bool $doRelation whether to export relation info
  634. * @param bool $doComments whether to add comments
  635. * @param bool $doMime whether to add MIME info
  636. * @param bool $doDates whether to add dates
  637. * @param array $aliases Alias information for db/table/column
  638. * @param string $separateFiles whether it is a separate-files export
  639. */
  640. public function exportDatabase(
  641. string $db,
  642. array $tables,
  643. string $whatStrucOrData,
  644. array $tableStructure,
  645. array $tableData,
  646. ExportPlugin $exportPlugin,
  647. string $crlf,
  648. string $errorUrl,
  649. string $exportType,
  650. bool $doRelation,
  651. bool $doComments,
  652. bool $doMime,
  653. bool $doDates,
  654. array $aliases,
  655. string $separateFiles
  656. ): void {
  657. $dbAlias = ! empty($aliases[$db]['alias'])
  658. ? $aliases[$db]['alias'] : '';
  659. if (! $exportPlugin->exportDBHeader($db, $dbAlias)) {
  660. return;
  661. }
  662. if (! $exportPlugin->exportDBCreate($db, $exportType, $dbAlias)) {
  663. return;
  664. }
  665. if ($separateFiles === 'database') {
  666. $this->saveObjectInBuffer('database', true);
  667. }
  668. if (
  669. ($GLOBALS['sql_structure_or_data'] === 'structure'
  670. || $GLOBALS['sql_structure_or_data'] === 'structure_and_data')
  671. && isset($GLOBALS['sql_procedure_function'])
  672. ) {
  673. $exportPlugin->exportRoutines($db, $aliases);
  674. if ($separateFiles === 'database') {
  675. $this->saveObjectInBuffer('routines');
  676. }
  677. }
  678. $views = [];
  679. foreach ($tables as $table) {
  680. $tableObject = new Table($table, $db);
  681. // if this is a view, collect it for later;
  682. // views must be exported after the tables
  683. $isView = $tableObject->isView();
  684. if ($isView) {
  685. $views[] = $table;
  686. }
  687. if (
  688. ($whatStrucOrData === 'structure'
  689. || $whatStrucOrData === 'structure_and_data')
  690. && in_array($table, $tableStructure)
  691. ) {
  692. // for a view, export a stand-in definition of the table
  693. // to resolve view dependencies (only when it's a single-file export)
  694. if ($isView) {
  695. if (
  696. $separateFiles == ''
  697. && isset($GLOBALS['sql_create_view'])
  698. && ! $exportPlugin->exportStructure(
  699. $db,
  700. $table,
  701. $crlf,
  702. $errorUrl,
  703. 'stand_in',
  704. $exportType,
  705. $doRelation,
  706. $doComments,
  707. $doMime,
  708. $doDates,
  709. $aliases
  710. )
  711. ) {
  712. break;
  713. }
  714. } elseif (isset($GLOBALS['sql_create_table'])) {
  715. $tableSize = $GLOBALS['maxsize'];
  716. // Checking if the maximum table size constrain has been set
  717. // And if that constrain is a valid number or not
  718. if ($tableSize !== '' && is_numeric($tableSize)) {
  719. // This obtains the current table's size
  720. $query = 'SELECT data_length + index_length
  721. from information_schema.TABLES
  722. WHERE table_schema = "' . $this->dbi->escapeString($db) . '"
  723. AND table_name = "' . $this->dbi->escapeString($table) . '"';
  724. $size = (int) $this->dbi->fetchValue($query);
  725. //Converting the size to MB
  726. $size /= 1024 / 1024;
  727. if ($size > $tableSize) {
  728. continue;
  729. }
  730. }
  731. if (
  732. ! $exportPlugin->exportStructure(
  733. $db,
  734. $table,
  735. $crlf,
  736. $errorUrl,
  737. 'create_table',
  738. $exportType,
  739. $doRelation,
  740. $doComments,
  741. $doMime,
  742. $doDates,
  743. $aliases
  744. )
  745. ) {
  746. break;
  747. }
  748. }
  749. }
  750. // if this is a view or a merge table, don't export data
  751. if (
  752. ($whatStrucOrData === 'data' || $whatStrucOrData === 'structure_and_data')
  753. && in_array($table, $tableData)
  754. && ! $isView
  755. ) {
  756. $tableObj = new Table($table, $db);
  757. $nonGeneratedCols = $tableObj->getNonGeneratedColumns(true);
  758. $localQuery = 'SELECT ' . implode(', ', $nonGeneratedCols)
  759. . ' FROM ' . Util::backquote($db)
  760. . '.' . Util::backquote($table);
  761. if (! $exportPlugin->exportData($db, $table, $crlf, $errorUrl, $localQuery, $aliases)) {
  762. break;
  763. }
  764. }
  765. // this buffer was filled, we save it and go to the next one
  766. if ($separateFiles === 'database') {
  767. $this->saveObjectInBuffer('table_' . $table);
  768. }
  769. // now export the triggers (needs to be done after the data because
  770. // triggers can modify already imported tables)
  771. if (
  772. ! isset($GLOBALS['sql_create_trigger']) || ($whatStrucOrData !== 'structure'
  773. && $whatStrucOrData !== 'structure_and_data')
  774. || ! in_array($table, $tableStructure)
  775. ) {
  776. continue;
  777. }
  778. if (
  779. ! $exportPlugin->exportStructure(
  780. $db,
  781. $table,
  782. $crlf,
  783. $errorUrl,
  784. 'triggers',
  785. $exportType,
  786. $doRelation,
  787. $doComments,
  788. $doMime,
  789. $doDates,
  790. $aliases
  791. )
  792. ) {
  793. break;
  794. }
  795. if ($separateFiles !== 'database') {
  796. continue;
  797. }
  798. $this->saveObjectInBuffer('table_' . $table, true);
  799. }
  800. if (isset($GLOBALS['sql_create_view'])) {
  801. foreach ($views as $view) {
  802. // no data export for a view
  803. if ($whatStrucOrData !== 'structure' && $whatStrucOrData !== 'structure_and_data') {
  804. continue;
  805. }
  806. if (
  807. ! $exportPlugin->exportStructure(
  808. $db,
  809. $view,
  810. $crlf,
  811. $errorUrl,
  812. 'create_view',
  813. $exportType,
  814. $doRelation,
  815. $doComments,
  816. $doMime,
  817. $doDates,
  818. $aliases
  819. )
  820. ) {
  821. break;
  822. }
  823. if ($separateFiles !== 'database') {
  824. continue;
  825. }
  826. $this->saveObjectInBuffer('view_' . $view);
  827. }
  828. }
  829. if (! $exportPlugin->exportDBFooter($db)) {
  830. return;
  831. }
  832. // export metadata related to this db
  833. if (isset($GLOBALS['sql_metadata'])) {
  834. // Types of metadata to export.
  835. // In the future these can be allowed to be selected by the user
  836. $metadataTypes = $this->getMetadataTypes();
  837. $exportPlugin->exportMetadata($db, $tables, $metadataTypes);
  838. if ($separateFiles === 'database') {
  839. $this->saveObjectInBuffer('metadata');
  840. }
  841. }
  842. if ($separateFiles === 'database') {
  843. $this->saveObjectInBuffer('extra');
  844. }
  845. if (
  846. ($GLOBALS['sql_structure_or_data'] !== 'structure'
  847. && $GLOBALS['sql_structure_or_data'] !== 'structure_and_data')
  848. || ! isset($GLOBALS['sql_procedure_function'])
  849. ) {
  850. return;
  851. }
  852. $exportPlugin->exportEvents($db);
  853. if ($separateFiles !== 'database') {
  854. return;
  855. }
  856. $this->saveObjectInBuffer('events');
  857. }
  858. /**
  859. * Export a raw query
  860. *
  861. * @param string $whatStrucOrData whether to export structure for each table or raw
  862. * @param ExportPlugin $exportPlugin the selected export plugin
  863. * @param string $crlf end of line character(s)
  864. * @param string $errorUrl the URL in case of error
  865. * @param string $sqlQuery the query to be executed
  866. * @param string $exportType the export type
  867. */
  868. public static function exportRaw(
  869. string $whatStrucOrData,
  870. ExportPlugin $exportPlugin,
  871. string $crlf,
  872. string $errorUrl,
  873. string $sqlQuery,
  874. string $exportType
  875. ): void {
  876. // In case the we need to dump just the raw query
  877. if ($whatStrucOrData !== 'raw') {
  878. return;
  879. }
  880. if (! $exportPlugin->exportRawQuery($errorUrl, $sqlQuery, $crlf)) {
  881. $GLOBALS['message'] = Message::error(
  882. // phpcs:disable Generic.Files.LineLength.TooLong
  883. /* l10n: A query written by the user is a "raw query" that could be using no tables or databases in particular */
  884. __('Exporting a raw query is not supported for this export method.')
  885. );
  886. return;
  887. }
  888. }
  889. /**
  890. * Export at the table level
  891. *
  892. * @param string $db the database to export
  893. * @param string $table the table to export
  894. * @param string $whatStrucOrData structure or data or both
  895. * @param ExportPlugin $exportPlugin the selected export plugin
  896. * @param string $crlf end of line character(s)
  897. * @param string $errorUrl the URL in case of error
  898. * @param string $exportType the export type
  899. * @param bool $doRelation whether to export relation info
  900. * @param bool $doComments whether to add comments
  901. * @param bool $doMime whether to add MIME info
  902. * @param bool $doDates whether to add dates
  903. * @param string|null $allrows whether "dump all rows" was ticked
  904. * @param string $limitTo upper limit
  905. * @param string $limitFrom starting limit
  906. * @param string $sqlQuery query for which exporting is requested
  907. * @param array $aliases Alias information for db/table/column
  908. */
  909. public function exportTable(
  910. string $db,
  911. string $table,
  912. string $whatStrucOrData,
  913. ExportPlugin $exportPlugin,
  914. string $crlf,
  915. string $errorUrl,
  916. string $exportType,
  917. bool $doRelation,
  918. bool $doComments,
  919. bool $doMime,
  920. bool $doDates,
  921. ?string $allrows,
  922. string $limitTo,
  923. string $limitFrom,
  924. string $sqlQuery,
  925. array $aliases
  926. ): void {
  927. $dbAlias = ! empty($aliases[$db]['alias'])
  928. ? $aliases[$db]['alias'] : '';
  929. if (! $exportPlugin->exportDBHeader($db, $dbAlias)) {
  930. return;
  931. }
  932. if (isset($allrows) && $allrows == '0' && $limitTo > 0 && $limitFrom >= 0) {
  933. $addQuery = ' LIMIT '
  934. . ($limitFrom > 0 ? $limitFrom . ', ' : '')
  935. . $limitTo;
  936. } else {
  937. $addQuery = '';
  938. }
  939. $tableObject = new Table($table, $db);
  940. $isView = $tableObject->isView();
  941. if ($whatStrucOrData === 'structure' || $whatStrucOrData === 'structure_and_data') {
  942. if ($isView) {
  943. if (isset($GLOBALS['sql_create_view'])) {
  944. if (
  945. ! $exportPlugin->exportStructure(
  946. $db,
  947. $table,
  948. $crlf,
  949. $errorUrl,
  950. 'create_view',
  951. $exportType,
  952. $doRelation,
  953. $doComments,
  954. $doMime,
  955. $doDates,
  956. $aliases
  957. )
  958. ) {
  959. return;
  960. }
  961. }
  962. } elseif (isset($GLOBALS['sql_create_table'])) {
  963. if (
  964. ! $exportPlugin->exportStructure(
  965. $db,
  966. $table,
  967. $crlf,
  968. $errorUrl,
  969. 'create_table',
  970. $exportType,
  971. $doRelation,
  972. $doComments,
  973. $doMime,
  974. $doDates,
  975. $aliases
  976. )
  977. ) {
  978. return;
  979. }
  980. }
  981. }
  982. // If this is an export of a single view, we have to export data;
  983. // for example, a PDF report
  984. // if it is a merge table, no data is exported
  985. if ($whatStrucOrData === 'data' || $whatStrucOrData === 'structure_and_data') {
  986. if (! empty($sqlQuery)) {
  987. // only preg_replace if needed
  988. if (! empty($addQuery)) {
  989. // remove trailing semicolon before adding a LIMIT
  990. $sqlQuery = preg_replace('%;\s*$%', '', $sqlQuery);
  991. }
  992. $localQuery = $sqlQuery . $addQuery;
  993. $this->dbi->selectDb($db);
  994. } else {
  995. // Data is exported only for Non-generated columns
  996. $tableObj = new Table($table, $db);
  997. $nonGeneratedCols = $tableObj->getNonGeneratedColumns(true);
  998. $localQuery = 'SELECT ' . implode(', ', $nonGeneratedCols)
  999. . ' FROM ' . Util::backquote($db)
  1000. . '.' . Util::backquote($table) . $addQuery;
  1001. }
  1002. if (! $exportPlugin->exportData($db, $table, $crlf, $errorUrl, $localQuery, $aliases)) {
  1003. return;
  1004. }
  1005. }
  1006. // now export the triggers (needs to be done after the data because
  1007. // triggers can modify already imported tables)
  1008. if (
  1009. isset($GLOBALS['sql_create_trigger']) && ($whatStrucOrData === 'structure'
  1010. || $whatStrucOrData === 'structure_and_data')
  1011. ) {
  1012. if (
  1013. ! $exportPlugin->exportStructure(
  1014. $db,
  1015. $table,
  1016. $crlf,
  1017. $errorUrl,
  1018. 'triggers',
  1019. $exportType,
  1020. $doRelation,
  1021. $doComments,
  1022. $doMime,
  1023. $doDates,
  1024. $aliases
  1025. )
  1026. ) {
  1027. return;
  1028. }
  1029. }
  1030. if (! $exportPlugin->exportDBFooter($db)) {
  1031. return;
  1032. }
  1033. if (! isset($GLOBALS['sql_metadata'])) {
  1034. return;
  1035. }
  1036. // Types of metadata to export.
  1037. // In the future these can be allowed to be selected by the user
  1038. $metadataTypes = $this->getMetadataTypes();
  1039. $exportPlugin->exportMetadata($db, $table, $metadataTypes);
  1040. }
  1041. /**
  1042. * Loads correct page after doing export
  1043. */
  1044. public function showPage(string $exportType): void
  1045. {
  1046. global $active_page, $containerBuilder;
  1047. if ($exportType === 'server') {
  1048. $active_page = Url::getFromRoute('/server/export');
  1049. /** @var ServerExportController $controller */
  1050. $controller = $containerBuilder->get(ServerExportController::class);
  1051. $controller();
  1052. return;
  1053. }
  1054. if ($exportType === 'database') {
  1055. $active_page = Url::getFromRoute('/database/export');
  1056. /** @var DatabaseExportController $controller */
  1057. $controller = $containerBuilder->get(DatabaseExportController::class);
  1058. $controller();
  1059. return;
  1060. }
  1061. $active_page = Url::getFromRoute('/table/export');
  1062. /** @var TableExportController $controller */
  1063. $controller = $containerBuilder->get(TableExportController::class);
  1064. $controller();
  1065. }
  1066. /**
  1067. * Merge two alias arrays, if array1 and array2 have
  1068. * conflicting alias then array2 value is used if it
  1069. * is non empty otherwise array1 value.
  1070. *
  1071. * @param array $aliases1 first array of aliases
  1072. * @param array $aliases2 second array of aliases
  1073. *
  1074. * @return array resultant merged aliases info
  1075. */
  1076. public function mergeAliases(array $aliases1, array $aliases2): array
  1077. {
  1078. // First do a recursive array merge
  1079. // on aliases arrays.
  1080. $aliases = array_merge_recursive($aliases1, $aliases2);
  1081. // Now, resolve conflicts in aliases, if any
  1082. foreach ($aliases as $dbName => $db) {
  1083. // If alias key is an array then
  1084. // it is a merge conflict.
  1085. if (isset($db['alias']) && is_array($db['alias'])) {
  1086. $val1 = $db['alias'][0];
  1087. $val2 = $db['alias'][1];
  1088. // Use aliases2 alias if non empty
  1089. $aliases[$dbName]['alias'] = empty($val2) ? $val1 : $val2;
  1090. }
  1091. if (! isset($db['tables'])) {
  1092. continue;
  1093. }
  1094. foreach ($db['tables'] as $tableName => $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[$dbName]['tables'][$tableName]['alias'] = empty($val2) ? $val1 : $val2;
  1100. }
  1101. if (! isset($tbl['columns'])) {
  1102. continue;
  1103. }
  1104. foreach ($tbl['columns'] as $col => $colAs) {
  1105. if (! isset($colAs) || ! is_array($colAs)) {
  1106. continue;
  1107. }
  1108. $val1 = $colAs[0];
  1109. $val2 = $colAs[1];
  1110. // Use aliases2 alias if non empty
  1111. $aliases[$dbName]['tables'][$tableName]['columns'][$col] = empty($val2) ? $val1 : $val2;
  1112. }
  1113. }
  1114. }
  1115. return $aliases;
  1116. }
  1117. /**
  1118. * Locks tables
  1119. *
  1120. * @param string $db database name
  1121. * @param array $tables list of table names
  1122. * @param string $lockType lock type; "[LOW_PRIORITY] WRITE" or "READ [LOCAL]"
  1123. *
  1124. * @return mixed result of the query
  1125. */
  1126. public function lockTables(string $db, array $tables, string $lockType = 'WRITE')
  1127. {
  1128. $locks = [];
  1129. foreach ($tables as $table) {
  1130. $locks[] = Util::backquote($db) . '.'
  1131. . Util::backquote($table) . ' ' . $lockType;
  1132. }
  1133. $sql = 'LOCK TABLES ' . implode(', ', $locks);
  1134. return $this->dbi->tryQuery($sql);
  1135. }
  1136. /**
  1137. * Releases table locks
  1138. *
  1139. * @return mixed result of the query
  1140. */
  1141. public function unlockTables()
  1142. {
  1143. return $this->dbi->tryQuery('UNLOCK TABLES');
  1144. }
  1145. /**
  1146. * Returns all the metadata types that can be exported with a database or a table
  1147. *
  1148. * @return string[] metadata types.
  1149. */
  1150. public function getMetadataTypes(): array
  1151. {
  1152. return [
  1153. 'column_info',
  1154. 'table_uiprefs',
  1155. 'tracking',
  1156. 'bookmark',
  1157. 'relation',
  1158. 'table_coords',
  1159. 'pdf_pages',
  1160. 'savedsearches',
  1161. 'central_columns',
  1162. 'export_templates',
  1163. ];
  1164. }
  1165. /**
  1166. * Returns the checked clause, depending on the presence of key in array
  1167. *
  1168. * @param string $key the key to look for
  1169. * @param array $array array to verify
  1170. *
  1171. * @return string the checked clause
  1172. */
  1173. public function getCheckedClause(string $key, array $array): string
  1174. {
  1175. if (in_array($key, $array)) {
  1176. return ' checked="checked"';
  1177. }
  1178. return '';
  1179. }
  1180. /**
  1181. * get all the export options and verify
  1182. * call and include the appropriate Schema Class depending on $export_type
  1183. *
  1184. * @param string|null $exportType format of the export
  1185. */
  1186. public function processExportSchema(?string $exportType): void
  1187. {
  1188. /**
  1189. * default is PDF, otherwise validate it's only letters a-z
  1190. */
  1191. if (! isset($exportType) || ! preg_match('/^[a-zA-Z]+$/', $exportType)) {
  1192. $exportType = 'pdf';
  1193. }
  1194. // sanitize this parameter which will be used below in a file inclusion
  1195. $exportType = Core::securePath($exportType);
  1196. // get the specific plugin
  1197. /** @var SchemaPlugin $exportPlugin */
  1198. $exportPlugin = Plugins::getPlugin('schema', $exportType);
  1199. // Check schema export type
  1200. if ($exportPlugin === null || ! is_object($exportPlugin)) {
  1201. Core::fatalError(__('Bad type!'));
  1202. }
  1203. $this->dbi->selectDb($_POST['db']);
  1204. $exportPlugin->exportSchema($_POST['db']);
  1205. }
  1206. }