Merge branch 'QA_5_2'

Signed-off-by: William Desportes <williamdes@wdes.fr>
This commit is contained in:
William Desportes 2026-02-24 12:28:24 +01:00
commit 015d75c0d1
No known key found for this signature in database
GPG Key ID: 70684F4717D49A31
47 changed files with 387 additions and 129 deletions

View File

@ -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

View File

@ -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

View File

@ -1316,6 +1316,7 @@
</RiskyTruthyFalsyComparison>
<UnusedVariable>
<code><![CDATA[$collationsList]]></code>
<code><![CDATA[$encryptionKeyLength]]></code>
</UnusedVariable>
</file>
<file src="src/Controllers/Import/ImportController.php">
@ -4048,9 +4049,6 @@
<code><![CDATA[$_POST['url']]]></code>
<code><![CDATA[$exception['url']]]></code>
</PossiblyInvalidCast>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$_SERVER['HTTP_USER_AGENT']]]></code>
</PossiblyUndefinedArrayOffset>
<RiskyTruthyFalsyComparison>
<code><![CDATA[empty($_POST['description'])]]></code>
<code><![CDATA[empty($_POST['exception'])]]></code>
@ -4973,6 +4971,9 @@
<code><![CDATA[$lastCumulativeSize->scale]]></code>
<code><![CDATA[$lastCumulativeSize->scale]]></code>
</PossiblyInvalidPropertyFetch>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$row[$columnIndex]]]></code>
</PossiblyUndefinedArrayOffset>
<RiskyTruthyFalsyComparison>
<code><![CDATA[ImportSettings::$runQuery]]></code>
</RiskyTruthyFalsyComparison>
@ -5820,16 +5821,12 @@
<MixedOperand>
<code><![CDATA[$comments[$fieldName]]]></code>
</MixedOperand>
<PossiblyFalseReference>
<code><![CDATA[fetchAssoc]]></code>
<code><![CDATA[getFieldNames]]></code>
<code><![CDATA[numFields]]></code>
</PossiblyFalseReference>
<PossiblyInvalidOperand>
<code><![CDATA[str_replace('_', '/', $mimeMap[$fieldName]['mimetype'])]]></code>
</PossiblyInvalidOperand>
<PossiblyNullArgument>
<code><![CDATA[$record[$columns[$i]]]]></code>
<code><![CDATA[$record[$columns[$i]]]]></code>
</PossiblyNullArgument>
<RiskyTruthyFalsyComparison>
<code><![CDATA[empty($this->config->selectedServer['port'])]]></code>
@ -5882,6 +5879,11 @@
<code><![CDATA[$pdf]]></code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Plugins/Export/ExportPhparray.php">
<PossiblyNullArgument>
<code><![CDATA[$record[$i]]]></code>
</PossiblyNullArgument>
</file>
<file src="src/Plugins/Export/ExportSql.php">
<ArgumentTypeCoercion>
<code><![CDATA[$token->value]]></code>

View File

@ -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,

View File

@ -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();
};

View File

@ -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
*

View File

@ -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');
}

View File

@ -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 = $('<div class="sql-highlight cm-s-default"></div>');
$pre.append($highlight);
if (typeof window.CodeMirror !== 'undefined') {
var $highlight = $('<div class="sql-highlight cm-s-default"></div>');
$pre.append($highlight);
// @ts-ignore
window.CodeMirror.runMode($sql.text(), 'text/x-mysql', $highlight[0]);
$sql.hide();

View File

@ -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();
}
});
/**

View File

@ -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.

View File

@ -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'),

View File

@ -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,

View File

@ -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');

View File

@ -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 = '<div class="col-auto">'
@ -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);

View File

@ -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 */

View File

@ -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);

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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 ? '' : ',');

View File

@ -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 = ' ';
}

View File

@ -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 = ' <!-- ' . __('Table') . ' '
@ -418,6 +420,8 @@ class ExportXml extends ExportPlugin
// the XML structure
if (! isset($record[$i])) {
$record[$i] = 'NULL';
} elseif ($fieldsMeta[$i]->isMappedTypeGeometry || $fieldsMeta[$i]->isBinary) {
$record[$i] = '0x' . bin2hex($record[$i]);
}
$buffer .= ' <column name="'

View File

@ -21,6 +21,7 @@ use PhpMyAdmin\Properties\Plugins\ExportPluginProperties;
use function __;
use function array_key_exists;
use function bin2hex;
use function is_numeric;
use function str_replace;
@ -130,6 +131,10 @@ class ExportYaml extends ExportPlugin
continue;
}
if ($fieldsMeta[$i]->isMappedTypeGeometry || $fieldsMeta[$i]->isBinary) {
$record[$i] = '0x' . bin2hex($record[$i]);
}
$isNotString = isset($fieldsMeta[$i]) && $fieldsMeta[$i]->isNotType(FieldMetadata::TYPE_STRING);
if (is_numeric($record[$i]) && $isNotString) {
$buffer .= ' ' . $columns[$i] . ': ' . $record[$i] . "\n";

View File

@ -183,9 +183,10 @@ class Pdf extends PdfLib
/**
* Generate table
*
* @param int|float $lineheight Height of line
* @param int|float $lineheight Height of line
* @param list<FieldMetadata> $fields meta info for fields in $this->results
*/
public function morepagestable(int|float $lineheight = 8): void
public function morepagestable(int|float $lineheight, array $fields): void
{
// some things to set and 'remember'
$l = $this->lMargin;
@ -212,6 +213,16 @@ class Pdf extends PdfLib
$this->page = $currpage;
$this->setXY($l, $h);
if ($this->tablewidths[$col] > 0) {
if ($txt !== null) {
if ($fields[$col]->isType(FieldMetadata::TYPE_GEOMETRY)) {
$txt = '[GEOMETRY]';
}
if ($fields[$col]->isType(FieldMetadata::TYPE_BLOB) || $fields[$col]->isBinary()) {
$txt = '[BLOB]';
}
}
$this->MultiCell($this->tablewidths[$col], $lineheight, $txt ?? 'NULL', 0, $this->colAlign[$col]);
$l += $this->tablewidths[$col];
}
@ -690,7 +701,7 @@ class Pdf extends PdfLib
}
/**
* @todo do not deactivate completely the display
* do not deactivate completely the display
* but show the field's name and [BLOB]
*/
if ($fields[$i]->isBinary()) {
@ -765,11 +776,12 @@ class Pdf extends PdfLib
// Pass 2
$this->results = $dbi->query($query, ConnectionType::User, DatabaseInterface::QUERY_UNBUFFERED);
$fields = $dbi->getFieldsMeta($this->results);
$this->setY($this->tMargin);
$this->AddPage();
$this->setFont(PdfLib::PMA_PDF_FONT, '', 9);
// phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$this->morepagestable($this->FontSizePt);
$this->morepagestable($this->FontSizePt, $fields);
}
public function setTitleFontSize(int $titleFontSize): void

View File

@ -16,15 +16,13 @@ class ChangePasswordTest extends TestBase
/**
* Create a test database for this test class
*/
protected static bool $createDatabase = false;
protected bool $createDatabase = false;
/**
* Tests the changing of the password
*/
public function testChangePassword(): void
{
$this->login();
$e = $this->waitForElement('id', 'change_password_anchor');
$e->click();

View File

@ -20,7 +20,6 @@ class CreateDropDatabaseTest extends TestBase
/* TODO: For now this tests needs superuser for deleting database */
$this->skipIfNotSuperUser();
$this->login();
}
/**

View File

@ -17,7 +17,7 @@ class CreateRemoveUserTest extends TestBase
/**
* Create a test database for this test class
*/
protected static bool $createDatabase = false;
protected bool $createDatabase = false;
/**
* Username for the user
@ -39,7 +39,6 @@ class CreateRemoveUserTest extends TestBase
$this->skipIfNotSuperUser();
$this->txtUsername = 'test_user_' . bin2hex(random_bytes(4));
$this->txtPassword = 'abc_123';
$this->login();
}
/**

View File

@ -34,7 +34,7 @@ class EventsTest extends TestBase
. 'INSERT INTO `test_table` (val) VALUES (2);'
. 'SET GLOBAL event_scheduler="ON";',
);
$this->login();
$this->navigateDatabase($this->databaseName);
// Let the Database page load

View File

@ -12,16 +12,6 @@ use PHPUnit\Framework\Attributes\Large;
#[Large]
class OperationsTest extends TestBase
{
/**
* setUp function
*/
protected function setUp(): void
{
parent::setUp();
$this->login();
}
private function getToDBOperations(): void
{
$this->gotoHomepage();

View File

@ -49,8 +49,6 @@ class ProceduresTest extends TestBase
. ');',
);
$this->login();
$this->navigateDatabase($this->databaseName);
$this->expandMore();
}

View File

@ -34,7 +34,6 @@ class StructureTest extends TestBase
. 'INSERT INTO `test_table` (val) VALUES (2);',
);
$this->login();
$this->navigateDatabase($this->databaseName);
// Let the Database page load

View File

@ -30,8 +30,6 @@ class ExportTest extends TestBase
. ');'
. 'INSERT INTO `test_table` (val) VALUES (2);',
);
$this->login();
}
/**

View File

@ -11,16 +11,6 @@ use PHPUnit\Framework\Attributes\Large;
#[Large]
class ImportTest extends TestBase
{
/**
* setUp function
*/
protected function setUp(): void
{
parent::setUp();
$this->login();
}
/**
* Test for server level import
*/

View File

@ -14,7 +14,12 @@ class LoginTest extends TestBase
/**
* Create a test database for this test class
*/
protected static bool $createDatabase = false;
protected bool $createDatabase = false;
/**
* Login before starting this test
*/
protected bool $login = false;
protected function setUp(): void
{
@ -28,7 +33,6 @@ class LoginTest extends TestBase
*/
public function testSuccessfulLogin(): void
{
$this->login();
$this->waitForElement('xpath', '//*[@id="server-breadcrumb"]');
self::assertTrue($this->isSuccessLogin());
$this->logOutIfLoggedIn();

View File

@ -28,7 +28,6 @@ class NormalizationTest extends TestBase
. ');',
);
$this->login();
$this->navigateTable('test_table');
$this->waitForElement('xpath', "(//a[contains(., 'Structure')])")->click();

View File

@ -16,7 +16,7 @@ class ServerSettingsTest extends TestBase
/**
* Create a test database for this test class
*/
protected static bool $createDatabase = false;
protected bool $createDatabase = false;
/**
* setUp function
@ -25,7 +25,6 @@ class ServerSettingsTest extends TestBase
{
parent::setUp();
$this->login();
$this->expandMore();
$this->waitForElement('partialLinkText', 'Settings')->click();
$this->waitAjax();

View File

@ -25,7 +25,6 @@ class SqlQueryTest extends TestBase
. ');'
. 'INSERT INTO `test_table` (val) VALUES (2), (3), (4), (5);',
);
$this->login();
}
/**

View File

@ -34,7 +34,6 @@ class BrowseTest extends TestBase
. " (3, 'Abcd', '2012-01-20 02:00:02');",
);
$this->login();
$this->navigateTable('test_table');
$this->waitAjax();

View File

@ -18,7 +18,6 @@ class CreateTest extends TestBase
{
parent::setUp();
$this->login();
$this->navigateDatabase($this->databaseName);
}

View File

@ -30,7 +30,6 @@ class InsertTest extends TestBase
. ');',
);
$this->login();
$this->navigateTable('test_table');
}

View File

@ -33,7 +33,6 @@ class OperationsTest extends TestBase
. 'INSERT INTO test_table (val, val2) VALUES (33, 44);',
);
$this->login();
$this->navigateTable('test_table');
$this->waitAjax();

View File

@ -29,7 +29,6 @@ class StructureTest extends TestBase
. ');',
);
$this->login();
$this->navigateTable('test_table');
$this->waitForElement('xpath', "(//a[contains(., 'Structure')])")->click();

View File

@ -84,7 +84,12 @@ abstract class TestBase extends TestCase
/**
* Create a test database for this test class
*/
protected static bool $createDatabase = true;
protected bool $createDatabase = true;
/**
* Login before starting this test
*/
protected bool $login = true;
/**
* Did the test create the phpMyAdmin storage database ?
@ -135,8 +140,12 @@ abstract class TestBase extends TestCase
$this->navigateTo('');
$this->webDriver->manage()->window()->maximize();
if (! static::$createDatabase) {
// Stop here, we were not asked to create a database
if ($this->login) {
$this->login();
}
if (! $this->createDatabase) {
// Stop here, we where not asked to create a database
return;
}
@ -152,7 +161,7 @@ abstract class TestBase extends TestCase
$this->dbQuery(
'CREATE DATABASE IF NOT EXISTS `' . $this->databaseName . '`; USE `' . $this->databaseName . '`;',
);
static::$createDatabase = true;
$this->createDatabase = true;
}
public function getDbPrefix(): string
@ -1085,7 +1094,7 @@ abstract class TestBase extends TestCase
*/
protected function tearDown(): void
{
if (static::$createDatabase) {
if ($this->createDatabase) {
$this->dbQuery('DROP DATABASE IF EXISTS `' . $this->databaseName . '`;');
}

View File

@ -33,7 +33,6 @@ class TrackingTest extends TestBase
. 'INSERT INTO `test_table` (val) VALUES (2), (3);',
);
$this->login();
$this->skipIfNotPMADB();
$this->navigateDatabase($this->databaseName);

View File

@ -33,8 +33,6 @@ class TriggersTest extends TestBase
. 'INSERT INTO `test_table2` (val) VALUES (2);',
);
$this->login();
$this->navigateDatabase($this->databaseName);
}

View File

@ -14,14 +14,7 @@ class XssTest extends TestBase
/**
* Create a test database for this test class
*/
protected static bool $createDatabase = false;
protected function setUp(): void
{
parent::setUp();
$this->login();
}
protected bool $createDatabase = false;
/**
* Tests the SQL query tab with a null query

85
tests/test_data/test.xml Normal file
View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- phpMyAdmin XML Dump
- version 5.2.4-dev
- https://www.phpmyadmin.net
-
- Host: localhost:3306
- Generation Time: Feb 19, 2026 at 03:28 AM
- Server version: 12.2.2-MariaDB
- PHP Version: 7.2.34
-->
<pma_xml_export version="1.0" xmlns:pma="https://www.phpmyadmin.net/some_doc_url/">
<!--
- Structure schemas
-->
<pma:structure_schemas>
<pma:database name="test" collation="utf8mb4_general_ci" charset="utf8mb4">
<pma:table name="test">
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;
</pma:table>
</pma:database>
</pma:structure_schemas>
<!--
- Database: 'test'
-->
<database name="test">
<!-- Table test -->
<table name="test">
<column name="id">1</column>
<column name="binary">0x10000000000000000000000000000000</column>
<column name="name">POLYGON</column>
<column name="shape">0x00000000010300000001000000040000000000000000405f40000000000000000000000000008063400000000000006940000000000040664000000000004057400000000000405f400000000000000000</column>
</table>
<table name="test">
<column name="id">2</column>
<column name="binary">0x20000000000000000000000000000000</column>
<column name="name">MULTIPOLYGON</column>
<column name="shape">0x00000000010600000002000000010300000001000000040000000000000000006140000000000000444000000000006062400000000000c0544000000000004064400000000000c0524000000000000061400000000000004440010300000001000000040000000000000000405a4000000000000000000000000000004c400000000000006940000000000080534000000000004057400000000000405a400000000000000000</column>
</table>
<table name="test">
<column name="id">3</column>
<column name="binary">0x30000000000000000000000000000000</column>
<column name="name">MULTIPOINT</column>
<column name="shape">0x0000000001040000000400000001010000000000000000405f400000000000004940010100000000000000008063400000000000406f40010100000000000000004066400000000000e0614001010000000000000000e065400000000000005440</column>
</table>
<table name="test">
<column name="id">4</column>
<column name="binary">0x40000000000000000000000000000000</column>
<column name="name">MULTILINESTRING</column>
<column name="shape">0x000000000105000000020000000102000000030000000000000000004240000000000080614000000000008047400000000000206d400000000000004f400000000000c052400102000000030000000000000000004240000000000000594000000000000031400000000000206d4000000000004066400000000000405740</column>
</table>
<table name="test">
<column name="id">5</column>
<column name="binary">0x50000000000000000000000000000000</column>
<column name="name">POINT</column>
<column name="shape">0x00000000010100000000000000000059400000000000406f40</column>
</table>
<table name="test">
<column name="id">6</column>
<column name="binary">0x60000000000000000000000000000000</column>
<column name="name">LINESTRING</column>
<column name="shape">0x0000000001020000000400000000000000008063400000000000003840000000000040664000000000000058400000000000606d4000000000000069400000000000a063400000000000c06140</column>
</table>
<table name="test">
<column name="id">7</column>
<column name="binary">0x70000000000000000000000000000000</column>
<column name="name">GEOMETRYCOLLECTION</column>
<column name="shape">0x000000000107000000030000000101000000000000000000594000000000000059400102000000050000000000000000000000000000000000000000000000000059400000000000005940000000000000694000000000000069400000000000c072400000000000c07240000000000000794000000000000079400103000000020000000500000000000000008041400000000000002440000000000000244000000000000034400000000000002e40000000000000444000000000008046400000000000804640000000000080414000000000000024400400000000000000000034400000000000003e40000000000080414000000000008041400000000000003e40000000000000344000000000000034400000000000003e40</column>
</table>
<table name="test">
<column name="id">8</column>
<column name="binary">0x80000000000000000000000000000000</column>
<column name="name">TEST</column>
<column name="shape">NULL</column>
</table>
</database>
</pma_xml_export>

View File

@ -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);
}
}