diff --git a/libraries/classes/Controllers/Database/ExportController.php b/libraries/classes/Controllers/Database/ExportController.php
index f8765a38e2..9545f9f84d 100644
--- a/libraries/classes/Controllers/Database/ExportController.php
+++ b/libraries/classes/Controllers/Database/ExportController.php
@@ -168,10 +168,17 @@ final class ExportController extends AbstractController
$unlim_num_rows = 0;
}
+ $isReturnBackFromRawExport = isset($_POST['export_type']) && $_POST['export_type'] === 'raw';
+ if (isset($_POST['raw_query']) || $isReturnBackFromRawExport) {
+ $export_type = 'raw';
+ } else {
+ $export_type = 'table';
+ }
+
$displayExport = new DisplayExport();
$this->response->addHTML(
$displayExport->getDisplay(
- 'database',
+ $export_type,
$db,
$table,
$sql_query,
diff --git a/libraries/classes/Controllers/ExportController.php b/libraries/classes/Controllers/ExportController.php
index df594d411d..df1f508280 100644
--- a/libraries/classes/Controllers/ExportController.php
+++ b/libraries/classes/Controllers/ExportController.php
@@ -334,6 +334,10 @@ final class ExportController extends AbstractController
'db' => $db,
'table' => $table,
]);
+ } elseif ($export_type === 'raw') {
+ $err_url = Url::getFromRoute('/server/export', [
+ 'sql_query' => $sql_query,
+ ]);
} else {
Core::fatalError(__('Bad parameters!'));
}
@@ -409,6 +413,11 @@ final class ExportController extends AbstractController
$mime_type = '';
}
+ // For raw query export, filename will be export.extension
+ if ($export_type === 'raw') {
+ [$filename ] = $this->export->getFinalFilenameAndMimetypeForFilename($export_plugin, $compression, 'export');
+ }
+
// Open file on server if needed
if ($save_on_server) {
list($save_filename, $message, $file_handle) = $this->export->openFile(
@@ -482,6 +491,10 @@ final class ExportController extends AbstractController
$whatStrucOrData = $GLOBALS[$what . '_structure_or_data'];
+ if ($export_type === 'raw') {
+ $whatStrucOrData = 'raw';
+ }
+
/**
* Builds the dump
*/
@@ -556,6 +569,15 @@ final class ExportController extends AbstractController
$separate_files
);
}
+ } elseif ($export_type === 'raw') {
+ Export::exportRaw(
+ $whatStrucOrData,
+ $export_plugin,
+ $crlf,
+ $err_url,
+ $sql_query,
+ $export_type
+ );
} else {
// We export just one table
// $allrows comes from the form when "Dump all rows" has been selected
diff --git a/libraries/classes/Display/Export.php b/libraries/classes/Display/Export.php
index 7a89c52605..74af369ab1 100644
--- a/libraries/classes/Display/Export.php
+++ b/libraries/classes/Display/Export.php
@@ -552,7 +552,7 @@ class Export
$html .= $this->getHtmlForOptionsSelection($exportType, $multiValues);
$tableObject = new Table($table, $db);
- if (strlen($table) > 0 && empty($numTables) && ! $tableObject->isMerge()) {
+ if (strlen($table) > 0 && empty($numTables) && ! $tableObject->isMerge() && $exportType !== 'raw') {
$html .= $this->getHtmlForOptionsRows($db, $table, $unlimNumRows);
}
diff --git a/libraries/classes/Display/Results.php b/libraries/classes/Display/Results.php
index b3a3c83a5b..90b5ef7b0e 100644
--- a/libraries/classes/Display/Results.php
+++ b/libraries/classes/Display/Results.php
@@ -4933,6 +4933,12 @@ class Results
$_url_params['single_table'] = 'true';
}
+ // In case this query doesn't involve any tables,
+ // implies only raw query is to be exported
+ if (! $analyzed_sql_results['select_tables']) {
+ $_url_params['raw_query'] = 'true';
+ }
+
if (! $header_shown) {
$results_operations_html .= $header;
$header_shown = true;
diff --git a/libraries/classes/Export.php b/libraries/classes/Export.php
index de0862c483..0049c590a2 100644
--- a/libraries/classes/Export.php
+++ b/libraries/classes/Export.php
@@ -279,6 +279,52 @@ class Export
return $memory_limit;
}
+ /**
+ * 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
+ $extensionStartPos = mb_strlen($filename) - mb_strlen(
+ $exportPlugin->getProperties()->getExtension()
+ ) - 1;
+ $userExtension = mb_substr(
+ $filename,
+ $extensionStartPos,
+ mb_strlen($filename)
+ );
+ $requiredExtension = '.' . $exportPlugin->getProperties()->getExtension();
+ if (mb_strtolower($userExtension) != $requiredExtension) {
+ $filename .= $requiredExtension;
+ }
+ $mime_type = $exportPlugin->getProperties()->getMimeType();
+
+ // If dump is going to be compressed, set correct mime_type and add
+ // compression to extension
+ if ($compression === 'gzip') {
+ $filename .= '.gz';
+ $mime_type = 'application/x-gzip';
+ } elseif ($compression === 'zip') {
+ $filename .= '.zip';
+ $mime_type = 'application/zip';
+ }
+ return [
+ $filename,
+ $mime_type,
+ ];
+ }
+
/**
* Return the filename and MIME type for export file
*
@@ -313,6 +359,14 @@ class Export
$filename_template
);
}
+ } elseif ($export_type == 'raw') {
+ if (! empty($remember_template)) {
+ $GLOBALS['PMA_Config']->setUserValue(
+ 'pma_raw_filename_template',
+ 'Export/file_template_raw',
+ $filename_template
+ );
+ }
} else {
if (! empty($remember_template)) {
$GLOBALS['PMA_Config']->setUserValue(
@@ -325,38 +379,13 @@ class Export
$filename = Util::expandUserString($filename_template);
// 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, $replaceDots = true);
+ $filename = Sanitize::sanitizeFilename($filename, true);
- // 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
- $extension_start_pos = mb_strlen($filename) - mb_strlen(
- $export_plugin->getProperties()->getExtension()
- ) - 1;
- $user_extension = mb_substr(
- $filename,
- $extension_start_pos,
- mb_strlen($filename)
+ return $this->getFinalFilenameAndMimetypeForFilename(
+ $export_plugin,
+ $compression,
+ $filename
);
- $required_extension = '.' . $export_plugin->getProperties()->getExtension();
- if (mb_strtolower($user_extension) != $required_extension) {
- $filename .= $required_extension;
- }
- $mime_type = $export_plugin->getProperties()->getMimeType();
-
- // If dump is going to be compressed, set correct mime_type and add
- // compression to extension
- if ($compression == 'gzip') {
- $filename .= '.gz';
- $mime_type = 'application/x-gzip';
- } elseif ($compression == 'zip') {
- $filename .= '.zip';
- $mime_type = 'application/zip';
- }
- return [
- $filename,
- $mime_type,
- ];
}
/**
@@ -890,6 +919,41 @@ class Export
}
}
+ /**
+ * Export raw query
+ *
+ * @param string $whatStrucOrData whether to export structure for each table or raw
+ * @param ExportPlugin $export_plugin the selected export plugin
+ * @param string $crlf end of line character(s)
+ * @param string $err_url the URL in case of error
+ * @param string $sql_query the query to be executed
+ * @param string $export_type the export type
+ *
+ * @return void
+ */
+ public static function exportRaw(
+ string $whatStrucOrData,
+ ExportPlugin $export_plugin,
+ string $crlf,
+ string $err_url,
+ string $sql_query,
+ string $export_type
+ ): void {
+ // In case the we need to dump just the raw query
+ if ($whatStrucOrData === 'raw') {
+ if (! $export_plugin->exportRawQuery(
+ $err_url,
+ $sql_query,
+ $crlf
+ )) {
+ $GLOBALS['message'] = Message::error(
+ __('Exporting a raw query is not supported for this export method.')
+ );
+ return;
+ }
+ }
+ }
+
/**
* Export at the table level
*
diff --git a/libraries/classes/Plugins/Export/ExportCsv.php b/libraries/classes/Plugins/Export/ExportCsv.php
index 5d8aa848c7..317fa1a641 100644
--- a/libraries/classes/Plugins/Export/ExportCsv.php
+++ b/libraries/classes/Plugins/Export/ExportCsv.php
@@ -338,4 +338,18 @@ class ExportCsv extends ExportPlugin
return true;
}
+
+ /**
+ * Outputs result of raw query in CSV format
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->exportData('', '', $crlf, $err_url, $sql_query);
+ }
}
diff --git a/libraries/classes/Plugins/Export/ExportJson.php b/libraries/classes/Plugins/Export/ExportJson.php
index 826c1cdd2f..f8588bebf5 100644
--- a/libraries/classes/Plugins/Export/ExportJson.php
+++ b/libraries/classes/Plugins/Export/ExportJson.php
@@ -287,4 +287,74 @@ class ExportJson extends ExportPlugin
return true;
}
+
+ /**
+ * Outputs result raw query in JSON format
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ $buffer = $this->encode(
+ [
+ 'type' => 'raw',
+ 'data' => '@@DATA@@',
+ ]
+ );
+ list($header, $footer) = explode('"@@DATA@@"', $buffer);
+
+ if (! $this->export->outputHandler($header . $crlf . '[' . $crlf)) {
+ return false;
+ }
+
+ $result = $GLOBALS['dbi']->query(
+ $sql_query,
+ DatabaseInterface::CONNECT_USER,
+ DatabaseInterface::QUERY_UNBUFFERED
+ );
+ $columns_cnt = $GLOBALS['dbi']->numFields($result);
+
+ $columns = [];
+ for ($i = 0; $i < $columns_cnt; $i++) {
+ $col_as = $GLOBALS['dbi']->fieldName($result, $i);
+ $columns[$i] = stripslashes($col_as);
+ }
+
+ $record_cnt = 0;
+ while ($record = $GLOBALS['dbi']->fetchRow($result)) {
+ $record_cnt++;
+
+ if ($record_cnt > 1) {
+ if (! $this->export->outputHandler(',' . $crlf)) {
+ return false;
+ }
+ }
+
+ $data = [];
+
+ for ($i = 0; $i < $columns_cnt; $i++) {
+ $data[$columns[$i]] = $record[$i];
+ }
+
+ $encodedData = $this->encode($data);
+ if (! $encodedData) {
+ return false;
+ }
+ if (! $this->export->outputHandler($encodedData)) {
+ return false;
+ }
+ }
+
+ if (! $this->export->outputHandler($crlf . ']' . $crlf . $footer . $crlf)) {
+ return false;
+ }
+
+ $GLOBALS['dbi']->freeResult($result);
+
+ return true;
+ }
}
diff --git a/libraries/classes/Plugins/Export/ExportLatex.php b/libraries/classes/Plugins/Export/ExportLatex.php
index 0a8079d7d0..11c88c648d 100644
--- a/libraries/classes/Plugins/Export/ExportLatex.php
+++ b/libraries/classes/Plugins/Export/ExportLatex.php
@@ -439,6 +439,20 @@ class ExportLatex extends ExportPlugin
return true;
} // end getTableLaTeX
+ /**
+ * Outputs result raw query
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the seperator for a file
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->exportData('', '', $crlf, $err_url, $sql_query);
+ }
+
/**
* Outputs table's structure
*
diff --git a/libraries/classes/Plugins/Export/ExportMediawiki.php b/libraries/classes/Plugins/Export/ExportMediawiki.php
index 0012af3246..ddd88844c7 100644
--- a/libraries/classes/Plugins/Export/ExportMediawiki.php
+++ b/libraries/classes/Plugins/Export/ExportMediawiki.php
@@ -289,7 +289,9 @@ class ExportMediawiki extends ExportPlugin
// Print data comment
$output = $this->_exportComment(
- 'Table data for ' . Util::backquote($table_alias)
+ $table_alias != ''
+ ? 'Table data for ' . Util::backquote($table_alias)
+ : 'Query results'
);
// Begin the table construction
@@ -348,6 +350,20 @@ class ExportMediawiki extends ExportPlugin
return $this->export->outputHandler($output);
}
+ /**
+ * Outputs result raw query in MediaWiki format
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->exportData('', '', $crlf, $err_url, $sql_query);
+ }
+
/**
* Outputs comments containing info about the exported tables
*
diff --git a/libraries/classes/Plugins/Export/ExportOds.php b/libraries/classes/Plugins/Export/ExportOds.php
index ad84f84c3f..91f04afe1f 100644
--- a/libraries/classes/Plugins/Export/ExportOds.php
+++ b/libraries/classes/Plugins/Export/ExportOds.php
@@ -337,4 +337,18 @@ class ExportOds extends ExportPlugin
return true;
}
+
+ /**
+ * Outputs result raw query in ODS format
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->exportData('', '', $crlf, $err_url, $sql_query);
+ }
}
diff --git a/libraries/classes/Plugins/Export/ExportOdt.php b/libraries/classes/Plugins/Export/ExportOdt.php
index 32f346904b..b01584549f 100644
--- a/libraries/classes/Plugins/Export/ExportOdt.php
+++ b/libraries/classes/Plugins/Export/ExportOdt.php
@@ -260,9 +260,12 @@ class ExportOdt extends ExportPlugin
$GLOBALS['odt_buffer']
.= ''
- . __('Dumping data for table') . ' ' . htmlspecialchars($table_alias)
- . ''
+ . ' text:is-list-header="true">';
+ $table_alias != ''
+ ? $GLOBALS['odt_buffer'] .= __('Dumping data for table') . ' ' . htmlspecialchars($table_alias)
+ : $GLOBALS['odt_buffer'] .= __('Dumping data for query result');
+ $GLOBALS['odt_buffer']
+ .= ''
. ''
. 'exportData('', '', $crlf, $err_url, $sql_query);
+ }
+
/**
* Returns a stand-in CREATE definition to resolve view dependencies
*
diff --git a/libraries/classes/Plugins/Export/ExportPdf.php b/libraries/classes/Plugins/Export/ExportPdf.php
index 4a73b76051..5cfd5c4282 100644
--- a/libraries/classes/Plugins/Export/ExportPdf.php
+++ b/libraries/classes/Plugins/Export/ExportPdf.php
@@ -232,6 +232,29 @@ class ExportPdf extends ExportPlugin
return true;
} // end of the 'PMA_exportData()' function
+ /**
+ * Outputs result of raw query in PDF format
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ $pdf = $this->_getPdf();
+ $attr = [
+ 'dbAlias' => '----',
+ 'tableAlias' => '----',
+ 'purpose' => __('Query result data'),
+ ];
+ $pdf->setAttributes($attr);
+ $pdf->mysqlReport($sql_query);
+
+ return true;
+ }
+
/**
* Outputs table structure
*
diff --git a/libraries/classes/Plugins/Export/ExportPhparray.php b/libraries/classes/Plugins/Export/ExportPhparray.php
index 60f6a4cffb..9a436fd457 100644
--- a/libraries/classes/Plugins/Export/ExportPhparray.php
+++ b/libraries/classes/Plugins/Export/ExportPhparray.php
@@ -249,4 +249,18 @@ class ExportPhparray extends ExportPlugin
return true;
}
+
+ /**
+ * Outputs result of raw query as PHP array
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->exportData('', '', $crlf, $err_url, $sql_query);
+ }
}
diff --git a/libraries/classes/Plugins/Export/ExportSql.php b/libraries/classes/Plugins/Export/ExportSql.php
index 1bf95ef2e4..472d13e6a1 100644
--- a/libraries/classes/Plugins/Export/ExportSql.php
+++ b/libraries/classes/Plugins/Export/ExportSql.php
@@ -90,6 +90,13 @@ class ExportSql extends ExportPlugin
$hide_sql = true;
}
+ // In case we have `raw_query` parameter set,
+ // we initialize SQL option
+ if (isset($_REQUEST['raw_query'])) {
+ $hide_structure = false;
+ $hide_sql = false;
+ }
+
if (! $hide_sql) {
$exportPluginProperties = new ExportPluginProperties();
$exportPluginProperties->setText('SQL');
@@ -2006,6 +2013,20 @@ class ExportSql extends ExportPlugin
return $schema_create;
} // end of the '_getTableComments()' function
+ /**
+ * Outputs raw query
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the seperator for a file
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->export->outputHandler($sql_query);
+ }
+
/**
* Outputs table's structure
*
diff --git a/libraries/classes/Plugins/Export/ExportTexytext.php b/libraries/classes/Plugins/Export/ExportTexytext.php
index 6cee2ba2c4..3fc369c573 100644
--- a/libraries/classes/Plugins/Export/ExportTexytext.php
+++ b/libraries/classes/Plugins/Export/ExportTexytext.php
@@ -186,7 +186,9 @@ class ExportTexytext extends ExportPlugin
$this->initAlias($aliases, $db_alias, $table_alias);
if (! $this->export->outputHandler(
- '== ' . __('Dumping data for table') . ' ' . $table_alias . "\n\n"
+ $table_alias != ''
+ ? '== ' . __('Dumping data for table') . ' ' . $table_alias . "\n\n"
+ : '==' . __('Dumping data for query result') . "\n\n"
)
) {
return false;
@@ -245,6 +247,20 @@ class ExportTexytext extends ExportPlugin
return true;
}
+ /**
+ * Outputs result raw query in TexyText format
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->exportData('', '', $crlf, $err_url, $sql_query);
+ }
+
/**
* Returns a stand-in CREATE definition to resolve view dependencies
*
diff --git a/libraries/classes/Plugins/Export/ExportYaml.php b/libraries/classes/Plugins/Export/ExportYaml.php
index 1098e17d2d..2e6b126f81 100644
--- a/libraries/classes/Plugins/Export/ExportYaml.php
+++ b/libraries/classes/Plugins/Export/ExportYaml.php
@@ -219,4 +219,18 @@ class ExportYaml extends ExportPlugin
return true;
} // end getTableYAML
+
+ /**
+ * Outputs result raw query in YAML format
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the end of line sequence
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(string $err_url, string $sql_query, string $crlf): bool
+ {
+ return $this->exportData('', '', $crlf, $err_url, $sql_query);
+ }
}
diff --git a/libraries/classes/Plugins/ExportPlugin.php b/libraries/classes/Plugins/ExportPlugin.php
index ee7b93a7bf..1d82a4282e 100644
--- a/libraries/classes/Plugins/ExportPlugin.php
+++ b/libraries/classes/Plugins/ExportPlugin.php
@@ -139,6 +139,23 @@ abstract class ExportPlugin
return true;
}
+ /**
+ * Outputs for raw query
+ *
+ * @param string $err_url the url to go back in case of error
+ * @param string $sql_query the rawquery to output
+ * @param string $crlf the seperator for a file
+ *
+ * @return bool if succeeded
+ */
+ public function exportRawQuery(
+ string $err_url,
+ string $sql_query,
+ string $crlf
+ ): bool {
+ return false;
+ }
+
/**
* Outputs table's structure
*
diff --git a/templates/display/export/option_header.twig b/templates/display/export/option_header.twig
index 579a17315e..a7e614eac3 100644
--- a/templates/display/export/option_header.twig
+++ b/templates/display/export/option_header.twig
@@ -5,6 +5,8 @@
{% trans 'Exporting databases from the current server' %}
{% elseif export_type == 'database' %}
{{ 'Exporting tables from "%s" database'|trans|format(db) }}
+ {% elseif export_type == 'raw' %}
+ {% trans 'Exporting raw query' %}
{% else %}
{{ 'Exporting rows from "%s" table'|trans|format(table) }}
{% endif %}
diff --git a/test/classes/ExportTest.php b/test/classes/ExportTest.php
index 27f3b46b68..caee5f0341 100644
--- a/test/classes/ExportTest.php
+++ b/test/classes/ExportTest.php
@@ -7,6 +7,7 @@ declare(strict_types=1);
namespace PhpMyAdmin\Tests;
use PhpMyAdmin\Export;
+use PhpMyAdmin\Plugins\Export\ExportPhparray;
use PHPUnit\Framework\TestCase;
/**
@@ -103,4 +104,29 @@ class ExportTest extends TestCase
$actual = $this->export->mergeAliases($aliases1, $aliases2);
$this->assertEquals($expected, $actual);
}
+
+ /**
+ * Test for getFinalFilenameAndMimetypeForFilename
+ *
+ * @return void
+ */
+ public function testGetFinalFilenameAndMimetypeForFilename()
+ {
+ $exportPlugin = new ExportPhparray();
+ $finalFileName = $this->export->getFinalFilenameAndMimetypeForFilename($exportPlugin, 'zip', 'myfilename');
+ $this->assertSame([
+ 'myfilename.php.zip',
+ 'application/zip',
+ ], $finalFileName);
+ $finalFileName = $this->export->getFinalFilenameAndMimetypeForFilename($exportPlugin, 'gzip', 'myfilename');
+ $this->assertSame([
+ 'myfilename.php.gz',
+ 'application/x-gzip',
+ ], $finalFileName);
+ $finalFileName = $this->export->getFinalFilenameAndMimetypeForFilename($exportPlugin, 'gzip', 'export.db1.table1.file');
+ $this->assertSame([
+ 'export.db1.table1.file.php.gz',
+ 'application/x-gzip',
+ ], $finalFileName);
+ }
}