Merge pull request #19068 from MauricioFauth/console-settings

Refactor Console settings handling
This commit is contained in:
Maurício Meneghini Fauth 2024-03-29 14:35:47 -03:00 committed by GitHub
commit 913e7d4b17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 560 additions and 171 deletions

View File

@ -93,6 +93,10 @@ return [
'class' => Console\Bookmark\RefreshController::class,
'arguments' => ['$response' => '@response', '$template' => '@template', '$console' => '@console'],
],
Console\UpdateConfigController::class => [
'class' => Console\UpdateConfigController::class,
'arguments' => ['$response' => '@response', '$template' => '@template', '$config' => '@config'],
],
Database\CentralColumns\PopulateColumnsController::class => [
'class' => Database\CentralColumns\PopulateColumnsController::class,
'arguments' => [

View File

@ -12449,6 +12449,12 @@
<code><![CDATA[DatabaseInterface::getInstance()]]></code>
</DeprecatedMethod>
</file>
<file src="tests/unit/Controllers/Console/UpdateConfigControllerTest.php">
<PossiblyUnusedMethod>
<code><![CDATA[invalidParamsProvider]]></code>
<code><![CDATA[validParamsProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="tests/unit/Controllers/Database/EventsControllerTest.php">
<DeprecatedMethod>
<code><![CDATA[Config::getInstance()]]></code>
@ -14482,6 +14488,7 @@
<code><![CDATA[Config::getInstance()]]></code>
</DeprecatedMethod>
<MixedAssignment>
<code><![CDATA[$json['error']]]></code>
<code><![CDATA[$value]]></code>
</MixedAssignment>
<PossiblyUnusedMethod>

View File

@ -939,7 +939,7 @@ const AJAX = {
console.log('AJAX error: status=' + request.status + ', text=' + request.statusText);
}
if (settings.url.includes('/git-revision')) {
if (settings.url.includes('/git-revision') || settings.url.includes('/console/update-config')) {
return;
}

View File

@ -4,10 +4,11 @@ import { AJAX } from './ajax.ts';
import { Functions } from './functions.ts';
import { CommonParams } from './common.ts';
import { Navigation } from './navigation.ts';
import { Config } from './console/config.ts';
import { getConfigValue } from './functions/config.ts';
import Config from './console/config.ts';
import { escapeHtml } from './functions/escape.ts';
let config: Config;
/**
* Console object
*/
@ -58,18 +59,13 @@ var Console = {
* Used for console initialize, reinit is ok, just some variable assignment
*/
initialize: function (): void {
if ($('#pma_console').length === 0) {
const consoleElement = document.getElementById('pma_console');
if (consoleElement === null) {
return;
}
getConfigValue('Console', false, (data) => {
Config.init(data);
Console.setupAfterInit();
}, () => {
Config.init({});// Avoid null pointers in setupAfterInit()
// Fetching data failed, still perform the console init
Console.setupAfterInit();
});
config = Config.createFromDataset(consoleElement.dataset);
Console.setupAfterInit();
},
/**
@ -101,28 +97,6 @@ var Console = {
// Event binds shouldn't run again
if (Console.isInitialized === false) {
// Load config first
if (Config.AlwaysExpand) {
(document.getElementById('consoleOptionsAlwaysExpandCheckbox') as HTMLInputElement).checked = true;
}
if (Config.StartHistory) {
(document.getElementById('consoleOptionsStartHistoryCheckbox') as HTMLInputElement).checked = true;
}
if (Config.CurrentQuery) {
(document.getElementById('consoleOptionsCurrentQueryCheckbox') as HTMLInputElement).checked = true;
}
if (Config.EnterExecutes) {
(document.getElementById('consoleOptionsEnterExecutesCheckbox') as HTMLInputElement).checked = true;
}
if (Config.DarkTheme) {
(document.getElementById('consoleOptionsDarkThemeCheckbox') as HTMLInputElement).checked = true;
$('#pma_console').find('>.content').addClass('console_dark_theme');
}
ConsoleResizer.initialize();
ConsoleInput.initialize();
ConsoleMessages.initialize();
@ -174,21 +148,65 @@ var Console = {
Console.hideCard($(this).closest('.card'));
});
$('#pma_console_options').find('input[type=checkbox]').on('change', function () {
Config.update();
const consoleOptionsAlwaysExpandCheckbox = document.getElementById('consoleOptionsAlwaysExpandCheckbox') as HTMLInputElement;
consoleOptionsAlwaysExpandCheckbox?.addEventListener('change', function (): void {
config.setAlwaysExpand(consoleOptionsAlwaysExpandCheckbox.checked);
});
$('#pma_console_options').find('.button.default').on('click', function () {
(document.getElementById('consoleOptionsAlwaysExpandCheckbox') as HTMLInputElement).checked = false;
(document.getElementById('consoleOptionsStartHistoryCheckbox') as HTMLInputElement).checked = false;
(document.getElementById('consoleOptionsCurrentQueryCheckbox') as HTMLInputElement).checked = true;
(document.getElementById('consoleOptionsEnterExecutesCheckbox') as HTMLInputElement).checked = false;
(document.getElementById('consoleOptionsDarkThemeCheckbox') as HTMLInputElement).checked = false;
Config.update();
const consoleOptionsStartHistoryCheckbox = document.getElementById('consoleOptionsStartHistoryCheckbox') as HTMLInputElement;
consoleOptionsStartHistoryCheckbox?.addEventListener('change', function (): void {
config.setStartHistory(consoleOptionsStartHistoryCheckbox.checked);
});
$('#consoleOptionsEnterExecutesCheckbox').on('change', function () {
ConsoleMessages.showInstructions(Config.EnterExecutes);
const consoleOptionsCurrentQueryCheckbox = document.getElementById('consoleOptionsCurrentQueryCheckbox') as HTMLInputElement;
consoleOptionsCurrentQueryCheckbox?.addEventListener('change', function (): void {
config.setCurrentQuery(consoleOptionsCurrentQueryCheckbox.checked);
});
const consoleOptionsEnterExecutesCheckbox = document.getElementById('consoleOptionsEnterExecutesCheckbox') as HTMLInputElement;
consoleOptionsEnterExecutesCheckbox?.addEventListener('change', function (): void {
const isEnterExecutes = consoleOptionsEnterExecutesCheckbox.checked;
config.setEnterExecutes(isEnterExecutes);
ConsoleMessages.showInstructions(isEnterExecutes);
});
const consoleOptionsDarkThemeCheckbox = document.getElementById('consoleOptionsDarkThemeCheckbox') as HTMLInputElement;
consoleOptionsDarkThemeCheckbox?.addEventListener('change', function (): void {
const isDarkTheme = consoleOptionsDarkThemeCheckbox.checked;
config.setDarkTheme(isDarkTheme);
const consoleContent = document.getElementById('pma_console').querySelector('.content');
consoleContent.classList.toggle('console_dark_theme', isDarkTheme);
});
const restoreConsoleOptionsButton = document.getElementById('pma_console_options').querySelector('.button.default');
restoreConsoleOptionsButton?.addEventListener('click', function (): void {
if (consoleOptionsAlwaysExpandCheckbox.checked) {
consoleOptionsAlwaysExpandCheckbox.checked = false;
config.setAlwaysExpand(false);
}
if (consoleOptionsStartHistoryCheckbox.checked) {
consoleOptionsStartHistoryCheckbox.checked = false;
config.setStartHistory(false);
}
if (! consoleOptionsCurrentQueryCheckbox.checked) {
consoleOptionsCurrentQueryCheckbox.checked = true;
config.setCurrentQuery(true);
}
if (consoleOptionsEnterExecutesCheckbox.checked) {
consoleOptionsEnterExecutesCheckbox.checked = false;
config.setEnterExecutes(false);
ConsoleMessages.showInstructions(false);
}
if (consoleOptionsDarkThemeCheckbox.checked) {
consoleOptionsDarkThemeCheckbox.checked = false;
config.setDarkTheme(false);
const consoleContent = document.getElementById('pma_console').querySelector('.content');
consoleContent.classList.remove('console_dark_theme');
}
});
$(document).on('ajaxComplete', function (event, xhr, ajaxOptions) {
@ -215,7 +233,7 @@ var Console = {
}
// Change console mode from cookie
switch (Config.Mode) {
switch (config.mode) {
case 'collapse':
Console.collapse();
break;
@ -227,7 +245,7 @@ var Console = {
Console.scrollBottom();
break;
default:
Config.set('Mode', 'info');
config.setMode('info');
Console.info();
}
},
@ -282,7 +300,7 @@ var Console = {
if (data.reloadQuerywindow.sql_query.length > 0) {
ConsoleMessages.appendQuery(data.reloadQuerywindow, 'successed')
// @ts-ignore
.$message.addClass(Config.CurrentQuery ? '' : 'hide');
.$message.addClass(config.currentQuery ? '' : 'hide');
}
}
},
@ -290,8 +308,8 @@ var Console = {
* Change console to collapse mode
*/
collapse: function (): void {
Config.set('Mode', 'collapse');
var pmaConsoleHeight = Math.max(92, Config.Height);
config.setMode('collapse');
var pmaConsoleHeight = Math.max(92, config.height);
Console.$consoleToolbar.addClass('collapsed');
Console.$consoleAllContents.height(pmaConsoleHeight);
@ -305,11 +323,11 @@ var Console = {
* @param {boolean} inputFocus If true, focus the input line after show()
*/
show: function (inputFocus = undefined): void {
Config.set('Mode', 'show');
config.setMode('show');
var pmaConsoleHeight = Math.max(92, Config.Height);
var pmaConsoleHeight = Math.max(92, config.height);
// eslint-disable-next-line compat/compat
pmaConsoleHeight = Math.min(Config.Height, (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight) - 25);
pmaConsoleHeight = Math.min(config.height, (window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight) - 25);
Console.$consoleContent.css({ display: 'block' });
if (Console.$consoleToolbar.hasClass('collapsed')) {
Console.$consoleToolbar.removeClass('collapsed');
@ -335,7 +353,7 @@ var Console = {
* Used for toggle buttons and shortcuts
*/
toggle: function (): void {
if (Config.Mode === 'show') {
if (config.mode === 'show') {
Console.collapse();
} else {
Console.show(true);
@ -413,7 +431,7 @@ var ConsoleResizer = {
* @param {MouseEvent} event
*/
mouseDown: function (event): void {
if (Config.Mode !== 'show') {
if (config.mode !== 'show') {
return;
}
@ -456,7 +474,7 @@ var ConsoleResizer = {
* Mouseup event handler for bind to resizer
*/
mouseUp: function (): void {
Config.set('Height', Math.round(ConsoleResizer.resultHeight));
config.setHeight(Math.round(ConsoleResizer.resultHeight));
Console.show();
$(document).off('mousemove');
$(document).off('mouseup');
@ -638,7 +656,7 @@ var ConsoleInput = {
*/
keyDown: function (event): void {
// Execute command
if (Config.EnterExecutes) {
if (config.enterExecutes) {
// Enter, but not in combination with Shift (which writes a new line).
if (! event.shiftKey && event.keyCode === 13) {
ConsoleInput.execute();
@ -810,7 +828,7 @@ var ConsoleMessages = {
var now = new Date();
var $newMessage =
$('<div class="message ' +
(Config.AlwaysExpand ? 'expanded' : 'collapsed') +
(config.alwaysExpand ? 'expanded' : 'collapsed') +
'" msgid="' + msgId + '"><div class="action_content"></div></div>');
switch (msgType) {
case 'query':
@ -1053,11 +1071,11 @@ var ConsoleMessages = {
*/
initialize: function (): void {
ConsoleMessages.messageEventBinds($('#pma_console').find('.message:not(.binded)'));
if (Config.StartHistory) {
if (config.startHistory) {
ConsoleMessages.showHistory();
}
ConsoleMessages.showInstructions(Config.EnterExecutes);
ConsoleMessages.showInstructions(config.enterExecutes);
}
};
@ -1163,17 +1181,17 @@ var ConsoleDebug = {
}
});
if (Config.GroupQueries) {
if (config.groupQueries) {
$('#debug_console').addClass('grouped');
} else {
$('#debug_console').addClass('ungrouped');
if (Config.OrderBy === 'count') {
if (config.orderBy === 'count') {
$('#debug_console').find('.button.order_by.sort_exec').addClass('active');
}
}
var orderBy = Config.OrderBy;
var order = Config.Order;
var orderBy = config.orderBy;
var order = config.order;
$('#debug_console').find('.button.order_by.sort_' + orderBy).addClass('active');
$('#debug_console').find('.button.order.order_' + order).addClass('active');
@ -1181,9 +1199,9 @@ var ConsoleDebug = {
$('#debug_console').find('.button.group_queries').on('click', function () {
$('#debug_console').addClass('grouped');
$('#debug_console').removeClass('ungrouped');
Config.set('GroupQueries', true);
config.setGroupQueries(true);
ConsoleDebug.refresh();
if (Config.OrderBy === 'count') {
if (config.orderBy === 'count') {
$('#debug_console').find('.button.order_by.sort_exec').removeClass('active');
}
});
@ -1191,9 +1209,9 @@ var ConsoleDebug = {
$('#debug_console').find('.button.ungroup_queries').on('click', function () {
$('#debug_console').addClass('ungrouped');
$('#debug_console').removeClass('grouped');
Config.set('GroupQueries', false);
config.setGroupQueries(false);
ConsoleDebug.refresh();
if (Config.OrderBy === 'count') {
if (config.orderBy === 'count') {
$('#debug_console').find('.button.order_by.sort_exec').addClass('active');
}
});
@ -1203,11 +1221,11 @@ var ConsoleDebug = {
$('#debug_console').find('.button.order_by').removeClass('active');
$this.addClass('active');
if ($this.hasClass('sort_time')) {
Config.set('OrderBy', 'time');
config.setOrderBy('time');
} else if ($this.hasClass('sort_exec')) {
Config.set('OrderBy', 'exec');
config.setOrderBy('exec');
} else if ($this.hasClass('sort_count')) {
Config.set('OrderBy', 'count');
config.setOrderBy('count');
}
ConsoleDebug.refresh();
@ -1218,9 +1236,9 @@ var ConsoleDebug = {
$('#debug_console').find('.button.order').removeClass('active');
$this.addClass('active');
if ($this.hasClass('order_asc')) {
Config.set('Order', 'asc');
config.setOrder('asc');
} else if ($this.hasClass('order_desc')) {
Config.set('Order', 'desc');
config.setOrder('desc');
}
ConsoleDebug.refresh();
@ -1507,7 +1525,7 @@ var ConsoleDebug = {
// For sorting queries
function sortByTime (a, b) {
var order = Config.Order === 'asc' ? 1 : -1;
var order = config.order === 'asc' ? 1 : -1;
if (Array.isArray(a) && Array.isArray(b)) {
// It is grouped
var timeA = 0;
@ -1528,15 +1546,15 @@ var ConsoleDebug = {
}
function sortByCount (a, b) {
var order = Config.Order === 'asc' ? 1 : -1;
var order = config.order === 'asc' ? 1 : -1;
return (a.length - b.length) * order;
}
var orderBy = Config.OrderBy;
var order = Config.Order;
var orderBy = config.orderBy;
var order = config.order;
if (Config.GroupQueries) {
if (config.groupQueries) {
// Sort queries
if (orderBy === 'time') {
uniqueQueries.sort(sortByTime);

View File

@ -1,90 +1,139 @@
import { setConfigValue } from '../functions/config.ts';
import $ from 'jquery';
import { ajaxShowMessage } from '../ajax-message.ts';
import { CommonParams } from '../common.ts';
import { escapeHtml } from '../functions/escape.ts';
/**
* @link https://docs.phpmyadmin.net/en/latest/config.html#console-settings
*/
export const Config = {
/**
* @type {boolean}
*/
StartHistory: false,
/**
* @type {boolean}
*/
AlwaysExpand: false,
/**
* @type {boolean}
*/
CurrentQuery: true,
/**
* @type {boolean}
*/
EnterExecutes: false,
/**
* @type {boolean}
*/
DarkTheme: false,
/**
* @type {'info'|'show'|'collapse'}
*/
Mode: 'info',
/**
* @type {number}
*/
Height: 92,
/**
* @type {boolean}
*/
GroupQueries: false,
/**
* @type {'exec'|'time'|'count'}
*/
OrderBy: 'exec',
/**
* @type {'asc'|'desc'}
*/
Order: 'asc',
export default class Config {
startHistory: boolean;
/**
* @param {Object} data
*/
init: function (data): void {
this.StartHistory = !! data.StartHistory;
this.AlwaysExpand = !! data.AlwaysExpand;
this.CurrentQuery = data.CurrentQuery !== undefined ? !! data.CurrentQuery : true;
this.EnterExecutes = !! data.EnterExecutes;
this.DarkTheme = !! data.DarkTheme;
this.Mode = data.Mode === 'show' || data.Mode === 'collapse' ? data.Mode : 'info';
this.Height = data.Height > 0 ? Number(data.Height) : 92;
this.GroupQueries = !! data.GroupQueries;
this.OrderBy = data.OrderBy === 'time' || data.OrderBy === 'count' ? data.OrderBy : 'exec';
this.Order = data.Order === 'desc' ? 'desc' : 'asc';
},
alwaysExpand: boolean;
/**
* @param {'StartHistory'|'AlwaysExpand'|'CurrentQuery'|'EnterExecutes'|'DarkTheme'|'Mode'|'Height'|'GroupQueries'|'OrderBy'|'Order'} key
* @param {boolean|string|number} value
*/
set: function (key, value): void {
this[key] = value;
setConfigValue('Console/' + key, value);
},
currentQuery: boolean;
/**
* Used for update console config
*/
update: function (): void {
this.set('AlwaysExpand', !! (document.getElementById('consoleOptionsAlwaysExpandCheckbox') as HTMLInputElement).checked);
this.set('StartHistory', !! (document.getElementById('consoleOptionsStartHistoryCheckbox') as HTMLInputElement).checked);
this.set('CurrentQuery', !! (document.getElementById('consoleOptionsCurrentQueryCheckbox') as HTMLInputElement).checked);
this.set('EnterExecutes', !! (document.getElementById('consoleOptionsEnterExecutesCheckbox') as HTMLInputElement).checked);
this.set('DarkTheme', !! (document.getElementById('consoleOptionsDarkThemeCheckbox') as HTMLInputElement).checked);
/* Setting the dark theme of the console*/
const consoleContent = document.getElementById('pma_console').querySelector('.content');
if (this.DarkTheme) {
consoleContent.classList.add('console_dark_theme');
} else {
consoleContent.classList.remove('console_dark_theme');
}
enterExecutes: boolean;
darkTheme: boolean;
mode: 'info'|'show'|'collapse';
height: number;
groupQueries: boolean;
orderBy: 'exec'|'time'|'count';
order: 'asc'|'desc';
constructor (
startHistory: boolean,
alwaysExpand: boolean,
currentQuery: boolean,
enterExecutes: boolean,
darkTheme: boolean,
mode: 'info'|'show'|'collapse',
height: number,
groupQueries: boolean,
orderBy: 'exec'|'time'|'count',
order: 'asc'|'desc',
) {
this.startHistory = startHistory;
this.alwaysExpand = alwaysExpand;
this.currentQuery = currentQuery;
this.enterExecutes = enterExecutes;
this.darkTheme = darkTheme;
this.mode = mode;
this.height = height;
this.groupQueries = groupQueries;
this.orderBy = orderBy;
this.order = order;
}
};
static createFromDataset (dataset: DOMStringMap): Config {
const height = Number(dataset.height);
return new this(
dataset.startHistory === 'true',
dataset.alwaysExpand === 'true',
dataset.currentQuery !== undefined ? dataset.currentQuery === 'true' : true,
dataset.enterExecutes === 'true',
dataset.darkTheme === 'true',
dataset.mode === 'show' || dataset.mode === 'collapse' ? dataset.mode : 'info',
height > 0 ? height : 92,
dataset.groupQueries === 'true',
dataset.orderBy === 'time' || dataset.orderBy === 'count' ? dataset.orderBy : 'exec',
dataset.order === 'desc' ? 'desc' : 'asc',
);
}
setStartHistory (value: boolean): void {
this.startHistory = value;
setConfigValue('StartHistory', value);
}
setAlwaysExpand (value: boolean): void {
this.alwaysExpand = value;
setConfigValue('AlwaysExpand', value);
}
setCurrentQuery (value: boolean): void {
this.currentQuery = value;
setConfigValue('CurrentQuery', value);
}
setEnterExecutes (value: boolean): void {
this.enterExecutes = value;
setConfigValue('EnterExecutes', value);
}
setDarkTheme (value: boolean): void {
this.darkTheme = value;
setConfigValue('DarkTheme', value);
}
setMode (value: 'info'|'show'|'collapse'): void {
this.mode = value;
setConfigValue('Mode', value);
}
setHeight (value: number): void {
this.height = value;
setConfigValue('Height', value);
}
setGroupQueries (value: boolean): void {
this.groupQueries = value;
setConfigValue('GroupQueries', value);
}
setOrderBy (value: 'exec'|'time'|'count'): void {
this.orderBy = value;
setConfigValue('OrderBy', value);
}
setOrder (value: 'asc'|'desc'): void {
this.order = value;
setConfigValue('Order', value);
}
}
/**
* @param {'StartHistory'|'AlwaysExpand'|'CurrentQuery'|'EnterExecutes'|'DarkTheme'|'Mode'|'Height'|'GroupQueries'|'OrderBy'|'Order'} key
* @param {boolean|string|number} value
*/
function setConfigValue (key: string, value: boolean|number|string): void {
$.post(
'index.php?route=/console/update-config',
{
'ajax_request': true,
server: CommonParams.get('server'),
key: key,
value: value,
},
).fail(function (data) {
const message = '<div class="alert alert-danger" role="alert">' + escapeHtml(data.responseJSON.error) + '</div>';
ajaxShowMessage(message, false);
});
}

View File

@ -1,5 +1,15 @@
<div id="pma_console_container" class="d-print-none">
<div id="pma_console">
<div id="pma_console"
data-start-history="{{ settings.StartHistory ? 'true' : 'false' }}"
data-always-expand="{{ settings.AlwaysExpand ? 'true' : 'false' }}"
data-current-query="{{ settings.CurrentQuery ? 'true' : 'false' }}"
data-enter-executes="{{ settings.EnterExecutes ? 'true' : 'false' }}"
data-dark-theme="{{ settings.DarkTheme ? 'true' : 'false' }}"
data-mode="{{ settings.Mode }}"
data-height="{{ settings.Height }}"
data-group-queries="{{ settings.GroupQueries ? 'true' : 'false' }}"
data-order-by="{{ settings.OrderBy }}"
data-order="{{ settings.Order }}">
<div class="toolbar collapsed">
<div class="switch_button console_switch">
{{ get_image('console', 'SQL Query Console'|trans) }}
@ -27,7 +37,7 @@
</div>
{# Console messages #}
<div class="content">
<div class="content{{ settings.DarkTheme ? ' console_dark_theme' }}">
<div class="console_message_container">
<div class="message welcome">
<span id="instructions-0">{% trans 'Press Ctrl+Enter to execute query' %}</span>
@ -165,29 +175,29 @@
<span>{{ 'Options'|trans }}</span>
</div>
<div class="button default">
<span>{{ 'Set default'|trans }}</span>
<span>{{ 'Restore default values'|trans }}</span>
</div>
</div>
<div class="content">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsAlwaysExpandCheckbox" name="always_expand">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsAlwaysExpandCheckbox" name="always_expand"{{ settings.AlwaysExpand ? ' checked' }}>
<label class="form-check-label" for="consoleOptionsAlwaysExpandCheckbox">{{ 'Always expand query messages'|trans }}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsStartHistoryCheckbox" name="start_history">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsStartHistoryCheckbox" name="start_history"{{ settings.StartHistory ? ' checked' }}>
<label class="form-check-label" for="consoleOptionsStartHistoryCheckbox">{{ 'Show query history at start'|trans }}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsCurrentQueryCheckbox" name="current_query">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsCurrentQueryCheckbox" name="current_query"{{ settings.CurrentQuery ? ' checked' }}>
<label class="form-check-label" for="consoleOptionsCurrentQueryCheckbox">{{ 'Show current browsing query'|trans }}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsEnterExecutesCheckbox" name="enter_executes">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsEnterExecutesCheckbox" name="enter_executes"{{ settings.EnterExecutes ? ' checked' }}>
<label class="form-check-label" for="consoleOptionsEnterExecutesCheckbox">{{ 'Execute queries on Enter and insert new line with Shift+Enter. To make this permanent, view settings.'|trans }}</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsDarkThemeCheckbox" name="dark_theme">
<input class="form-check-input" type="checkbox" role="switch" id="consoleOptionsDarkThemeCheckbox" name="dark_theme"{{ settings.DarkTheme ? ' checked' }}>
<label class="form-check-label" for="consoleOptionsDarkThemeCheckbox">{{ 'Switch to dark theme'|trans }}</label>
</div>
</div>

View File

@ -113,6 +113,7 @@ class Console
$bookmarkContent = $this->getBookmarkContent();
return $this->template->render('console/display', [
'settings' => $this->config->config->Console->asArray(),
'has_bookmark_feature' => $bookmarkFeature !== null,
'sql_history' => $sqlHistory,
'bookmark_content' => $bookmarkContent,

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Controllers\Console;
use Fig\Http\Message\StatusCodeInterface;
use InvalidArgumentException;
use PhpMyAdmin\Config;
use PhpMyAdmin\Controllers\AbstractController;
use PhpMyAdmin\Http\Response;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Template;
use function __;
use function in_array;
use function is_numeric;
final class UpdateConfigController extends AbstractController
{
public function __construct(ResponseRenderer $response, Template $template, private Config $config)
{
parent::__construct($response, $template);
}
public function __invoke(ServerRequest $request): Response
{
try {
$key = $this->parseKeyParam($request->getParsedBodyParam('key'));
$value = $this->parseValueParam($key, $request->getParsedBodyParam('value'));
} catch (InvalidArgumentException $exception) {
$this->response->setStatusCode(StatusCodeInterface::STATUS_BAD_REQUEST);
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => $exception->getMessage()]);
return $this->response->response();
}
$result = $this->config->setUserValue(null, 'Console/' . $key, $value);
if ($result !== true) {
$this->response->setStatusCode(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
$this->response->setRequestStatus(false);
$this->response->addJSON(['message' => $result->getMessage()]);
return $this->response->response();
}
$this->response->addJSON('message', __('Console settings has been updated successfully.'));
return $this->response->response();
}
/** @psalm-return 'StartHistory'|'AlwaysExpand'|'CurrentQuery'|'EnterExecutes'|'DarkTheme'|'Mode'|'Height'|'GroupQueries'|'OrderBy'|'Order' */
private function parseKeyParam(mixed $key): string
{
if (
! in_array($key, [
'StartHistory',
'AlwaysExpand',
'CurrentQuery',
'EnterExecutes',
'DarkTheme',
'Mode',
'Height',
'GroupQueries',
'OrderBy',
'Order',
], true)
) {
throw new InvalidArgumentException(__('Unexpected parameter value.'));
}
return $key;
}
/** @psalm-param 'StartHistory'|'AlwaysExpand'|'CurrentQuery'|'EnterExecutes'|'DarkTheme'|'Mode'|'Height'|'GroupQueries'|'OrderBy'|'Order' $key */
private function parseValueParam(string $key, mixed $value): bool|int|string
{
if (
in_array($key, [
'StartHistory',
'AlwaysExpand',
'CurrentQuery',
'EnterExecutes',
'DarkTheme',
'GroupQueries',
], true)
&& in_array($value, ['true', 'false'], true)
) {
return $value === 'true';
}
if ($key === 'Mode' && in_array($value, ['show', 'collapse', 'info'], true)) {
return $value;
}
if ($key === 'Height' && is_numeric($value) && $value > 0) {
return (int) $value;
}
if ($key === 'OrderBy' && in_array($value, ['exec', 'time', 'count'], true)) {
return $value;
}
if ($key === 'Order' && in_array($value, ['asc', 'desc'], true)) {
return $value;
}
throw new InvalidArgumentException(__('Unexpected parameter value.'));
}
}

View File

@ -12,6 +12,7 @@ use PhpMyAdmin\Controllers\CollationConnectionController;
use PhpMyAdmin\Controllers\ColumnController;
use PhpMyAdmin\Controllers\Config;
use PhpMyAdmin\Controllers\Console\Bookmark;
use PhpMyAdmin\Controllers\Console\UpdateConfigController;
use PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Controllers\DatabaseController;
use PhpMyAdmin\Controllers\ErrorReportController;
@ -57,9 +58,12 @@ final class Routes
$routes->post('/get', Config\GetConfigController::class);
$routes->post('/set', Config\SetConfigController::class);
});
$routes->addGroup('/console/bookmark', static function (RouteCollector $routes): void {
$routes->post('/add', Bookmark\AddController::class);
$routes->get('/refresh', Bookmark\RefreshController::class);
$routes->addGroup('/console', static function (RouteCollector $routes): void {
$routes->addGroup('/bookmark', static function (RouteCollector $routes): void {
$routes->post('/add', Bookmark\AddController::class);
$routes->get('/refresh', Bookmark\RefreshController::class);
});
$routes->post('/update-config', UpdateConfigController::class);
});
$routes->addGroup('/database', static function (RouteCollector $routes): void {
$routes->addGroup('/central-columns', static function (RouteCollector $routes): void {

View File

@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests\Controllers\Console;
use PhpMyAdmin\Config;
use PhpMyAdmin\Controllers\Console\UpdateConfigController;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Http\Factory\ServerRequestFactory;
use PhpMyAdmin\Message;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tests\AbstractTestCase;
use PhpMyAdmin\Tests\Stubs\ResponseRenderer;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use function json_decode;
#[CoversClass(UpdateConfigController::class)]
final class UpdateConfigControllerTest extends AbstractTestCase
{
#[DataProvider('validParamsProvider')]
public function testValidParams(string $key, string $value, bool|int|string $expected): void
{
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withParsedBody(['key' => $key, 'value' => $value]);
DatabaseInterface::$instance = $this->createDatabaseInterface();
$config = new Config();
$responseRenderer = new ResponseRenderer();
$responseRenderer->setAjax(true);
$controller = new UpdateConfigController($responseRenderer, new Template($config), $config);
$response = $controller($request);
$responseBody = (string) $response->getBody();
self::assertJson($responseBody);
self::assertSame(
['message' => 'Console settings has been updated successfully.', 'success' => true],
json_decode($responseBody, true),
);
self::assertSame($expected, $config->settings['Console'][$key]);
}
/** @return iterable<array{string, string, bool|int|string}> */
public static function validParamsProvider(): iterable
{
yield ['StartHistory', 'true', true];
yield ['StartHistory', 'false', false];
yield ['AlwaysExpand', 'true', true];
yield ['AlwaysExpand', 'false', false];
yield ['CurrentQuery', 'true', true];
yield ['CurrentQuery', 'false', false];
yield ['EnterExecutes', 'true', true];
yield ['EnterExecutes', 'false', false];
yield ['DarkTheme', 'true', true];
yield ['DarkTheme', 'false', false];
yield ['Mode', 'show', 'show'];
yield ['Mode', 'collapse', 'collapse'];
yield ['Mode', 'info', 'info'];
yield ['Height', '1', 1];
yield ['Height', '92', 92];
yield ['GroupQueries', 'true', true];
yield ['GroupQueries', 'false', false];
yield ['OrderBy', 'exec', 'exec'];
yield ['OrderBy', 'time', 'time'];
yield ['OrderBy', 'count', 'count'];
yield ['Order', 'asc', 'asc'];
yield ['Order', 'desc', 'desc'];
}
/**
* @param string|string[] $key
* @param string|string[] $value
*/
#[DataProvider('invalidParamsProvider')]
public function testInvalidParams(array|string $key, array|string $value): void
{
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withParsedBody(['key' => $key, 'value' => $value]);
DatabaseInterface::$instance = $this->createDatabaseInterface();
$config = new Config();
$responseRenderer = new ResponseRenderer();
$responseRenderer->setAjax(true);
$controller = new UpdateConfigController($responseRenderer, new Template($config), $config);
$response = $controller($request);
$responseBody = (string) $response->getBody();
self::assertJson($responseBody);
self::assertSame(
['success' => false, 'error' => 'Unexpected parameter value.'],
json_decode($responseBody, true),
);
}
/** @return iterable<array{string|string[], string|string[]}> */
public static function invalidParamsProvider(): iterable
{
yield ['StartHistory', ''];
yield ['StartHistory', 'invalid'];
yield ['StartHistory', ['invalid']];
yield ['AlwaysExpand', ''];
yield ['AlwaysExpand', 'invalid'];
yield ['AlwaysExpand', ['invalid']];
yield ['CurrentQuery', ''];
yield ['CurrentQuery', 'invalid'];
yield ['CurrentQuery', ['invalid']];
yield ['EnterExecutes', ''];
yield ['EnterExecutes', 'invalid'];
yield ['EnterExecutes', ['invalid']];
yield ['DarkTheme', ''];
yield ['DarkTheme', 'invalid'];
yield ['DarkTheme', ['invalid']];
yield ['Mode', ''];
yield ['Mode', 'invalid'];
yield ['Mode', ['invalid']];
yield ['Height', ''];
yield ['Height', 'invalid'];
yield ['Height', ['invalid']];
yield ['Height', '0'];
yield ['Height', '-1'];
yield ['GroupQueries', ''];
yield ['GroupQueries', 'invalid'];
yield ['GroupQueries', ['invalid']];
yield ['OrderBy', ''];
yield ['OrderBy', 'invalid'];
yield ['OrderBy', ['invalid']];
yield ['Order', ''];
yield ['Order', 'invalid'];
yield ['Order', ['invalid']];
yield ['', 'invalid'];
yield ['invalid', 'invalid'];
yield [['invalid'], 'invalid'];
}
public function testFailedConfigSaving(): void
{
$request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/')
->withParsedBody(['key' => 'StartHistory', 'value' => 'true']);
$config = self::createStub(Config::class);
$config->method('setUserValue')->willReturn(Message::error('Could not save configuration'));
$responseRenderer = new ResponseRenderer();
$responseRenderer->setAjax(true);
$controller = new UpdateConfigController($responseRenderer, new Template($config), $config);
$response = $controller($request);
$responseBody = (string) $response->getBody();
self::assertJson($responseBody);
self::assertSame(
['success' => false, 'error' => 'Could not save configuration'],
json_decode($responseBody, true),
);
self::assertSame(['message' => 'Could not save configuration'], $responseRenderer->getJSONResult());
self::assertFalse($responseRenderer->hasSuccessState(), 'Should be a failed response.');
}
}

View File

@ -14,6 +14,7 @@ use PhpMyAdmin\Controllers\CollationConnectionController;
use PhpMyAdmin\Controllers\ColumnController;
use PhpMyAdmin\Controllers\Config;
use PhpMyAdmin\Controllers\Console\Bookmark;
use PhpMyAdmin\Controllers\Console\UpdateConfigController;
use PhpMyAdmin\Controllers\Database;
use PhpMyAdmin\Controllers\Database\Structure\CentralColumns;
use PhpMyAdmin\Controllers\DatabaseController;
@ -164,6 +165,7 @@ final class RoutesTest extends TestCase
'/config/get' => Config\GetConfigController::class,
'/config/set' => Config\SetConfigController::class,
'/console/bookmark/add' => Bookmark\AddController::class,
'/console/update-config' => UpdateConfigController::class,
'/database/central-columns' => Database\CentralColumnsController::class,
'/database/central-columns/populate' => Database\CentralColumns\PopulateColumnsController::class,
'/database/designer' => Database\DesignerController::class,

View File

@ -25,6 +25,7 @@ use PhpMyAdmin\Message;
use PhpMyAdmin\Template;
use function is_array;
use function json_encode;
class ResponseRenderer extends \PhpMyAdmin\ResponseRenderer
{
@ -186,4 +187,26 @@ class ResponseRenderer extends \PhpMyAdmin\ResponseRenderer
{
return $this->response;
}
public function response(): Response
{
if ($this->isAjax()) {
$json = $this->getJSONResult();
if ($this->isSuccess) {
$json['success'] = true;
} else {
$json['success'] = false;
$json['error'] = $json['message'];
unset($json['message']);
}
$output = (string) json_encode($json);
} else {
$output = $this->getHTMLResult();
}
$this->response->getBody()->write($output);
return $this->response;
}
}