dbi = $dbi; } /** * Sets a session variable upon a possible fatal error during export */ public function shutdown(): void { $error = error_get_last(); if ($error == null || ! mb_strpos($error['message'], 'execution time')) { return; } //set session variable to check if there was error while exporting $_SESSION['pma_export_error'] = $error['message']; } /** * Detect ob_gzhandler */ public function isGzHandlerEnabled(): bool { /** @var string[] $handlers */ $handlers = ob_list_handlers(); return in_array('ob_gzhandler', $handlers); } /** * Detect whether gzencode is needed; it might not be needed if * the server is already compressing by itself */ public function gzencodeNeeded(): bool { /* * We should gzencode only if the function exists * but we don't want to compress twice, therefore * gzencode only if transparent compression is not enabled * and gz compression was not asked via $cfg['OBGzip'] * but transparent compression does not apply when saving to server */ return function_exists('gzencode') && ((! ini_get('zlib.output_compression') && ! $this->isGzHandlerEnabled()) || $GLOBALS['save_on_server'] || $GLOBALS['config']->get('PMA_USR_BROWSER_AGENT') === 'CHROME'); } /** * Output handler for all exports, if needed buffering, it stores data into * $this->dumpBuffer, otherwise it prints them out. * * @param string $line the insert statement */ public function outputHandler(?string $line): bool { $GLOBALS['time_start'] = $GLOBALS['time_start'] ?? null; $GLOBALS['save_filename'] = $GLOBALS['save_filename'] ?? null; // Kanji encoding convert feature if ($GLOBALS['output_kanji_conversion']) { $line = Encoding::kanjiStrConv($line, $GLOBALS['knjenc'], $GLOBALS['xkana'] ?? ''); } // If we have to buffer data, we will perform everything at once at the end if ($GLOBALS['buffer_needed']) { $this->dumpBuffer .= $line; if ($GLOBALS['onfly_compression']) { $this->dumpBufferLength += strlen((string) $line); if ($this->dumpBufferLength > $GLOBALS['memory_limit']) { if ($GLOBALS['output_charset_conversion']) { $this->dumpBuffer = Encoding::convertString('utf-8', $GLOBALS['charset'], $this->dumpBuffer); } if ($GLOBALS['compression'] === 'gzip' && $this->gzencodeNeeded()) { // as a gzipped file // without the optional parameter level because it bugs $this->dumpBuffer = gzencode($this->dumpBuffer); } if ($GLOBALS['save_on_server']) { $writeResult = @fwrite($GLOBALS['file_handle'], (string) $this->dumpBuffer); // Here, use strlen rather than mb_strlen to get the length // in bytes to compare against the number of bytes written. if ($writeResult != strlen((string) $this->dumpBuffer)) { $GLOBALS['message'] = Message::error( __('Insufficient space to save the file %s.') ); $GLOBALS['message']->addParam($GLOBALS['save_filename']); return false; } } else { echo $this->dumpBuffer; } $this->dumpBuffer = ''; $this->dumpBufferLength = 0; } } else { $timeNow = time(); if ($GLOBALS['time_start'] >= $timeNow + 30) { $GLOBALS['time_start'] = $timeNow; header('X-pmaPing: Pong'); } } } elseif ($GLOBALS['asfile']) { if ($GLOBALS['output_charset_conversion']) { $line = Encoding::convertString('utf-8', $GLOBALS['charset'], $line); } if ($GLOBALS['save_on_server'] && mb_strlen((string) $line) > 0) { if ($GLOBALS['file_handle'] !== null) { $writeResult = @fwrite($GLOBALS['file_handle'], (string) $line); } else { $writeResult = false; } // Here, use strlen rather than mb_strlen to get the length // in bytes to compare against the number of bytes written. if (! $writeResult || $writeResult != strlen((string) $line)) { $GLOBALS['message'] = Message::error( __('Insufficient space to save the file %s.') ); $GLOBALS['message']->addParam($GLOBALS['save_filename']); return false; } $timeNow = time(); if ($GLOBALS['time_start'] >= $timeNow + 30) { $GLOBALS['time_start'] = $timeNow; header('X-pmaPing: Pong'); } } else { // We export as file - output normally echo $line; } } else { // We export as html - replace special chars echo htmlspecialchars((string) $line); } return true; } /** * Returns HTML containing the footer for a displayed export * * @param string $exportType the export type * @param string $db the database name * @param string $table the table name * * @return string the HTML output */ public function getHtmlForDisplayedExportFooter( string $exportType, string $db, string $table ): string { /** * Close the html tags and add the footers for on-screen export */ return '' . ' ' . '
' // bottom back button . $this->getHTMLForBackButton($exportType, $db, $table) . $this->getHTMLForRefreshButton($exportType) . '' . '' . "\n"; } /** * Computes the memory limit for export * * @return int the memory limit */ public function getMemoryLimit(): int { $memoryLimit = trim((string) ini_get('memory_limit')); $memoryLimitNumber = (int) substr($memoryLimit, 0, -1); $lowerLastChar = strtolower(substr($memoryLimit, -1)); // 2 MB as default if (empty($memoryLimit) || $memoryLimit == '-1') { $memoryLimit = 2 * 1024 * 1024; } elseif ($lowerLastChar === 'm') { $memoryLimit = $memoryLimitNumber * 1024 * 1024; } elseif ($lowerLastChar === 'k') { $memoryLimit = $memoryLimitNumber * 1024; } elseif ($lowerLastChar === 'g') { $memoryLimit = $memoryLimitNumber * 1024 * 1024 * 1024; } else { $memoryLimit = (int) $memoryLimit; } // Some of memory is needed for other things and as threshold. // During export I had allocated (see memory_get_usage function) // approx 1.2MB so this comes from that. if ($memoryLimit > 1500000) { $memoryLimit -= 1500000; } // Some memory is needed for compression, assume 1/3 $memoryLimit /= 8; return $memoryLimit; } /** * Returns the filename and MIME type for a compression and an export plugin * * @param ExportPlugin $exportPlugin the export plugin * @param string $compression compression asked * @param string $filename the filename * * @return string[] the filename and mime type */ public function getFinalFilenameAndMimetypeForFilename( ExportPlugin $exportPlugin, string $compression, string $filename ): array { // Grab basic dump extension and mime type // Check if the user already added extension; // get the substring where the extension would be if it was included $requiredExtension = '.' . $exportPlugin->getProperties()->getExtension(); $extensionLength = mb_strlen($requiredExtension); $userExtension = mb_substr($filename, -$extensionLength); if (mb_strtolower($userExtension) != $requiredExtension) { $filename .= $requiredExtension; } $mediaType = $exportPlugin->getProperties()->getMimeType(); // If dump is going to be compressed, set correct mime_type and add // compression to extension if ($compression === 'gzip') { $filename .= '.gz'; $mediaType = 'application/x-gzip'; } elseif ($compression === 'zip') { $filename .= '.zip'; $mediaType = 'application/zip'; } return [ $filename, $mediaType, ]; } /** * Return the filename and MIME type for export file * * @param string $exportType type of export * @param string $rememberTemplate whether to remember template * @param ExportPlugin $exportPlugin the export plugin * @param string $compression compression asked * @param string $filenameTemplate the filename template * * @return string[] the filename template and mime type */ public function getFilenameAndMimetype( string $exportType, string $rememberTemplate, ExportPlugin $exportPlugin, string $compression, string $filenameTemplate ): array { if ($exportType === 'server') { if (! empty($rememberTemplate)) { $GLOBALS['config']->setUserValue( 'pma_server_filename_template', 'Export/file_template_server', $filenameTemplate ); } } elseif ($exportType === 'database') { if (! empty($rememberTemplate)) { $GLOBALS['config']->setUserValue( 'pma_db_filename_template', 'Export/file_template_database', $filenameTemplate ); } } elseif ($exportType === 'raw') { if (! empty($rememberTemplate)) { $GLOBALS['config']->setUserValue( 'pma_raw_filename_template', 'Export/file_template_raw', $filenameTemplate ); } } else { if (! empty($rememberTemplate)) { $GLOBALS['config']->setUserValue( 'pma_table_filename_template', 'Export/file_template_table', $filenameTemplate ); } } $filename = Util::expandUserString($filenameTemplate); // remove dots in filename (coming from either the template or already // part of the filename) to avoid a remote code execution vulnerability $filename = Sanitize::sanitizeFilename($filename, true); return $this->getFinalFilenameAndMimetypeForFilename($exportPlugin, $compression, $filename); } /** * Open the export file * * @param string $filename the export filename * @param bool $quickExport whether it's a quick export or not * * @return array the full save filename, possible message and the file handle */ public function openFile(string $filename, bool $quickExport): array { $fileHandle = null; $message = ''; $doNotSaveItOver = true; if (isset($_POST['quick_export_onserver_overwrite'])) { $doNotSaveItOver = $_POST['quick_export_onserver_overwrite'] !== 'saveitover'; } $saveFilename = Util::userDir((string) ($GLOBALS['cfg']['SaveDir'] ?? '')) . preg_replace('@[/\\\\]@', '_', $filename); if ( @file_exists($saveFilename) && ((! $quickExport && empty($_POST['onserver_overwrite'])) || ($quickExport && $doNotSaveItOver)) ) { $message = Message::error( __( 'File %s already exists on server, change filename or check overwrite option.' ) ); $message->addParam($saveFilename); } elseif (@is_file($saveFilename) && ! @is_writable($saveFilename)) { $message = Message::error( __( 'The web server does not have permission to save the file %s.' ) ); $message->addParam($saveFilename); } else { $fileHandle = @fopen($saveFilename, 'w'); if ($fileHandle === false) { $message = Message::error( __( 'The web server does not have permission to save the file %s.' ) ); $message->addParam($saveFilename); } } return [ $saveFilename, $message, $fileHandle, ]; } /** * Close the export file * * @param resource $fileHandle the export file handle * @param string $dumpBuffer the current dump buffer * @param string $saveFilename the export filename * * @return Message a message object (or empty string) */ public function closeFile( $fileHandle, string $dumpBuffer, string $saveFilename ): Message { $writeResult = @fwrite($fileHandle, $dumpBuffer); fclose($fileHandle); // Here, use strlen rather than mb_strlen to get the length // in bytes to compare against the number of bytes written. if (strlen($dumpBuffer) > 0 && (! $writeResult || $writeResult != strlen($dumpBuffer))) { $message = new Message( __('Insufficient space to save the file %s.'), Message::ERROR, [$saveFilename] ); } else { $message = new Message( __('Dump has been saved to file %s.'), Message::SUCCESS, [$saveFilename] ); } return $message; } /** * Compress the export buffer * * @param array|string $dumpBuffer the current dump buffer * @param string $compression the compression mode * @param string $filename the filename * * @return array|string|bool */ public function compress($dumpBuffer, string $compression, string $filename) { if ($compression === 'zip' && function_exists('gzcompress')) { $zipExtension = new ZipExtension(); $filename = substr($filename, 0, -4); // remove extension (.zip) $dumpBuffer = $zipExtension->createFile($dumpBuffer, $filename); } elseif ($compression === 'gzip' && $this->gzencodeNeeded() && is_string($dumpBuffer)) { // without the optional parameter level because it bugs $dumpBuffer = gzencode($dumpBuffer); } return $dumpBuffer; } /** * Saves the dump buffer for a particular table in an array * Used in separate files export * * @param string $objectName the name of current object to be stored * @param bool $append optional boolean to append to an existing index or not */ public function saveObjectInBuffer(string $objectName, bool $append = false): void { if (! empty($this->dumpBuffer)) { if ($append && isset($this->dumpBufferObjects[$objectName])) { $this->dumpBufferObjects[$objectName] .= $this->dumpBuffer; } else { $this->dumpBufferObjects[$objectName] = $this->dumpBuffer; } } // Re - initialize $this->dumpBuffer = ''; $this->dumpBufferLength = 0; } /** * Returns HTML containing the header for a displayed export * * @param string $exportType the export type * @param string $db the database name * @param string $table the table name * * @return string the generated HTML and back button */ public function getHtmlForDisplayedExportHeader( string $exportType, string $db, string $table ): string { /** * Displays a back button with all the $_POST data in the URL */ return '
' . '
' . $this->getHTMLForBackButton($exportType, $db, $table) . $this->getHTMLForRefreshButton($exportType) . '
' . '
' . '