Merge branch 'QA_5_1'

Signed-off-by: William Desportes <williamdes@wdes.fr>
This commit is contained in:
William Desportes 2021-04-23 18:32:45 +02:00
commit d82e85f852
No known key found for this signature in database
GPG Key ID: 90A0EF1B8251A889
9 changed files with 158 additions and 56 deletions

View File

@ -93,6 +93,10 @@ phpMyAdmin - ChangeLog
- issue Fixed "data is undefined" JS error
- issue Fixed 2 un-translated buttons on central columns edit
- issue #16810 Fixed SQL query shown twice on central columns actions
- issue #16771 Fixed PHP array export to work on very large datasets
- issue #16847 Fixed JSON export nullable binary PHP error
- issue #16847 Fixed JSON export text fields and binary data
- issue #14773 Fix exporting a raw query is not working
5.1.0 (2021-02-24)
- issue #15350 Change Media (MIME) type references to Media type

View File

@ -6,7 +6,7 @@ AJAX.registerOnload('export_output.js', function () {
}
});
$('#export_refresh_btn').on('click', function (e) {
$('.export_refresh_btn').on('click', function (e) {
e.preventDefault();
$('#export_refresh_form').trigger('submit');
});

View File

@ -123,8 +123,14 @@ class ExportController extends AbstractController
return;
}
$exportType = 'table';
$isReturnBackFromRawExport = isset($_POST['export_type']) && $_POST['export_type'] === 'raw';
if (isset($_POST['raw_query']) || $isReturnBackFromRawExport) {
$exportType = 'raw';
}
$options = $this->export->getOptions(
'table',
$exportType,
$db,
$table,
$sql_query,
@ -134,6 +140,7 @@ class ExportController extends AbstractController
);
$this->render('table/export/index', array_merge($options, [
'export_type' => $exportType,
'page_settings_error_html' => $pageSettingsErrorHtml,
'page_settings_html' => $pageSettingsHtml,
'message' => $message,

View File

@ -623,7 +623,7 @@ class Export
$html .= $backButton;
$refreshButton = '<form id="export_refresh_form" method="POST" action="'
. Url::getFromRoute('/export') . '" class="disableAjax">';
$refreshButton .= '[ <a class="disableAjax" id="export_refresh_btn">' . __('Refresh') . '</a> ]';
$refreshButton .= '[ <a class="disableAjax export_refresh_btn">' . __('Refresh') . '</a> ]';
foreach ($_POST as $name => $value) {
if (is_array($value)) {
foreach ($value as $val) {

View File

@ -238,6 +238,42 @@ class ExportJson extends ExportPlugin
'data' => '@@DATA@@',
]
);
return $this->doExportForQuery(
$dbi,
$sqlQuery,
$buffer,
$crlf,
$aliases,
$db,
$table
);
}
/**
* Export to JSON
*
* @phpstan-param array{
* string: array{
* 'tables': array{
* string: array{
* 'columns': array{string: string}
* }
* }
* }
* }|array|null $aliases
*
* @return bool False on export fail and true on export end success
*/
protected function doExportForQuery(
DatabaseInterface $dbi,
string $sqlQuery,
string $buffer,
string $crlf,
?array $aliases,
?string $db,
?string $table
): bool {
[$header, $footer] = explode('"@@DATA@@"', $buffer);
if (! $this->export->outputHandler($header . $crlf . '[' . $crlf)) {
@ -255,7 +291,10 @@ class ExportJson extends ExportPlugin
$columns = [];
for ($i = 0; $i < $columns_cnt; $i++) {
$col_as = $dbi->fieldName($result, $i);
if (! empty($aliases[$db]['tables'][$table]['columns'][$col_as])) {
if (
$db !== null && $table !== null && $aliases !== null
&& ! empty($aliases[$db]['tables'][$table]['columns'][$col_as])
) {
$col_as = $aliases[$db]['tables'][$table]['columns'][$col_as];
}
@ -276,9 +315,22 @@ class ExportJson extends ExportPlugin
$data = [];
for ($i = 0; $i < $columns_cnt; $i++) {
// 63 is the binary charset, see: https://dev.mysql.com/doc/internals/en/charsets.html
$isBlobAndIsBinaryCharset = isset($fieldsMeta[$i])
&& $fieldsMeta[$i]->isType(FieldMetadata::TYPE_BLOB)
&& $fieldsMeta[$i]->charsetnr === 63;
// This can occur for binary fields
$isBinaryString = isset($fieldsMeta[$i])
&& $fieldsMeta[$i]->isType(FieldMetadata::TYPE_STRING)
&& $fieldsMeta[$i]->charsetnr === 63;
if (
isset($fieldsMeta[$i]) && ($fieldsMeta[$i]->isMappedTypeGeometry
|| $fieldsMeta[$i]->isType(FieldMetadata::TYPE_BLOB))
isset($fieldsMeta[$i]) &&
(
$fieldsMeta[$i]->isMappedTypeGeometry ||
$isBlobAndIsBinaryCharset ||
$isBinaryString
) &&
$record[$i] !== null
) {
// export GIS and blob types as hex
$record[$i] = '0x' . bin2hex($record[$i]);
@ -325,57 +377,15 @@ class ExportJson extends ExportPlugin
'data' => '@@DATA@@',
]
);
[$header, $footer] = explode('"@@DATA@@"', $buffer);
if (! $this->export->outputHandler($header . $crlf . '[' . $crlf)) {
return false;
}
$result = $dbi->query(
return $this->doExportForQuery(
$dbi,
$sqlQuery,
DatabaseInterface::CONNECT_USER,
DatabaseInterface::QUERY_UNBUFFERED
$buffer,
$crlf,
null,
null,
null
);
$columns_cnt = $dbi->numFields($result);
$columns = [];
for ($i = 0; $i < $columns_cnt; $i++) {
$col_as = $dbi->fieldName($result, $i);
$columns[$i] = stripslashes($col_as);
}
$record_cnt = 0;
while ($record = $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;
}
$dbi->freeResult($result);
return true;
}
}

View File

@ -229,7 +229,12 @@ class ExportPhparray extends ExportPlugin
. $this->commentString(Util::backquote($db_alias)) . '.'
. $this->commentString(Util::backquote($table_alias)) . ' */' . $crlf;
$buffer .= '$' . $tablefixed . ' = array(';
if (! $this->export->outputHandler($buffer)) {
return false;
}
// Reset the buffer
$buffer = '';
while ($record = $dbi->fetchRow($result)) {
$record_cnt++;
@ -246,6 +251,12 @@ class ExportPhparray extends ExportPlugin
}
$buffer .= ')';
if (! $this->export->outputHandler($buffer)) {
return false;
}
// Reset the buffer
$buffer = '';
}
$buffer .= $crlf . ');' . $crlf;

View File

@ -2,7 +2,14 @@
{% block message %}{{ message|raw }}{% endblock %}
{% block title %}{{ 'Exporting rows from "%s" table'|trans|format(table) }}{% endblock %}
{% block title %}
{% if export_type == 'raw' %}
{% trans %}Exporting a raw query{% notes %}A query that the user has written freely{% endtrans %}
{% else %}
{{ 'Exporting rows from "%s" table'|trans|format(table) }}
{% endif %}
{% endblock %}
{% set filename_hint %}
{% trans '@SERVER@ will become the server name, @DATABASE@ will become the database name and @TABLE@ will become the table name.' %}

View File

@ -199,4 +199,51 @@ class ExportJsonTest extends AbstractTestCase
'SELECT * FROM `test_db`.`test_table`;'
));
}
public function testExportComplexData(): void
{
// normalString binaryField textField blobField
$this->expectOutputString(
'{"type":"table","name":"test_table_complex","database":"test_db","data":'
. "\n[\n"
. '{"f1":"\"\'\"><iframe onload=alert(1)>\u0448\u0435\u043b\u043b\u044b",'
. '"f2":"0x3078313233343638353766656665",'
. '"f3":"My awesome\nText","f4":"0x307861663132333466363863353766656665"},' . "\n"
. '{"f1":null,"f2":null,"f3":null,"f4":null},' . "\n"
. '{"f1":"","f2":"0x307831","f3":"\u0448\u0435\u043b\u043b\u044b","f4":"0x307832"}' . "\n"
. "]\n}\n"
);
$this->assertTrue(
$this->object->exportData(
'test_db',
'test_table_complex',
"\n",
'example.com',
'SELECT * FROM `test_db`.`test_table_complex`;'
)
);
}
public function testExportRawComplexData(): void
{
$this->expectOutputString(
'{"type":"raw","data":'
. "\n[\n"
. '{"f1":"\"\'\"><iframe onload=alert(1)>\u0448\u0435\u043b\u043b\u044b",'
. '"f2":"0x3078313233343638353766656665",'
. '"f3":"My awesome\nText","f4":"0x307861663132333466363863353766656665"},' . "\n"
. '{"f1":null,"f2":null,"f3":null,"f4":null},' . "\n"
. '{"f1":"","f2":"0x307831","f3":"\u0448\u0435\u043b\u043b\u044b","f4":"0x307832"}' . "\n"
. "]\n}\n"
);
$this->assertTrue(
$this->object->exportRawQuery(
'example.com',
'SELECT * FROM `test_db`.`test_table_complex`;',
"\n"
)
);
}
}

View File

@ -22,6 +22,7 @@ use function preg_replace;
use function str_replace;
use function trim;
use const MYSQLI_TYPE_BLOB;
use const MYSQLI_TYPE_DATETIME;
use const MYSQLI_TYPE_DECIMAL;
use const MYSQLI_TYPE_STRING;
@ -2404,6 +2405,21 @@ class DbiDummy implements DbiExtension
['3', 'Abcd', '2012-01-20 02:00:02'],
],
],
[
'query' => 'SELECT * FROM `test_db`.`test_table_complex`;',
'columns' => ['f1', 'f2', 'f3', 'f4'],
'result' => [
['"\'"><iframe onload=alert(1)>шеллы', '0x12346857fefe', "My awesome\nText", '0xaf1234f68c57fefe'],
[null, null, null, null],
['', '0x1', 'шеллы', '0x2'],
],
'metadata' => [
new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) ['charsetnr' => 33]),
new FieldMetadata(MYSQLI_TYPE_STRING, 0, (object) ['charsetnr' => 63]),
new FieldMetadata(MYSQLI_TYPE_BLOB, 0, (object) ['charsetnr' => 23]),
new FieldMetadata(MYSQLI_TYPE_BLOB, 0, (object) ['charsetnr' => 63]),
],
],
[
'query' => 'SHOW PROCEDURE STATUS;',
'columns' => ['Db', 'Name', 'Type'],