diff --git a/.github/workflows/test-selenium.yml b/.github/workflows/test-selenium.yml index 9b77649732..3b982f8c89 100644 --- a/.github/workflows/test-selenium.yml +++ b/.github/workflows/test-selenium.yml @@ -13,11 +13,28 @@ permissions: env: php-version: "8.2" + node-version: "20" jobs: + generate-matrix: + name: Generate the matrix of Selenium tests + if: "!contains(github.event.head_commit.message, '[ci selenium skip]')" + runs-on: ubuntu-latest + outputs: + tests: ${{ steps.tests-list.outputs.tests }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + - id: tests-list + name: Send test names to outputs + run: | + set -euo pipefail + TEST_NAMES="[$(find $PWD/tests/end-to-end/ -type f -name '*Test.php' -print |sed -E "s,$PWD/tests/end-to-end/(.*)Test.php,{\"test-name\": \"\1\"}," | paste -sd ",")]" + echo "tests=$(printf "$TEST_NAMES")" >> $GITHUB_OUTPUT selenium: name: "Selenium" if: "!contains(github.event.head_commit.message, '[ci selenium skip]')" + needs: generate-matrix runs-on: "ubuntu-latest" services: database-server: @@ -58,28 +75,7 @@ jobs: strategy: fail-fast: false matrix: - test-name: - - "ChangePassword" - - "CreateDropDatabase" - - "CreateRemoveUser" - - "Database/Events" - - "Database/Operations" - - "Database/Procedures" - - "Database/Structure" - - "Export" - - "Import" - - "Login" - - "Normalization" - - "ServerSettings" - - "SqlQuery" - - "Table/Browse" - - "Table/Create" - - "Table/Insert" - - "Table/Operations" - - "Table/Structure" - - "Tracking" - - "Triggers" - - "Xss" + include: ${{ fromJSON(needs.generate-matrix.outputs.tests) }} steps: - name: Checkout code uses: actions/checkout@v6 @@ -103,7 +99,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '${{ env.node-version }}' cache: 'yarn' - name: Install modules diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d4d87b5d1a..25d2136001 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -7092,7 +7092,7 @@ parameters: - message: '#^Cannot cast mixed to string\.$#' identifier: cast.string - count: 6 + count: 7 path: src/Import/Import.php - @@ -7125,6 +7125,12 @@ parameters: count: 1 path: src/Import/Import.php + - + message: '#^Only booleans are allowed in a negated boolean, int\|false given\.$#' + identifier: booleanNot.exprNotBoolean + count: 1 + path: src/Import/Import.php + - message: '#^Parameter \#1 \$precision of static method PhpMyAdmin\\Import\\DecimalSize\:\:fromPrecisionAndScale\(\) expects int, int\|PhpMyAdmin\\Import\\DecimalSize given\.$#' identifier: argument.type @@ -8634,24 +8640,6 @@ parameters: count: 1 path: src/Plugins/Export/ExportJson.php - - - message: '#^Cannot call method fetchAssoc\(\) on PhpMyAdmin\\Dbal\\ResultInterface\|false\.$#' - identifier: method.nonObject - count: 1 - path: src/Plugins/Export/ExportLatex.php - - - - message: '#^Cannot call method getFieldNames\(\) on PhpMyAdmin\\Dbal\\ResultInterface\|false\.$#' - identifier: method.nonObject - count: 1 - path: src/Plugins/Export/ExportLatex.php - - - - message: '#^Cannot call method numFields\(\) on PhpMyAdmin\\Dbal\\ResultInterface\|false\.$#' - identifier: method.nonObject - count: 1 - path: src/Plugins/Export/ExportLatex.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 60d4bc6e94..19d70caa63 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1316,6 +1316,7 @@ + @@ -4048,9 +4049,6 @@ - - - @@ -4973,6 +4971,9 @@ scale]]> scale]]> + + + @@ -5820,16 +5821,12 @@ - - - - - + config->selectedServer['port'])]]> @@ -5882,6 +5879,11 @@ + + + + + value]]> diff --git a/resources/js/designer/move.ts b/resources/js/designer/move.ts index 9883c95110..f92fb62b7f 100644 --- a/resources/js/designer/move.ts +++ b/resources/js/designer/move.ts @@ -510,6 +510,26 @@ const rect = function (x1, y1, w, h, color) { }; // --------------------------- FULLSCREEN ------------------------------------- +const requestFullscreen = function (e) { + if (e.requestFullscreen) { + return e.requestFullscreen(); + } else if (e.webkitRequestFullscreen) { + return e.webkitRequestFullscreen(); + } else if (e.mozRequestFullScreen) { + return e.mozRequestFullScreen(); + } else if (e.msRequestFullscreen) { + return e.msRequestFullscreen(); + } +}; + +const fullscreenEnabled = function () { + // eslint-disable-next-line compat/compat + return document.fullscreenEnabled || + document.webkitFullscreenEnabled || + document.mozFullScreenEnabled || + document.msFullscreenEnabled; +}; + const toggleFullscreen = function () { var valueSent = ''; var $img = $('#toggleFullscreen').find('img'); @@ -517,7 +537,7 @@ const toggleFullscreen = function () { var $content = $('#page_content'); const pageContent = document.getElementById('page_content'); - if (! document.fullscreenEnabled) { + if (! DesignerMove.fullscreenEnabled()) { ajaxShowMessage(window.Messages.strFullscreenRequestDenied, null, 'error'); return; @@ -534,7 +554,7 @@ const toggleFullscreen = function () { $('#osn_tab').css({ 'width': screen.width + 'px', 'height': screen.height }); valueSent = 'on'; - pageContent.requestFullscreen(); + DesignerMove.requestFullscreen(pageContent); } else { $img.attr('src', $img.data('enter')) .attr('title', $span.data('enter')); @@ -2077,6 +2097,8 @@ const DesignerMove = { clear: clear, rect: rect, toggleFullscreen: toggleFullscreen, + fullscreenEnabled: fullscreenEnabled, + requestFullscreen: requestFullscreen, addTableToTablesList: addTableToTablesList, displayModal: displayModal, addOtherDbTables: addOtherDbTables, diff --git a/resources/js/makegrid.ts b/resources/js/makegrid.ts index c5a7bdfb7c..11ffbb070f 100644 --- a/resources/js/makegrid.ts +++ b/resources/js/makegrid.ts @@ -8,7 +8,8 @@ import { getCellValue, stringifyJSON, toggleDatepickerIfInvalid, - updateCode + updateCode, + userAgent } from './modules/functions.ts'; import { CommonParams } from './modules/common.ts'; import highlightSql from './modules/sql-highlight.ts'; @@ -277,7 +278,7 @@ const makeGrid = function (t, enableResize = undefined, enableReorder = undefine for (var n = 0, l = $firstRowCols.length; n < l; n++) { var $col = $($firstRowCols[n]); var colWidth; - if (navigator.userAgent.toLowerCase().indexOf('safari') !== -1) { + if (userAgent().toLowerCase().indexOf('safari') !== -1) { colWidth = $col.outerWidth(); } else { colWidth = $col.outerWidth(true); @@ -301,6 +302,20 @@ const makeGrid = function (t, enableResize = undefined, enableReorder = undefine $(g.cRsz).css('height', $(g.t).height()); }, + /** + * Clears the current cell edit state, internal flags, + * and any pending save request. + */ + resetGridEditState: function () { + g.isCellEditActive = false; + g.isEditCellTextEditable = false; + g.currentEditCell = null; + g.wasEditedCellNull = false; + g.isCellEdited = false; + g.isSaving = false; + g.lastXHR = null; + }, + /** * Shift column from index oldn to newn. * @@ -2823,6 +2838,9 @@ const makeGrid = function (t, enableResize = undefined, enableReorder = undefine // some adjustment $(t).removeClass('data'); $(g.gDiv).addClass('data'); + /* Store the grid controller instance on the table element so it can be accessed later by other modules + (e.g. during AJAX teardown) without exposing the grid object as a global variable.*/ + $(t).data('pmaGrid', g); g.initCellSelection(); }; diff --git a/resources/js/modules/functions.ts b/resources/js/modules/functions.ts index 5652127a3a..79ead34e11 100644 --- a/resources/js/modules/functions.ts +++ b/resources/js/modules/functions.ts @@ -64,6 +64,19 @@ export function addNoCacheToAjaxRequests (options: JQuery.AjaxSettings, original } } +/** + * Get an empty string for user-agent, if undefined + */ +export function userAgent (): string { + try { + return navigator.userAgent; + } catch (e) { + console.error(e); + + return ''; + } +} + /** * Adds a date/time picker to an element * diff --git a/resources/js/modules/functions/event-loader.ts b/resources/js/modules/functions/event-loader.ts index a01602adfb..9b167fb9e7 100644 --- a/resources/js/modules/functions/event-loader.ts +++ b/resources/js/modules/functions/event-loader.ts @@ -38,7 +38,8 @@ import { teardownIdleEvent, teardownRecentFavoriteTables, teardownSortLinkMouseEvent, - teardownSqlQueryEditEvents + teardownSqlQueryEditEvents, + userAgent, } from '../functions.ts'; import handleCreateViewModal from './handleCreateViewModal.ts'; @@ -93,7 +94,7 @@ export function onloadFunctions () { /** * Add attribute to text boxes for iOS devices (based on bugID: 3508912) */ - if (navigator.userAgent.match(/(iphone|ipod|ipad)/i)) { + if (userAgent().match(/(iphone|ipod|ipad)/i)) { $('input[type=text]').attr('autocapitalize', 'off').attr('autocorrect', 'off'); } diff --git a/resources/js/modules/sql-highlight.ts b/resources/js/modules/sql-highlight.ts index 92cc0b21ad..2e848c2bfb 100644 --- a/resources/js/modules/sql-highlight.ts +++ b/resources/js/modules/sql-highlight.ts @@ -462,9 +462,9 @@ export default function highlightSql ($base) { var $pre = $sql.closest('pre'); /* We only care about visible elements to avoid double processing */ if ($sql.is(':visible')) { - var $highlight = $('
'); - $pre.append($highlight); if (typeof window.CodeMirror !== 'undefined') { + var $highlight = $('
'); + $pre.append($highlight); // @ts-ignore window.CodeMirror.runMode($sql.text(), 'text/x-mysql', $highlight[0]); $sql.hide(); diff --git a/resources/js/table/change.ts b/resources/js/table/change.ts index c1520d7734..3450eec4b5 100644 --- a/resources/js/table/change.ts +++ b/resources/js/table/change.ts @@ -505,6 +505,13 @@ AJAX.registerTeardown('table/change.js', function () { $(document).off('click', 'input.checkbox_null'); $('select[name="submit_type"]').off('change'); $(document).off('change', '#insert_rows'); + + // Reset grid edit state + var grid = $('table.table_results').data('pmaGrid'); + + if (grid && typeof grid.resetGridEditState === 'function') { + grid.resetGridEditState(); + } }); /** diff --git a/src/Controllers/HomeController.php b/src/Controllers/HomeController.php index 09d0596dae..92bed94182 100644 --- a/src/Controllers/HomeController.php +++ b/src/Controllers/HomeController.php @@ -343,6 +343,37 @@ final class HomeController implements InvocableController } } + /** + * Check if user does not have defined query encryption key and it is being used. + */ + if ($this->config->config->URLQueryEncryption) { + $encryptionKeyLength = 0; + $encryptionKeyLength = mb_strlen($this->config->config->URLQueryEncryptionSecretKey, '8bit'); + + if ($encryptionKeyLength < SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { + $this->errors[] = [ + 'message' => __( + 'The configuration file needs a valid key for query encryption.' + . ' A temporary key was automatically generated for you.' + . ' Please refer to the [doc@cfg_URLQueryEncryptionSecretKey]documentation[/doc].', + ), + 'severity' => 'warning', + ]; + } elseif ($encryptionKeyLength > SODIUM_CRYPTO_SECRETBOX_KEYBYTES) { + $this->errors[] = [ + 'message' => sprintf( + __( + 'The query encryption key in the configuration file is longer than necessary.' + . ' It should only be %d bytes long.' + . ' Please refer to the [doc@cfg_URLQueryEncryptionSecretKey]documentation[/doc].', + ), + SODIUM_CRYPTO_SECRETBOX_KEYBYTES, + ), + 'severity' => 'warning', + ]; + } + } + /** * Check for existence of config directory which should not exist in * production environment. diff --git a/src/Controllers/JavaScriptMessagesController.php b/src/Controllers/JavaScriptMessagesController.php index c6f3821f43..0671df4dd9 100644 --- a/src/Controllers/JavaScriptMessagesController.php +++ b/src/Controllers/JavaScriptMessagesController.php @@ -692,7 +692,10 @@ final class JavaScriptMessagesController implements InvocableController 'strHide' => __('Hide'), 'strShow' => __('Show'), 'strStructure' => __('Structure'), - 'strFullscreenRequestDenied' => __('The fullscreen request was denied.'), + 'strFullscreenRequestDenied' => __( + 'The fullscreen request was denied because' + . ' the Fullscreen API is disabled by user preference in the browser.', + ), 'strMonthNameJan' => _pgettext('Month name', 'January'), 'strMonthNameFeb' => _pgettext('Month name', 'February'), diff --git a/src/Error/ErrorReport.php b/src/Error/ErrorReport.php index 982f742332..b29083bee8 100644 --- a/src/Error/ErrorReport.php +++ b/src/Error/ErrorReport.php @@ -75,7 +75,7 @@ class ErrorReport 'browser_version' => $userAgentParser->getUserBrowserVersion(), 'user_os' => $userAgentParser->getClientPlatform(), 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? null, - 'user_agent_string' => $_SERVER['HTTP_USER_AGENT'], + 'user_agent_string' => $_SERVER['HTTP_USER_AGENT'] ?? null, 'locale' => $this->config->getCookie('pma_lang'), 'configuration_storage' => $relationParameters->db === null ? 'disabled' : 'enabled', 'php_version' => PHP_VERSION, diff --git a/src/Header.php b/src/Header.php index fabc460cbc..453388b2f7 100644 --- a/src/Header.php +++ b/src/Header.php @@ -217,10 +217,12 @@ class Header $themeManager = ContainerBuilder::getContainer()->get(ThemeManager::class); $theme = $themeManager->theme; $scripts = $this->getScripts(); + $userAgent = Core::getEnv('HTTP_USER_AGENT'); // The user preferences have been merged at this point // so we can conditionally add CodeMirror, other scripts and settings - if ($this->config->config->CodemirrorEnable) { + // See #20159, why we need to check for HTTP_USER_AGENT here + if ($this->config->config->CodemirrorEnable && $userAgent !== '') { $scripts->addFile('vendor/codemirror/lib/codemirror.js'); $scripts->addFile('vendor/codemirror/mode/sql/sql.js'); $scripts->addFile('vendor/codemirror/addon/runmode/runmode.js'); diff --git a/src/Html/Generator.php b/src/Html/Generator.php index f9ec9a566a..67b257affb 100644 --- a/src/Html/Generator.php +++ b/src/Html/Generator.php @@ -565,7 +565,7 @@ class Generator if ( ! empty($config->settings['SQLQuery']['Refresh']) && Sql::$showAsPhp === null // 'Submit query' does the same - && preg_match('@^(SELECT|SHOW)[[:space:]]+@i', $sqlQuery) === 1 + && preg_match('@^(ANALYZE|EXPLAIN|SELECT|SHOW)[[:space:]]+@i', $sqlQuery) === 1 ) { $refreshLink = Url::getFromRoute('/sql', $urlParams); $refreshLink = '
' @@ -954,6 +954,7 @@ class Generator // Has as sql_query without a signature, to be accepted it needs to be sent using POST str_contains($url, 'sql_query=') && ! str_contains($url, 'sql_signature=') ) + || $config->config->URLQueryEncryption || str_contains($url, 'view[as]='); if ($respectUrlLengthLimit && $isDataPostFormatSupported) { $parts = explode('?', $url, 2); diff --git a/src/Import/Import.php b/src/Import/Import.php index b35f52df3d..b9ae466545 100644 --- a/src/Import/Import.php +++ b/src/Import/Import.php @@ -847,7 +847,8 @@ final class Import if ($analyses !== null) { $isVarchar = $analyses[$tableIndex][$columnIndex]->type === ColumnType::Varchar; } else { - $isVarchar = ! is_numeric($row[$columnIndex]); + $isVarchar = ! is_numeric($row[$columnIndex]) + && ! preg_match('/^0x[0-9a-f]+$/', (string) $row[$columnIndex]); } /* Don't put quotes around NULL fields */ diff --git a/src/Plugins/Export/ExportCsv.php b/src/Plugins/Export/ExportCsv.php index 3d8144f6cb..2547706360 100644 --- a/src/Plugins/Export/ExportCsv.php +++ b/src/Plugins/Export/ExportCsv.php @@ -21,6 +21,7 @@ use PhpMyAdmin\Properties\Options\Items\TextPropertyItem; use PhpMyAdmin\Properties\Plugins\ExportPluginProperties; use function __; +use function bin2hex; use function implode; use function is_string; use function str_replace; @@ -139,6 +140,7 @@ class ExportCsv extends ExportPlugin array $aliases = [], ): void { $result = $this->dbi->query($sqlQuery, ConnectionType::User, DatabaseInterface::QUERY_UNBUFFERED); + $fieldsMeta = $this->dbi->getFieldsMeta($result); $charsNeedingEnclosure = $this->separator . $this->enclosed . $this->terminated; @@ -165,7 +167,7 @@ class ExportCsv extends ExportPlugin // Format the data while ($row = $result->fetchRow()) { $insertValues = []; - foreach ($row as $field) { + foreach ($row as $j => $field) { if ($field === null) { $insertValues[] = $this->null; continue; @@ -176,6 +178,11 @@ class ExportCsv extends ExportPlugin continue; } + if ($fieldsMeta[$j]->isMappedTypeGeometry || $fieldsMeta[$j]->isBinary) { + $insertValues[] = '0x' . bin2hex($row[$j] ?? ''); + continue; + } + // remove CRLF characters within field if ($this->removeCrLf) { $field = str_replace(["\r", "\n"], '', $field); diff --git a/src/Plugins/Export/ExportLatex.php b/src/Plugins/Export/ExportLatex.php index 7f42a6a94e..19f8ec591e 100644 --- a/src/Plugins/Export/ExportLatex.php +++ b/src/Plugins/Export/ExportLatex.php @@ -28,6 +28,8 @@ use function __; use function addcslashes; use function array_keys; use function array_values; +use function assert; +use function bin2hex; use function in_array; use function is_string; use function mb_strpos; @@ -272,8 +274,10 @@ class ExportLatex extends ExportPlugin $tableAlias = $this->getTableAlias($aliases, $db, $table); $result = $this->dbi->tryQuery($sqlQuery, ConnectionType::User, DatabaseInterface::QUERY_UNBUFFERED); + assert($result !== false); $columnsCnt = $result->numFields(); + $fieldsMeta = $this->dbi->getFieldsMeta($result); $columns = []; $columnsAlias = []; foreach ($result->getFieldNames() as $i => $colAs) { @@ -350,7 +354,13 @@ class ExportLatex extends ExportPlugin /** @infection-ignore-all */ for ($i = 0; $i < $columnsCnt; $i++) { if ($record[$columns[$i]] !== null) { - $columnValue = self::texEscape($record[$columns[$i]]); + if ($fieldsMeta[$i]->isMappedTypeGeometry || $fieldsMeta[$i]->isBinary) { + $columnValue = self::texEscape( + '0x' . bin2hex($record[$columns[$i]]), + ); + } else { + $columnValue = self::texEscape($record[$columns[$i]]); + } } else { $columnValue = $this->null; } diff --git a/src/Plugins/Export/ExportMediawiki.php b/src/Plugins/Export/ExportMediawiki.php index 955ec0a97e..334c123f52 100644 --- a/src/Plugins/Export/ExportMediawiki.php +++ b/src/Plugins/Export/ExportMediawiki.php @@ -23,6 +23,7 @@ use PhpMyAdmin\Util; use function __; use function array_values; +use function bin2hex; use function htmlspecialchars; use function str_repeat; @@ -226,6 +227,7 @@ class ExportMediawiki extends ExportPlugin // Get the table data from the database $result = $this->dbi->query($sqlQuery, ConnectionType::User, DatabaseInterface::QUERY_UNBUFFERED); $fieldsCnt = $result->numFields(); + $fieldsMeta = $this->dbi->getFieldsMeta($result); while ($row = $result->fetchRow()) { $output .= '|-' . $this->exportCRLF(); @@ -233,6 +235,12 @@ class ExportMediawiki extends ExportPlugin // Use '|' for separating table columns /** @infection-ignore-all */ for ($i = 0; $i < $fieldsCnt; ++$i) { + if (! isset($row[$i])) { + $row[$i] = 'NULL'; + } elseif ($fieldsMeta[$i]->isMappedTypeGeometry || $fieldsMeta[$i]->isBinary) { + $row[$i] = '0x' . bin2hex($row[$i]); + } + $output .= ' | ' . $row[$i] . $this->exportCRLF(); } } diff --git a/src/Plugins/Export/ExportPhparray.php b/src/Plugins/Export/ExportPhparray.php index 5bbe10a93e..15e3a9b411 100644 --- a/src/Plugins/Export/ExportPhparray.php +++ b/src/Plugins/Export/ExportPhparray.php @@ -21,6 +21,7 @@ use PhpMyAdmin\Util; use PhpMyAdmin\Version; use function __; +use function bin2hex; use function preg_match; use function preg_replace; use function strtr; @@ -127,6 +128,7 @@ class ExportPhparray extends ExportPlugin $result = $this->dbi->query($sqlQuery, ConnectionType::User, DatabaseInterface::QUERY_UNBUFFERED); $columnsCnt = $result->numFields(); + $fieldsMeta = $this->dbi->getFieldsMeta($result); $columns = []; foreach ($result->getFieldNames() as $i => $colAs) { $colAs = $this->getColumnAlias($aliases, $db, $table, $colAs); @@ -171,6 +173,10 @@ class ExportPhparray extends ExportPlugin /** @infection-ignore-all */ for ($i = 0; $i < $columnsCnt; $i++) { + if ($record[$i] !== null && ($fieldsMeta[$i]->isMappedTypeGeometry || $fieldsMeta[$i]->isBinary)) { + $record[$i] = '0x' . bin2hex($record[$i]); + } + $buffer .= var_export($columns[$i], true) . ' => ' . var_export($record[$i], true) . ($i + 1 >= $columnsCnt ? '' : ','); diff --git a/src/Plugins/Export/ExportTexytext.php b/src/Plugins/Export/ExportTexytext.php index ef908a5ef7..2665e8511b 100644 --- a/src/Plugins/Export/ExportTexytext.php +++ b/src/Plugins/Export/ExportTexytext.php @@ -25,6 +25,7 @@ use PhpMyAdmin\Triggers\Triggers; use PhpMyAdmin\Util; use function __; +use function bin2hex; use function htmlspecialchars; use function in_array; use function is_string; @@ -143,6 +144,7 @@ class ExportTexytext extends ExportPlugin * Gets the data from the database */ $result = $this->dbi->query($sqlQuery, ConnectionType::User, DatabaseInterface::QUERY_UNBUFFERED); + $fieldsMeta = $this->dbi->getFieldsMeta($result); // If required, get fields name at the first line if ($this->columns) { @@ -160,11 +162,15 @@ class ExportTexytext extends ExportPlugin // Format the data while ($row = $result->fetchRow()) { $textOutput = ''; - foreach ($row as $field) { + foreach ($row as $j => $field) { if ($field === null) { $value = $this->null; } elseif ($field !== '') { $value = $field; + + if ($fieldsMeta[$j]->isMappedTypeGeometry || $fieldsMeta[$j]->isBinary) { + $value = '0x' . bin2hex($value); + } } else { $value = ' '; } diff --git a/src/Plugins/Export/ExportXml.php b/src/Plugins/Export/ExportXml.php index ae43e648ba..70af2a6191 100644 --- a/src/Plugins/Export/ExportXml.php +++ b/src/Plugins/Export/ExportXml.php @@ -25,6 +25,7 @@ use PhpMyAdmin\Util; use PhpMyAdmin\Version; use function __; +use function bin2hex; use function htmlspecialchars; use function mb_substr; use function rtrim; @@ -402,6 +403,7 @@ class ExportXml extends ExportPlugin $result = $this->dbi->query($sqlQuery, ConnectionType::User, DatabaseInterface::QUERY_UNBUFFERED); $columnsCnt = $result->numFields(); + $fieldsMeta = $this->dbi->getFieldsMeta($result); $columns = $result->getFieldNames(); $buffer = ' + + + + + + + CREATE TABLE `test` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `binary` binary(16) NOT NULL, + `name` varchar(20) NOT NULL, + `shape` geometry DEFAULT NULL, + PRIMARY KEY (`id`) + ) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + + + + + + + + 1 + 0x10000000000000000000000000000000 + POLYGON + 0x00000000010300000001000000040000000000000000405f40000000000000000000000000008063400000000000006940000000000040664000000000004057400000000000405f400000000000000000 +
+ + 2 + 0x20000000000000000000000000000000 + MULTIPOLYGON + 0x00000000010600000002000000010300000001000000040000000000000000006140000000000000444000000000006062400000000000c0544000000000004064400000000000c0524000000000000061400000000000004440010300000001000000040000000000000000405a4000000000000000000000000000004c400000000000006940000000000080534000000000004057400000000000405a400000000000000000 +
+ + 3 + 0x30000000000000000000000000000000 + MULTIPOINT + 0x0000000001040000000400000001010000000000000000405f400000000000004940010100000000000000008063400000000000406f40010100000000000000004066400000000000e0614001010000000000000000e065400000000000005440 +
+ + 4 + 0x40000000000000000000000000000000 + MULTILINESTRING + 0x000000000105000000020000000102000000030000000000000000004240000000000080614000000000008047400000000000206d400000000000004f400000000000c052400102000000030000000000000000004240000000000000594000000000000031400000000000206d4000000000004066400000000000405740 +
+ + 5 + 0x50000000000000000000000000000000 + POINT + 0x00000000010100000000000000000059400000000000406f40 +
+ + 6 + 0x60000000000000000000000000000000 + LINESTRING + 0x0000000001020000000400000000000000008063400000000000003840000000000040664000000000000058400000000000606d4000000000000069400000000000a063400000000000c06140 +
+ + 7 + 0x70000000000000000000000000000000 + GEOMETRYCOLLECTION + 0x000000000107000000030000000101000000000000000000594000000000000059400102000000050000000000000000000000000000000000000000000000000059400000000000005940000000000000694000000000000069400000000000c072400000000000c07240000000000000794000000000000079400103000000020000000500000000000000008041400000000000002440000000000000244000000000000034400000000000002e40000000000000444000000000008046400000000000804640000000000080414000000000000024400400000000000000000034400000000000003e40000000000080414000000000008041400000000000003e40000000000000344000000000000034400000000000003e40 +
+ + 8 + 0x80000000000000000000000000000000 + TEST + NULL +
+
+
diff --git a/tests/unit/Plugins/Import/ImportXmlTest.php b/tests/unit/Plugins/Import/ImportXmlTest.php index 6bd2c5b8f5..10d03897fc 100644 --- a/tests/unit/Plugins/Import/ImportXmlTest.php +++ b/tests/unit/Plugins/Import/ImportXmlTest.php @@ -108,6 +108,24 @@ final class ImportXmlTest extends AbstractTestCase pma_bookmarktest (Structure) (Options) */ + //assert that all sql are executed + self::assertSame( + 'CREATE DATABASE IF NOT EXISTS `phpmyadmintest` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;' + . 'USE `phpmyadmintest`;' . "\n" + . 'CREATE TABLE IF NOT EXISTS `pma_bookmarktest` (' . "\n" + . ' `id` int(11) NOT NULL AUTO_INCREMENT,' . "\n" + . ' `dbase` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT \'\',' . "\n" + . ' `user` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT \'\',' . "\n" + . ' `label` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT \'\',' . "\n" + . ' `query` text COLLATE utf8_bin NOT NULL,' . "\n" + . ' PRIMARY KEY (`id`)' . "\n" + . ') ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\'Bookmarks\';' . "\n" + . ' ;' + . 'INSERT INTO `phpmyadmintest`.`pma_bookmarktest` (`id`, `dbase`, `user`, `label`, `query`) ' + . 'VALUES (, \'\', \'\', \'\', \'\');;', + Current::$sqlQuery, + ); + self::assertStringContainsString( 'The following structures have either been created or altered.', ImportSettings::$importNotice, @@ -126,4 +144,50 @@ final class ImportXmlTest extends AbstractTestCase return new ImportXml(new Import($dbiObject, new ResponseRenderer(), $config), $dbiObject, $config); } + + /** + * Test for doImport using second dataset + */ + #[RequiresPhpExtension('simplexml')] + public function testDoImportDataset2(): void + { + $dbi = $this->getMockBuilder(DatabaseInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $importHandle = new File(ImportSettings::$importFile); + $importHandle->open(); + + ImportSettings::$importFile = 'test/test_data/test.xml'; + + $importXml = $this->getImportXml($dbi); + $importXml->doImport($importHandle); + + self::assertSame( + 'CREATE DATABASE IF NOT EXISTS `phpmyadmintest` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;' + . 'USE `phpmyadmintest`;' . "\n" + . 'CREATE TABLE IF NOT EXISTS `pma_bookmarktest` (' . "\n" + . ' `id` int(11) NOT NULL AUTO_INCREMENT,' . "\n" + . ' `dbase` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT \'\',' . "\n" + . ' `user` varchar(255) COLLATE utf8_bin NOT NULL DEFAULT \'\',' . "\n" + . ' `label` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT \'\',' . "\n" + . ' `query` text COLLATE utf8_bin NOT NULL,' . "\n" + . ' PRIMARY KEY (`id`)' . "\n" + . ') ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=\'Bookmarks\';' . "\n" + . ' ;' + . 'INSERT INTO `phpmyadmintest`.`pma_bookmarktest` (`id`, `dbase`, `user`, `label`, `query`) ' + . 'VALUES (, \'\', \'\', \'\', \'\');;', + Current::$sqlQuery, + ); + + self::assertStringContainsString( + 'The following structures have either been created or altered.', + ImportSettings::$importNotice, + ); + self::assertStringContainsString('Go to database: `test`', ImportSettings::$importNotice); + self::assertStringContainsString('Edit settings for `test`', ImportSettings::$importNotice); + self::assertStringContainsString('Go to table: `test`', ImportSettings::$importNotice); + self::assertStringContainsString('Edit settings for `test`', ImportSettings::$importNotice); + self::assertTrue(ImportSettings::$finished); + } }