Export.php 46 KB

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