Merge branch 'QA_5_2'

Already merged in master: https://github.com/phpmyadmin/phpmyadmin/pull/19960
Re-write of the test from 4de9f0f0b1
Merge of 631cac096f
Merge of 58cee9656f but could not find the code to implement

Signed-off-by: William Desportes <williamdes@wdes.fr>
This commit is contained in:
William Desportes 2025-12-28 12:39:56 +01:00
commit 4da3aaaf7a
No known key found for this signature in database
GPG Key ID: 70684F4717D49A31
23 changed files with 346 additions and 57 deletions

View File

@ -25,7 +25,7 @@ jobs:
- { version: '6.0', branch: 'master', php-version: '8.2', node-version: '20', python-version: '3.11' }
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ matrix.branch }}
@ -40,13 +40,13 @@ jobs:
coverage: none
- name: Set up Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
@ -78,7 +78,7 @@ jobs:
continue-on-error: true
- name: Upload snapshots
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: phpMyAdmin-${{ matrix.version }}-snapshots
path: release/*

View File

@ -23,10 +23,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'yarn'
@ -42,10 +42,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'yarn'
@ -61,10 +61,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'yarn'

View File

@ -19,7 +19,7 @@ jobs:
php-version: ["8.2"]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
@ -40,7 +40,7 @@ jobs:
- name: Cache coding-standard
# Allow the previous check to fail but not abort
if: always()
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: .phpcs-cache
key: phpcs-cache
@ -67,7 +67,7 @@ jobs:
php-version: ["8.2"]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Lint phpdoc blocks
uses: sudo-bot/action-doctum@v5

View File

@ -34,13 +34,13 @@ jobs:
- ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 2
- name: Checkout base ref code
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.base_ref }}

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Sphinx for the documentation build
run: |
@ -33,7 +33,7 @@ jobs:
php-version: ["8.2"]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install gettext
run: sudo apt-get install -y gettext
@ -49,13 +49,13 @@ jobs:
uses: ramsey/composer-install@v3
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'yarn'
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.11'

View File

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Run ShellCheck
uses: reviewdog/action-shellcheck@v1

View File

@ -82,7 +82,7 @@ jobs:
- "Xss"
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
@ -101,7 +101,7 @@ jobs:
uses: ramsey/composer-install@v3
- name: Set up Node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'yarn'
@ -148,7 +148,7 @@ jobs:
./tests/stop-local-server
- name: Upload screenshots
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: ${{ failure() }}
with:
name: selenium-screenshots

View File

@ -28,7 +28,7 @@ jobs:
- { php-version: '82', experimental: false, arch: 's390x', exclude-phpunit-groups: '--exclude-group extension-iconv --exclude-group 32bit-incompatible' }
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Write script
# tcpdf allowed memory exhausted needs the memory_limit workaround
@ -78,7 +78,7 @@ jobs:
php-extensions: 'mbstring, iconv, mysqli, zip, bz2'
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
# Fetch some commits for Scrutinizer coverage upload
fetch-depth: 15
@ -139,7 +139,7 @@ jobs:
extension: ["dbase"]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
# Fetch some commits for Scrutinizer coverage upload
fetch-depth: 15

View File

@ -21,7 +21,7 @@ jobs:
php-version: ["8.2"]
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install Gettext
run: |

View File

@ -204,18 +204,6 @@ parameters:
count: 1
path: examples/signon.php
-
message: '#^Comparison operation "\<" between int\<80200, 80599\> and 80200 is always false\.$#'
identifier: smaller.alwaysFalse
count: 1
path: public/index.php
-
message: '#^Comparison operation "\<" between int\<80200, 80599\> and 80200 is always false\.$#'
identifier: smaller.alwaysFalse
count: 1
path: public/setup/index.php
-
message: '#^Loose comparison via "\=\=" is not allowed\.$#'
identifier: equal.notAllowed
@ -17928,12 +17916,24 @@ parameters:
count: 1
path: tests/unit/UserPreferencesTest.php
-
message: '#^Cannot access offset ''secret'' on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible
count: 1
path: tests/unit/UserPreferencesTest.php
-
message: '#^Cannot access offset ''server_2'' on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible
count: 1
path: tests/unit/UserPreferencesTest.php
-
message: '#^Cannot access offset ''settings'' on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible
count: 1
path: tests/unit/UserPreferencesTest.php
-
message: '#^Cannot access offset ''ts'' on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible
@ -17958,6 +17958,18 @@ parameters:
count: 1
path: tests/unit/UserPreferencesTest.php
-
message: '#^Parameter \#2 \$result of method PhpMyAdmin\\Tests\\Stubs\\DbiDummy\:\:addResult\(\) expects bool\|list\<non\-empty\-list\<float\|int\|string\|null\>\>, array\{array\{non\-empty\-string\|false, 1767029179\}\} given\.$#'
identifier: argument.type
count: 2
path: tests/unit/UserPreferencesTest.php
-
message: '#^Parameter \#3 \$subject of function str_replace expects array\<string\>\|string, string\|false given\.$#'
identifier: argument.type
count: 1
path: tests/unit/UserPreferencesTest.php
-
message: '#^Property PhpMyAdmin\\Config\:\:\$settings \(array\{PmaAbsoluteUri\: string, AuthLog\: string, AuthLogSuccess\: bool, PmaNoRelation_DisableWarning\: bool, SuhosinDisableWarning\: bool, LoginCookieValidityDisableWarning\: bool, ReservedWordDisableWarning\: bool, TranslationWarningThreshold\: int, \.\.\.\}\) does not accept array\{''Server/hide_db''\: ''testval123'', ''Server/port''\: ''213''\}\.$#'
identifier: assign.propertyType

View File

@ -1,6 +1,7 @@
includes:
- phpstan-baseline.neon
parameters:
phpVersion: 80200 # PHP 8.2
level: max
paths:
- app

View File

@ -1 +1 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="m6 6v18h18v-18zm14 8v2h-10v-2z"/></svg>
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect x="7" y="7" width="14" height="14" fill="white"/><path d="m6 6v18h18v-18zm14 8v2h-10v-2z"/></svg>

Before

Width:  |  Height:  |  Size: 108 B

After

Width:  |  Height:  |  Size: 164 B

View File

@ -1 +1 @@
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="m6 6v18h18v-18zm14 8v2h-4v4h-2v-4h-4v-2h4v-4h2v4z"/></svg>
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><rect x="6" y="6" width="14" height="14" fill="white"/><path d="m6 6v18h18v-18zm14 8v2h-4v4h-2v-4h-4v-2h4v-4h2v4z"/></svg>

Before

Width:  |  Height:  |  Size: 127 B

After

Width:  |  Height:  |  Size: 182 B

View File

@ -0,0 +1,24 @@
/**
* Applies JSON syntax highlighting transformation using CodeMirror
*
* @param {JQuery} $base base element which contains the JSON code blocks
*/
export default function highlightJson ($base) {
var $elm = $base.find('code.json');
$elm.each(function () {
var $json = $(this);
var $pre = $json.find('pre');
/* We only care about visible elements to avoid double processing */
if ($pre.is(':visible')) {
var $highlight = $('<div class="json-highlight cm-s-default"></div>');
$json.append($highlight);
// @ts-ignore
if (typeof window.CodeMirror !== 'undefined' && typeof window.CodeMirror.runMode === 'function') {
// @ts-ignore
window.CodeMirror.runMode($json.text(), 'application/json', $highlight[0]);
$pre.hide();
}
}
});
}

View File

@ -6,6 +6,7 @@ import { Navigation } from './modules/navigation.ts';
import { CommonParams } from './modules/common.ts';
import createProfilingChart from './modules/functions/createProfilingChart.ts';
import highlightSql from './modules/sql-highlight.ts';
import highlightJson from './modules/json-highlight.ts';
import { ajaxRemoveMessage, ajaxShowMessage } from './modules/ajax-message.ts';
import { escapeBacktick, escapeHtml } from './modules/functions/escape.ts';
import refreshMainContent from './modules/functions/refreshMainContent.ts';
@ -943,6 +944,7 @@ AJAX.registerOnload('sql.js', function () {
.trigger('makeGrid');
highlightSql($sqlqueryresults);
highlightJson($sqlqueryresults);
}); // end $.post()
}); // end displayOptionsForm handler

View File

@ -1,21 +1,10 @@
import $ from 'jquery';
import { AJAX } from '../modules/ajax.ts';
import highlightJson from '../modules/json-highlight.ts';
/**
* JSON syntax highlighting transformation plugin
*/
AJAX.registerOnload('transformations/json.js', function () {
var $elm = $('#page_content').find('code.json');
$elm.each(function () {
var $json = $(this);
var $pre = $json.closest('pre');
/* We only care about visible elements to avoid double processing */
if ($json.is(':visible')) {
var $highlight = $('<div class="json-highlight cm-s-default"></div>');
$pre.append($highlight);
// @ts-ignore
window.CodeMirror.runMode($json.text(), 'application/json', $highlight[0]);
$json.hide();
}
});
highlightJson($('#page_content'));
});

View File

@ -234,7 +234,11 @@
<td colspan="{{ action_colspan }}"
class="text-center">
{% if current_table['Comment'] is not empty %}
{{ current_table['Comment'] }}
{% else %}
{{ t('in use') }}
{% endif %}
</td>
{% endif %}
</tr>

View File

@ -21,7 +21,7 @@
{% if table is not empty %}
<li class="breadcrumb-item">
{{ show_icons('TabsMode') ? get_image(table.is_view ? 'b_views' : 's_tbl') }}
<a href="{{ url(table.url, {'db': database.name, 'table': table.name}) }}" data-raw-text="{{ table.name }}" draggable="false">
<a href="{{ url(table.url, {'db': database.name, 'table': table.name, 'pos': 0}) }}" data-raw-text="{{ table.name }}" draggable="false">
{% if show_text('TabsMode') %}
{% if table.is_view %}
{{ t('View:') }}

View File

@ -51,6 +51,7 @@ use function rtrim;
use function sprintf;
use function str_contains;
use function str_replace;
use function str_starts_with;
use function stripos;
use function strtolower;
use function strtoupper;
@ -517,6 +518,13 @@ class Table implements Stringable
}
} elseif ($type === 'BINARY' || $type === 'VARBINARY') {
$query .= ' DEFAULT 0x' . $defaultValue;
} elseif ($defaultValue === 'unix_timestamp()' || $defaultValue === 'UNIX_TIMESTAMP()') {
$query .= ' DEFAULT ' . $defaultValue;
} elseif (
str_starts_with($defaultValue, 'current_timestamp')
|| str_starts_with($defaultValue, 'CURRENT_TIMESTAMP')
) {
$query .= ' DEFAULT CURRENT_TIMESTAMP(' . $length . ')';
} else {
$query .= ' DEFAULT ' . $dbi->quoteString($defaultValue);
}

View File

@ -115,6 +115,16 @@ class UserPreferences
*/
public function save(array $configArray): true|Message
{
// Smart merge: If this looks like a partial save (missing 2fa but we have it stored),
// automatically merge to prevent accidental overwrites
if (! isset($configArray['2fa'])) {
$existingPrefs = $this->load();
if (isset($existingPrefs['config_data']['2fa'])) {
// This is likely a partial save from page settings - merge to preserve 2fa
$configArray = array_merge($existingPrefs['config_data'], $configArray);
}
}
$relationParameters = $this->relation->getRelationParameters();
$cacheKey = 'server_' . Current::$server;
if (

View File

@ -923,6 +923,113 @@ class TableTest extends AbstractTestCase
);
}
public function testGenerateFieldSpecForRename(): void
{
// Rename a TIMESTAMP column from created_at to created_ts
$name = 'created_ts';
$type = 'TIMESTAMP';
$length = '6';
$attribute = '';
$collation = '';
$null = 'NO';
$defaultType = 'USER_DEFINED';
$defaultValue = 'current_timestamp(6)';
$extra = '';
$comment = '';
$virtuality = '';
$expression = '';
$moveTo = '';
$columnsWithIndex = ['id'];
$oldColumnName = 'created_at';
$query = Table::generateFieldSpec(
$name,
$type,
$length,
$attribute,
$collation,
$null,
$defaultType,
$defaultValue,
$extra,
$comment,
$virtuality,
$expression,
$moveTo,
);
self::assertSame('`created_ts` TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)', $query);
// Rename an INT column from created_at to created_ts
// This default values does not insert, but the column definition is valid
// And can be renamed
$name = 'created_ts';
$type = 'INT';
$length = '6';
$attribute = '';
$collation = '';
$null = 'NO';
$defaultType = 'USER_DEFINED';
$defaultValue = 'current_timestamp(6)';
$extra = '';
$comment = '';
$virtuality = '';
$expression = '';
$moveTo = '';
$columnsWithIndex = ['id'];
$oldColumnName = 'created_at';
$query = Table::generateFieldSpec(
$name,
$type,
$length,
$attribute,
$collation,
$null,
$defaultType,
$defaultValue,
$extra,
$comment,
$virtuality,
$expression,
$moveTo,
);
self::assertSame('`created_ts` INT(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)', $query);
// Rename an INT column from created_at to created_ts
$name = 'created_ts';
$type = 'INT';
$length = '6';
$attribute = '';
$collation = '';
$null = 'NO';
$defaultType = 'USER_DEFINED';
$defaultValue = 'UNIX_TIMESTAMP()';
$extra = '';
$comment = '';
$virtuality = '';
$expression = '';
$moveTo = '';
$columnsWithIndex = ['id'];
$oldColumnName = 'created_at';
$query = Table::generateFieldSpec(
$name,
$type,
$length,
$attribute,
$collation,
$null,
$defaultType,
$defaultValue,
$extra,
$comment,
$virtuality,
$expression,
$moveTo,
);
self::assertSame('`created_ts` INT(6) NOT NULL DEFAULT UNIX_TIMESTAMP()', $query);
}
/**
* Test for duplicateInfo
*/

View File

@ -23,6 +23,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
use ReflectionProperty;
use function json_encode;
use function str_replace;
use function time;
#[CoversClass(UserPreferences::class)]
@ -238,6 +239,137 @@ class UserPreferencesTest extends AbstractTestCase
);
}
/**
* Test for save and keep 2FA settings
*/
public function testSaveAndKeep2FA(): void
{
$initialConfig = [
'2fa' => ['backend' => 'application', 'settings' => ['secret' => 'thisisasecret']],
'theme' => 'dark',
];
$dummyDbi = $this->createDbiDummy();
$dummyDbi->removeDefaultResults();
$dummyDbi->addSelectDb('pma-db');
$dummyDbi->addResult(
'SHOW TABLES FROM `pma-db`;',
[['pma__userconfig']],
['Tables_in_pma-db'],
);
$dummyDbi->addResult(
'SELECT NULL FROM `pma__userconfig` LIMIT 0',
[['NULL']],
);
$dbi = $this->createDatabaseInterface($dummyDbi);
$config = new Config();
$config->selectedServer['pmadb'] = 'pma-db';
$config->selectedServer['userconfig'] = 'pma__userconfig';
$relation = new Relation($dbi, $config);
(new ReflectionProperty(Relation::class, 'cache'))->setValue(null, null);
$relationParameters = $relation->getRelationParameters();
self::assertNotNull($relationParameters->userPreferencesFeature);
$userPreferences = new UserPreferences($dbi, $relation, new Template());
// phpcs:disable Generic.Files.LineLength.TooLong
$dummyDbi->addResult(
<<<'SQL'
SELECT `config_data`, UNIX_TIMESTAMP(`timevalue`) ts FROM `pma-db`.`pma__userconfig` WHERE `username` = 'root'
SQL,
[],
);
$dummyDbi->addResult(
<<<'SQL'
SELECT `username` FROM `pma-db`.`pma__userconfig` WHERE `username` = 'root'
SQL,
[],
);
$dummyDbi->addResult(
<<<'SQL'
INSERT INTO `pma-db`.`pma__userconfig` (`username`, `timevalue`,`config_data`) VALUES ('root', NOW(), '[1]')
SQL,
[],
);
$result = $userPreferences->save([1]);
$dummyDbi->addResult(
<<<'SQL'
SELECT `username` FROM `pma-db`.`pma__userconfig` WHERE `username` = 'root'
SQL,
[],
);
$encodedConfig = json_encode($initialConfig);
$encodedEscapedConfig = str_replace('"', '\"', $encodedConfig);
$dummyDbi->addResult(
<<<SQL
INSERT INTO `pma-db`.`pma__userconfig` (`username`, `timevalue`,`config_data`) VALUES ('root', NOW(), '$encodedEscapedConfig')
SQL,
[],
);
// Test 2fa preservation on partial save
// Initial save with 2fa
$userPreferences->save($initialConfig);
// Not using the session storage
self::assertFalse(isset($_SESSION['userconfig']));
$dummyDbi->addResult(
<<<'SQL'
SELECT `config_data`, UNIX_TIMESTAMP(`timevalue`) ts FROM `pma-db`.`pma__userconfig` WHERE `username` = 'root'
SQL,
[[$encodedConfig, 1767029179]],
['config_data', 'ts'],
);
$dummyDbi->addResult(
<<<'SQL'
SELECT `username` FROM `pma-db`.`pma__userconfig` WHERE `username` = 'root'
SQL,
[['root']],
['username'],
);
// 2FA is combined with the previous config
$dummyDbi->addResult(
<<<'SQL'
UPDATE `pma-db`.`pma__userconfig` SET `timevalue` = NOW(), `config_data` = '{\"2fa\":{\"backend\":\"application\",\"settings\":{\"secret\":\"thisisasecret\"}},\"theme\":\"dark\",\"Console\\/Mode\":\"collapse\"}' WHERE `username` = 'root'
SQL,
[['root']],
['username'],
);
// Partial save without 2fa
$partialConfig = ['Console/Mode' => 'collapse'];
$userPreferences->save($partialConfig);
$encodedConfig = json_encode([...$initialConfig, 'Console/Mode' => 'collapse']);
$dummyDbi->addResult(
<<<'SQL'
SELECT `config_data`, UNIX_TIMESTAMP(`timevalue`) ts FROM `pma-db`.`pma__userconfig` WHERE `username` = 'root'
SQL,
[[$encodedConfig, 1767029179]],
['config_data', 'ts'],
);
// Check that 2fa is still present
$resultConfig = $userPreferences->load()['config_data'];
self::assertSame('thisisasecret', $resultConfig['2fa']['settings']['secret']);
self::assertSame('collapse', $resultConfig['Console/Mode']);
$dummyDbi->assertAllSelectsConsumed();
// phpcs:enable Generic.Files.LineLength.TooLong
}
/**
* Test for apply
*/