Merge branch 'QA_4_9-security' into QA_5_1-security

Signed-off-by: Maurício Meneghini Fauth <mauricio@fauth.dev>
This commit is contained in:
Maurício Meneghini Fauth 2022-01-10 18:39:23 -03:00
commit 02e8588404
No known key found for this signature in database
GPG Key ID: 6A16FD38AFC89CC8
58 changed files with 1032 additions and 202 deletions

View File

@ -67,7 +67,7 @@
"pragmarx/google2fa-qrcode": "<1.0.1"
},
"suggest": {
"ext-openssl": "Cookie encryption",
"ext-openssl": "For encryption performance",
"ext-curl": "Updates checking",
"ext-opcache": "Better performance",
"ext-zlib": "For gz import and export",
@ -77,9 +77,12 @@
"ext-mbstring": "For best performance",
"tecnickcom/tcpdf": "For PDF support",
"pragmarx/google2fa-qrcode": "For 2FA authentication",
"code-lts/u2f-php-server": "For FIDO U2F authentication"
"code-lts/u2f-php-server": "For FIDO U2F authentication",
"paragonie/sodium_compat": "For modern encryption support"
},
"require-dev": {
"code-lts/u2f-php-server": "^1.2",
"paragonie/sodium_compat": "^1.17",
"php-webdriver/webdriver": "^1.11",
"phpmyadmin/coding-standard": "^2.1.1",
"phpstan/extension-installer": "^1.1",
@ -87,7 +90,6 @@
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^7.5 || ^8.0 || ^9.0",
"pragmarx/google2fa-qrcode": "^1.0.1",
"code-lts/u2f-php-server": "^1.2",
"symfony/console": "^4.4",
"symfony/finder": "^4.4",
"symfony/twig-bridge": "^4.4",

View File

@ -147,6 +147,13 @@ $cfg['SaveDir'] = '';
*/
//$cfg['SendErrorReports'] = 'always';
/**
* 'URLQueryEncryption' defines whether phpMyAdmin will encrypt sensitive data from the URL query string.
* 'URLQueryEncryptionSecretKey' is a 32 bytes long secret key used to encrypt/decrypt the URL query string.
*/
//$cfg['URLQueryEncryption'] = true;
//$cfg['URLQueryEncryptionSecretKey'] = '';
/**
* You can find more configuration options in the documentation
* in the doc/ folder or at <https://docs.phpmyadmin.net/>.

View File

@ -1498,6 +1498,20 @@ Server connection settings
after logout (doesn't affect config authentication method). Should be
absolute including protocol.
.. config:option:: $cfg['Servers'][$i]['hide_connection_errors']
:type: boolean
:default: false
.. versionadded:: 4.9.8
Whether to show or hide detailed MySQL/MariaDB connection errors on the login page.
.. note::
This error message can contain the target database server hostname or IP address,
which may reveal information about your network to an attacker.
Generic settings
----------------
@ -1787,6 +1801,27 @@ Generic settings
When enabled, a user can drag a file in to their browser and phpMyAdmin will
attempt to import the file.
.. config:option:: $cfg['URLQueryEncryption']
:type: boolean
:default: false
.. versionadded:: 4.9.8
Define whether phpMyAdmin will encrypt sensitive data (like database name
and table name) from the URL query string. Default is to not encrypt the URL
query string.
.. config:option:: $cfg['URLQueryEncryptionSecretKey']
:type: string
:default: ``''``
.. versionadded:: 4.9.8
A secret key used to encrypt/decrypt the URL query string.
Should be 32 bytes long.
Cookie authentication options
-----------------------------

View File

@ -163,7 +163,7 @@ Navigation.loadChildNodes = function (isNode, $expandElem, callback) {
};
}
$.get('index.php?route=/navigation&ajax_request=1', params, function (data) {
$.post('index.php?route=/navigation&ajax_request=1', params, function (data) {
if (typeof data !== 'undefined' && data.success === true) {
$destination.find('div.list_container').remove(); // FIXME: Hack, there shouldn't be a list container there
if (isNode) {

View File

@ -71,6 +71,7 @@ use function trigger_error;
use function unserialize;
use function urldecode;
use function vsprintf;
use function json_decode;
/**
* Core class
@ -1426,4 +1427,37 @@ class Core
return $containerBuilder;
}
/**
* @return void
*/
public static function populateRequestWithEncryptedQueryParams()
{
if (
(! isset($_GET['eq']) || ! is_string($_GET['eq']))
&& (! isset($_POST['eq']) || ! is_string($_POST['eq']))
) {
unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']);
return;
}
$isFromPost = isset($_POST['eq']);
$decryptedQuery = Url::decryptQuery($isFromPost ? $_POST['eq'] : $_GET['eq']);
unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']);
if ($decryptedQuery === null) {
return;
}
$urlQueryParams = (array) json_decode($decryptedQuery);
foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) {
if ($isFromPost) {
$_POST[$urlQueryParamKey] = $urlQueryParamValue;
} else {
$_GET[$urlQueryParamKey] = $urlQueryParamValue;
}
$_REQUEST[$urlQueryParamKey] = $urlQueryParamValue;
}
}
}

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Crypto;
use phpseclib\Crypt\AES;
use phpseclib\Crypt\Random;
use Throwable;
use function is_callable;
use function defined;
use const SODIUM_CRYPTO_SECRETBOX_KEYBYTES;
use function is_string;
use function mb_strlen;
use function random_bytes;
use function hash_hmac;
use function mb_substr;
use function hash_equals;
use const SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
use function sodium_crypto_secretbox;
use function sodium_crypto_secretbox_open;
final class Crypto
{
/** @var bool */
private $hasRandomBytesSupport;
/** @var bool */
private $hasSodiumSupport;
/**
* @param bool $forceFallback Force the usage of the fallback functions.
*/
public function __construct($forceFallback = false)
{
$this->hasRandomBytesSupport = ! $forceFallback && is_callable('random_bytes');
$this->hasSodiumSupport = ! $forceFallback
&& $this->hasRandomBytesSupport
&& is_callable('sodium_crypto_secretbox')
&& is_callable('sodium_crypto_secretbox_open')
&& defined('SODIUM_CRYPTO_SECRETBOX_NONCEBYTES')
&& defined('SODIUM_CRYPTO_SECRETBOX_KEYBYTES');
}
/**
* @param string $plaintext
*
* @return string
*/
public function encrypt($plaintext)
{
if ($this->hasSodiumSupport) {
return $this->encryptWithSodium($plaintext);
}
return $this->encryptWithPhpseclib($plaintext);
}
/**
* @param string $ciphertext
*
* @return string|null
*/
public function decrypt($ciphertext)
{
if ($this->hasSodiumSupport) {
return $this->decryptWithSodium($ciphertext);
}
return $this->decryptWithPhpseclib($ciphertext);
}
/**
* @return string
*/
private function getEncryptionKey()
{
global $PMA_Config;
$keyLength = $this->hasSodiumSupport ? SODIUM_CRYPTO_SECRETBOX_KEYBYTES : 32;
$key = $PMA_Config->get('URLQueryEncryptionSecretKey');
if (is_string($key) && mb_strlen($key, '8bit') === $keyLength) {
return $key;
}
$key = $_SESSION['URLQueryEncryptionSecretKey'] ?? null;
if (is_string($key) && mb_strlen($key, '8bit') === $keyLength) {
return $key;
}
$key = $this->hasRandomBytesSupport ? random_bytes($keyLength) : Random::string($keyLength);
$_SESSION['URLQueryEncryptionSecretKey'] = $key;
return $key;
}
/**
* @param string $plaintext
*
* @return string
*/
private function encryptWithPhpseclib($plaintext)
{
$key = $this->getEncryptionKey();
$cipher = new AES(AES::MODE_CBC);
$iv = $this->hasRandomBytesSupport ? random_bytes(16) : Random::string(16);
$cipher->setIV($iv);
$cipher->setKey($key);
$ciphertext = $cipher->encrypt($plaintext);
$hmac = hash_hmac('sha256', $iv . $ciphertext, $key, true);
return $hmac . $iv . $ciphertext;
}
/**
* @param string $encrypted
*
* @return string|null
*/
private function decryptWithPhpseclib($encrypted)
{
$key = $this->getEncryptionKey();
$hmac = mb_substr($encrypted, 0, 32, '8bit');
$iv = mb_substr($encrypted, 32, 16, '8bit');
$ciphertext = mb_substr($encrypted, 48, null, '8bit');
$calculatedHmac = hash_hmac('sha256', $iv . $ciphertext, $key, true);
if (! hash_equals($hmac, $calculatedHmac)) {
return null;
}
$cipher = new AES(AES::MODE_CBC);
$cipher->setIV($iv);
$cipher->setKey($key);
return $cipher->decrypt($ciphertext);
}
/**
* @param string $plaintext
*
* @return string
*/
private function encryptWithSodium($plaintext)
{
$key = $this->getEncryptionKey();
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
return $nonce . $ciphertext;
}
/**
* @param string $encrypted
*
* @return string|null
*/
private function decryptWithSodium($encrypted)
{
$key = $this->getEncryptionKey();
$nonce = mb_substr($encrypted, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($encrypted, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
try {
$decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $key);
} catch (Throwable $e) {
return null;
}
if ($decrypted === false) {
return null;
}
return $decrypted;
}
}

View File

@ -75,6 +75,8 @@ use function mysqli_init;
use function stripos;
use function trigger_error;
use function mysqli_get_client_info;
use function sprintf;
use const E_USER_ERROR;
/**
* Interface to the MySQL Improved extension (MySQLi)
@ -163,15 +165,27 @@ class DbiMysqli implements DbiExtension
$host = $server['host'];
}
$return_value = $mysqli->real_connect(
$host,
$user,
$password,
'',
$server['port'],
(string) $server['socket'],
$client_flags
);
if ($server['hide_connection_errors']) {
$return_value = @$mysqli->real_connect(
$host,
$user,
$password,
'',
$server['port'],
(string) $server['socket'],
$client_flags
);
} else {
$return_value = $mysqli->real_connect(
$host,
$user,
$password,
'',
$server['port'],
(string) $server['socket'],
$client_flags
);
}
if ($return_value === false || $return_value === null) {
/*
@ -198,6 +212,20 @@ class DbiMysqli implements DbiExtension
return self::connect($user, $password, $server);
}
if ($error_number === 1045 && $server['hide_connection_errors']) {
trigger_error(
sprintf(
__(
'Error 1045: Access denied for user. Additional error information'
. ' may be available, but is being hidden by the %s configuration directive.'
),
'[code][doc@cfg_Servers_hide_connection_errors]'
. '$cfg[\'Servers\'][$i][\'hide_connection_errors\'][/doc][/code]'
),
E_USER_ERROR
);
}
return false;
}

View File

@ -1559,9 +1559,8 @@ class Results
$tmp_image = '<img class="fulltext" src="' . $tmp_image_file . '" alt="'
. $tmp_txt . '" title="' . $tmp_txt . '">';
$tmp_url = Url::getFromRoute('/sql', $url_params_full_text);
return Generator::linkOrButton($tmp_url, $tmp_image);
return Generator::linkOrButton(Url::getFromRoute('/sql'), $url_params_full_text, $tmp_image);
}
/**
@ -1677,16 +1676,14 @@ class Results
'session_max_rows' => $session_max_rows,
'is_browse_distinct' => $this->properties['is_browse_distinct'],
];
$single_order_url = Url::getFromRoute('/sql', $_single_url_params);
$multi_order_url = Url::getFromRoute('/sql', $_multi_url_params);
// Displays the sorting URL
// enable sort order swapping for image
$order_link = $this->getSortOrderLink(
$order_img,
$fields_meta,
$single_order_url,
$multi_order_url
$_single_url_params,
$_multi_url_params
);
$order_link .= $this->getSortOrderHiddenInputs(
@ -1996,10 +1993,10 @@ class Results
*
* @see getTableHeaders()
*
* @param string $order_img the sort order image
* @param stdClass $fields_meta set of field properties
* @param string $order_url the url for sort
* @param string $multi_order_url the url for sort
* @param string $order_img the sort order image
* @param stdClass $fields_meta set of field properties
* @param array $order_url_params the url params for sort
* @param array $multi_order_url_params the url params for sort
*
* @return string the sort order link
*
@ -2008,17 +2005,21 @@ class Results
private function getSortOrderLink(
$order_img,
$fields_meta,
$order_url,
$multi_order_url
$order_url_params,
$multi_order_url_params
) {
$order_link_params = ['class' => 'sortlink'];
$order_link_content = htmlspecialchars($fields_meta->name ?? '');
$inner_link_content = $order_link_content . $order_img
. '<input type="hidden" value="' . $multi_order_url . '">';
. '<input type="hidden" value="'
. Url::getFromRoute('/sql')
. Url::getCommon($multi_order_url_params, '?', false)
. '">';
return Generator::linkOrButton(
$order_url,
Url::getFromRoute('/sql'),
$order_url_params,
$inner_link_content,
$order_link_params
);
@ -2519,6 +2520,8 @@ class Results
$copy_url = null;
$copy_str = null;
$edit_url = null;
$editCopyUrlParams = null;
$delUrlParams = null;
// 1.2 Defines the URLs for the modify/delete link(s)
@ -2559,6 +2562,7 @@ class Results
$copy_url,
$edit_str,
$copy_str,
$editCopyUrlParams,
]
= $this->getModifiedLinks(
$where_clause,
@ -2568,7 +2572,7 @@ class Results
}
// 1.2.2 Delete/Kill link(s)
[$del_url, $del_str, $js_conf]
[$del_url, $del_str, $js_conf, $delUrlParams]
= $this->getDeleteAndKillLinks(
$where_clause,
$clause_is_unique,
@ -2584,9 +2588,18 @@ class Results
$table_body_html .= $this->template->render('display/results/checkbox_and_links', [
'position' => self::POSITION_LEFT,
'has_checkbox' => ! empty($del_url) && $displayParts['del_lnk'] !== self::KILL_PROCESS,
'edit' => ['url' => $edit_url, 'string' => $edit_str, 'clause_is_unique' => $clause_is_unique],
'copy' => ['url' => $copy_url, 'string' => $copy_str],
'delete' => ['url' => $del_url, 'string' => $del_str],
'edit' => [
'url' => $edit_url,
'params' => $editCopyUrlParams + ['default_action' => 'update'],
'string' => $edit_str,
'clause_is_unique' => $clause_is_unique,
],
'copy' => [
'url' => $copy_url,
'params' => $editCopyUrlParams + ['default_action' => 'insert'],
'string' => $copy_str,
],
'delete' => ['url' => $del_url, 'params' => $delUrlParams, 'string' => $del_str],
'row_number' => $row_no,
'where_clause' => $where_clause,
'condition' => json_encode($condition_array),
@ -2597,9 +2610,18 @@ class Results
$table_body_html .= $this->template->render('display/results/checkbox_and_links', [
'position' => self::POSITION_NONE,
'has_checkbox' => ! empty($del_url) && $displayParts['del_lnk'] !== self::KILL_PROCESS,
'edit' => ['url' => $edit_url, 'string' => $edit_str, 'clause_is_unique' => $clause_is_unique],
'copy' => ['url' => $copy_url, 'string' => $copy_str],
'delete' => ['url' => $del_url, 'string' => $del_str],
'edit' => [
'url' => $edit_url,
'params' => $editCopyUrlParams + ['default_action' => 'update'],
'string' => $edit_str,
'clause_is_unique' => $clause_is_unique,
],
'copy' => [
'url' => $copy_url,
'params' => $editCopyUrlParams + ['default_action' => 'insert'],
'string' => $copy_str,
],
'delete' => ['url' => $del_url, 'params' => $delUrlParams, 'string' => $del_str],
'row_number' => $row_no,
'where_clause' => $where_clause,
'condition' => json_encode($condition_array),
@ -2637,11 +2659,16 @@ class Results
'has_checkbox' => ! empty($del_url) && $displayParts['del_lnk'] !== self::KILL_PROCESS,
'edit' => [
'url' => $edit_url,
'params' => $editCopyUrlParams + ['default_action' => 'update'],
'string' => $edit_str,
'clause_is_unique' => $clause_is_unique ?? true,
],
'copy' => ['url' => $copy_url, 'string' => $copy_str],
'delete' => ['url' => $del_url, 'string' => $del_str],
'copy' => [
'url' => $copy_url,
'params' => $editCopyUrlParams + ['default_action' => 'insert'],
'string' => $copy_str,
],
'delete' => ['url' => $del_url, 'params' => $delUrlParams, 'string' => $del_str],
'row_number' => $row_no,
'where_clause' => $where_clause ?? '',
'condition' => json_encode($condition_array ?? []),
@ -3231,7 +3258,7 @@ class Results
* @param bool $clause_is_unique the unique condition of clause
* @param string $url_sql_query the analyzed sql query
*
* @return array<int,string> 5 element array - $edit_url, $copy_url,
* @return array<int,string|array> 5 element array - $edit_url, $copy_url,
* $edit_str, $copy_str
*
* @access private
@ -3250,15 +3277,9 @@ class Results
'goto' => Url::getFromRoute('/sql'),
];
$edit_url = Url::getFromRoute(
'/table/change',
$_url_params + ['default_action' => 'update']
);
$edit_url = Url::getFromRoute('/table/change');
$copy_url = Url::getFromRoute(
'/table/change',
$_url_params + ['default_action' => 'insert']
);
$copy_url = Url::getFromRoute('/table/change');
$edit_str = $this->getActionLinkContent(
'b_edit',
@ -3274,6 +3295,7 @@ class Results
$copy_url,
$edit_str,
$copy_str,
$_url_params,
];
}
@ -3327,7 +3349,7 @@ class Results
'message_to_show' => __('The row has been deleted.'),
'goto' => $lnk_goto,
];
$del_url = Url::getFromRoute('/sql', $_url_params);
$del_url = Url::getFromRoute('/sql');
$js_conf = 'DELETE FROM ' . $this->properties['table']
. ' WHERE ' . $where_clause
@ -3352,20 +3374,21 @@ class Results
'goto' => $lnk_goto,
];
$del_url = Url::getFromRoute('/sql', $_url_params);
$del_url = Url::getFromRoute('/sql');
$js_conf = $kill;
$del_str = Generator::getIcon(
'b_drop',
__('Kill')
);
} else {
$del_url = $del_str = $js_conf = null;
$del_url = $del_str = $js_conf = $_url_params = null;
}
return [
$del_url,
$del_str,
$js_conf,
$_url_params,
];
}
@ -5067,7 +5090,8 @@ class Results
$tag_params['class'] = 'ajax';
}
$result .= Generator::linkOrButton(
Url::getFromRoute('/sql', $_url_params),
Url::getFromRoute('/sql'),
$_url_params,
$displayedData,
$tag_params
);

View File

@ -222,6 +222,7 @@ class ErrorReport
if (isset($components['query'])) {
parse_str($components['query'], $queryArray);
unset($queryArray['db'], $queryArray['table'], $queryArray['token'], $queryArray['server']);
unset($queryArray['eq']);
$query = http_build_query($queryArray);
} else {
$query = '';

View File

@ -565,16 +565,18 @@ class Export
*/
$back_button = '<p id="export_back_button">[ <a href="';
if ($export_type === 'server') {
$back_button .= Url::getFromRoute('/server/export') . '" data-post="' . Url::getCommon([], '');
$back_button .= Url::getFromRoute('/server/export') . '" data-post="' . Url::getCommon([], '', false);
} elseif ($export_type === 'database') {
$back_button .= Url::getFromRoute('/database/export') . '" data-post="' . Url::getCommon(['db' => $db], '');
$back_button .= Url::getFromRoute('/database/export') . '" data-post="' . Url::getCommon(
['db' => $db],
'',
false
);
} else {
$back_button .= Url::getFromRoute('/table/export') . '" data-post="' . Url::getCommon(
[
'db' => $db,
'table' => $table,
],
''
['db' => $db, 'table' => $table],
'',
false
);
}

View File

@ -653,7 +653,8 @@ class Generator
$explain_params['sql_query'] = 'EXPLAIN ' . $sql_query;
$explain_link = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $explain_params),
Url::getFromRoute('/import'),
$explain_params,
__('Explain SQL')
) . '&nbsp;]';
} elseif (preg_match(
@ -664,7 +665,8 @@ class Generator
= mb_substr($sql_query, 8);
$explain_link = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $explain_params),
Url::getFromRoute('/import'),
$explain_params,
__('Skip Explain SQL')
) . ']';
$url = 'https://mariadb.org/explain_analyzer/analyze/'
@ -673,6 +675,7 @@ class Generator
$explain_link .= ' ['
. self::linkOrButton(
htmlspecialchars('url.php?url=' . urlencode($url)),
null,
sprintf(__('Analyze Explain at %s'), 'mariadb.org'),
[],
'_blank',
@ -689,9 +692,8 @@ class Generator
if (! empty($cfg['SQLQuery']['Edit'])
&& empty($GLOBALS['show_as_php'])
) {
$edit_link .= Url::getCommon($url_params, '&');
$edit_link = ' [&nbsp;'
. self::linkOrButton($edit_link, __('Edit'))
. self::linkOrButton($edit_link, $url_params, __('Edit'))
. '&nbsp;]';
} else {
$edit_link = '';
@ -703,14 +705,16 @@ class Generator
if (! empty($GLOBALS['show_as_php'])) {
$php_link = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $url_params),
Url::getFromRoute('/import'),
$url_params,
__('Without PHP code')
)
. '&nbsp;]';
$php_link .= ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $url_params),
Url::getFromRoute('/import'),
$url_params,
__('Submit query')
)
. '&nbsp;]';
@ -719,7 +723,8 @@ class Generator
$php_params['show_as_php'] = 1;
$php_link = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $php_params),
Url::getFromRoute('/import'),
$php_params,
__('Create PHP code')
)
. '&nbsp;]';
@ -733,9 +738,9 @@ class Generator
&& ! isset($GLOBALS['show_as_php']) // 'Submit query' does the same
&& preg_match('@^(SELECT|SHOW)[[:space:]]+@i', $sql_query)
) {
$refresh_link = Url::getFromRoute('/sql', $url_params);
$refresh_link = Url::getFromRoute('/sql');
$refresh_link = ' [&nbsp;'
. self::linkOrButton($refresh_link, __('Refresh')) . '&nbsp;]';
. self::linkOrButton($refresh_link, $url_params, __('Refresh')) . '&nbsp;]';
} else {
$refresh_link = '';
}
@ -770,6 +775,7 @@ class Generator
$inline_edit_link = ' [&nbsp;'
. self::linkOrButton(
'#',
null,
_pgettext('Inline edit query', 'Edit inline'),
['class' => 'inline_edit_sql']
)
@ -1089,21 +1095,27 @@ class Generator
* - URL components are over Suhosin limits
* - There is SQL query in the parameters
*
* @param string $url the URL
* @param string $message the link message
* @param mixed $tag_params string: js confirmation; array: additional tag
* params (f.e. style="")
* @param string $target target
* @param string $urlPath the URL
* @param array|null $urlParams URL parameters
* @param string $message the link message
* @param mixed $tag_params string: js confirmation; array: additional tag params (f.e. style="")
* @param string $target target
*
* @return string the results to be echoed or saved in an array
*/
public static function linkOrButton(
$url,
$urlPath,
$urlParams,
$message,
$tag_params = [],
$target = '',
bool $respectUrlLengthLimit = true
): string {
$url = $urlPath;
if (is_array($urlParams)) {
$url = $urlPath . Url::getCommon($urlParams, '?', false);
}
$url_length = strlen($url);
if (! is_array($tag_params)) {
@ -1165,6 +1177,11 @@ class Generator
) {
$url .= '?' . explode('&', $parts[1], 2)[0];
}
} else {
$url = $urlPath;
if (is_array($urlParams)) {
$url = $urlPath . Url::getCommon($urlParams);
}
}
foreach ($tag_params as $par_name => $par_value) {

View File

@ -329,13 +329,13 @@ class InsertEdit
if (! $is_show) {
return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="'
. Url::getCommon($this_url_params, '') . '">'
. Url::getCommon($this_url_params, '', false) . '">'
. $this->showTypeOrFunctionLabel($which)
. '</a>';
}
return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
. Url::getCommon($this_url_params, '')
. Url::getCommon($this_url_params, '', false)
. '" title="' . __('Hide') . '">'
. $this->showTypeOrFunctionLabel($which)
. '</a></th>';
@ -920,7 +920,8 @@ class InsertEdit
'rownumber' => $rownumber,
'data' => $data,
],
''
'',
false
) . '">'
. Generator::getIcon('b_browse', __('Browse foreign values')) . '</a>';
@ -1788,6 +1789,7 @@ class InsertEdit
return '<span class="open_gis_editor" data-row-id="' . $rowId . '">'
. Generator::linkOrButton(
'#',
null,
$edit_str,
[],
'_blank'

View File

@ -52,6 +52,9 @@ use function trim;
use function urlencode;
use function usort;
use function vsprintf;
use function parse_url;
use function parse_str;
use function htmlspecialchars_decode;
/**
* Displays a collapsible of database objects in the navigation frame
@ -145,13 +148,13 @@ class NavigationTree
$this->pos = $this->getNavigationDbPos();
}
// Get the active node
if (isset($_REQUEST['aPath'])) {
$this->aPath[0] = $this->parsePath($_REQUEST['aPath']);
$this->pos2Name[0] = $_REQUEST['pos2_name'] ?? '';
$this->pos2Value[0] = (int) ($_REQUEST['pos2_value'] ?? 0);
if (isset($_REQUEST['pos3_name'])) {
$this->pos3Name[0] = $_REQUEST['pos3_name'] ?? '';
$this->pos3Value[0] = (int) $_REQUEST['pos3_value'];
if (isset($_POST['aPath'])) {
$this->aPath[0] = $this->parsePath($_POST['aPath']);
$this->pos2Name[0] = $_POST['pos2_name'] ?? '';
$this->pos2Value[0] = (int) ($_POST['pos2_value'] ?? 0);
if (isset($_POST['pos3_name'])) {
$this->pos3Name[0] = $_POST['pos3_name'] ?? '';
$this->pos3Value[0] = (int) $_POST['pos3_value'];
}
} else {
if (isset($_POST['n0_aPath'])) {
@ -172,8 +175,8 @@ class NavigationTree
}
}
}
if (isset($_REQUEST['vPath'])) {
$this->vPath[0] = $this->parsePath($_REQUEST['vPath']);
if (isset($_POST['vPath'])) {
$this->vPath[0] = $this->parsePath($_POST['vPath']);
} else {
if (isset($_POST['n0_vPath'])) {
$count = 0;
@ -185,11 +188,11 @@ class NavigationTree
}
}
}
if (isset($_REQUEST['searchClause'])) {
$this->searchClause = $_REQUEST['searchClause'];
if (isset($_POST['searchClause'])) {
$this->searchClause = $_POST['searchClause'];
}
if (isset($_REQUEST['searchClause2'])) {
$this->searchClause2 = $_REQUEST['searchClause2'];
if (isset($_POST['searchClause2'])) {
$this->searchClause2 = $_POST['searchClause2'];
}
// Initialize the tree by creating a root node
$node = NodeFactory::getInstance('NodeDatabaseContainer', 'root');
@ -1193,7 +1196,7 @@ class NavigationTree
}
foreach ($icons as $key => $icon) {
$link = vsprintf($iconLinks[$key], $args);
$link = $this->encryptQueryParams(vsprintf($iconLinks[$key], $args));
if ($linkClass != '') {
$retval .= "<a class='" . $linkClass . "' href='" . $link . "'>";
$retval .= '' . $icon . '</a>';
@ -1211,7 +1214,7 @@ class NavigationTree
foreach ($node->parents(true) as $parent) {
$args[] = urlencode($parent->realName);
}
$link = vsprintf($node->links['text'], $args);
$link = $this->encryptQueryParams(vsprintf($node->links['text'], $args));
$title = $node->links['title'] ?? $node->title ?? '';
if ($nodeIsContainer) {
$retval .= "&nbsp;<a class='hover_show_full' href='" . $link . "'>";
@ -1601,4 +1604,23 @@ class NavigationTree
return $retval;
}
/**
* @param string $link
*
* @return string
*/
private function encryptQueryParams($link)
{
global $PMA_Config;
if (! $PMA_Config->get('URLQueryEncryption')) {
return $link;
}
$url = parse_url($link);
parse_str(htmlspecialchars_decode($url['query']), $query);
return $url['path'] . '?' . htmlspecialchars(Url::buildHttpQuery($query));
}
}

View File

@ -702,7 +702,7 @@ class NodeDatabase extends Node
];
$ret = '<span class="dbItemControls">'
. '<a href="' . Url::getFromRoute('/navigation') . '" data-post="'
. Url::getCommon($params, '') . '"'
. Url::getCommon($params, '', false) . '"'
. ' class="showUnhide ajax">'
. Generator::getImage(
'show',

View File

@ -46,7 +46,7 @@ abstract class NodeDatabaseChild extends Node
$ret = '<span class="navItemControls">'
. '<a href="' . Url::getFromRoute('/navigation') . '" data-post="'
. Url::getCommon($params, '') . '"'
. Url::getCommon($params, '', false) . '"'
. ' class="hideNavItem ajax">'
. Generator::getImage('hide', __('Hide'))
. '</a></span>';

View File

@ -2223,7 +2223,8 @@ class Relation
);
}
$message->addParamHtml(
'<a href="' . Url::getFromRoute('/check-relations') . '" data-post="' . Url::getCommon($params, '') . '">'
'<a href="' . Url::getFromRoute('/check-relations')
. '" data-post="' . Url::getCommon($params, '', false) . '">'
);
$message->addParamHtml('</a>');

View File

@ -140,7 +140,7 @@ class ReplicationGui
}
$urlParams['sr_slave_control_parm'] = 'IO_THREAD';
$slaveControlIoLink = Url::getCommon($urlParams, '');
$slaveControlIoLink = Url::getCommon($urlParams, '', false);
if ($serverSlaveReplication[0]['Slave_SQL_Running'] === 'No') {
$urlParams['sr_slave_action'] = 'start';
@ -149,7 +149,7 @@ class ReplicationGui
}
$urlParams['sr_slave_control_parm'] = 'SQL_THREAD';
$slaveControlSqlLink = Url::getCommon($urlParams, '');
$slaveControlSqlLink = Url::getCommon($urlParams, '', false);
if ($serverSlaveReplication[0]['Slave_IO_Running'] === 'No'
|| $serverSlaveReplication[0]['Slave_SQL_Running'] === 'No'
@ -160,21 +160,21 @@ class ReplicationGui
}
$urlParams['sr_slave_control_parm'] = null;
$slaveControlFullLink = Url::getCommon($urlParams, '');
$slaveControlFullLink = Url::getCommon($urlParams, '', false);
$urlParams['sr_slave_action'] = 'reset';
$slaveControlResetLink = Url::getCommon($urlParams, '');
$slaveControlResetLink = Url::getCommon($urlParams, '', false);
$urlParams = $GLOBALS['url_params'];
$urlParams['sr_take_action'] = true;
$urlParams['sr_slave_skip_error'] = true;
$slaveSkipErrorLink = Url::getCommon($urlParams, '');
$slaveSkipErrorLink = Url::getCommon($urlParams, '', false);
$urlParams = $GLOBALS['url_params'];
$urlParams['sl_configure'] = true;
$urlParams['repl_clear_scr'] = true;
$reconfigureMasterLink = Url::getCommon($urlParams, '');
$reconfigureMasterLink = Url::getCommon($urlParams, '', false);
$slaveStatusTable = $this->getHtmlForReplicationStatusTable('slave', true, false);

View File

@ -1606,7 +1606,7 @@ class Privileges
$html .= ' href="' . Url::getFromRoute('/server/privileges');
if ($linktype === 'revoke') {
$html .= '" data-post="' . Url::getCommon($params, '');
$html .= '" data-post="' . Url::getCommon($params, '', false);
} else {
$html .= Url::getCommon($params, '&');
}

View File

@ -111,7 +111,8 @@ class UserGroups
'viewUsers' => 1,
'userGroup' => $groupName,
],
''
'',
false
);
$userGroupVal['viewUsersIcon'] = Generator::getIcon('b_usrlist', __('View users'));
@ -120,7 +121,8 @@ class UserGroups
'editUserGroup' => 1,
'userGroup' => $groupName,
],
''
'',
false
);
$userGroupVal['editUsersIcon'] = Generator::getIcon('b_edit', __('Edit'));
@ -129,7 +131,8 @@ class UserGroups
'deleteUserGroup' => 1,
'userGroup' => $groupName,
],
''
'',
false
);
$userGroupVal['deleteUsersIcon'] = Generator::getIcon('b_drop', __('Delete'));
$userGroupsValues[] = $userGroupVal;

View File

@ -7,6 +7,8 @@ declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Crypto\Crypto;
use function htmlentities;
use function htmlspecialchars;
use function http_build_query;
@ -14,6 +16,11 @@ use function ini_get;
use function is_array;
use function mb_strpos;
use function strlen;
use function in_array;
use function json_encode;
use function strtr;
use function base64_encode;
use function base64_decode;
/**
* Static methods for URL/hidden inputs generating
@ -166,14 +173,15 @@ class Url
*
* @param array<string,int|string|bool> $params optional, Contains an associative array with url params
* @param string $divider optional character to use instead of '?'
* @param bool $encrypt whether to encrypt URL params
*
* @return string string with URL parameters
*
* @access public
*/
public static function getCommon(array $params = [], $divider = '?')
public static function getCommon(array $params = [], $divider = '?', $encrypt = true)
{
return self::getCommonRaw($params, $divider);
return self::getCommonRaw($params, $divider, $encrypt);
}
/**
@ -201,17 +209,16 @@ class Url
*
* @param array<string|int,int|string|bool> $params optional, Contains an associative array with url params
* @param string $divider optional character to use instead of '?'
* @param bool $encrypt whether to encrypt URL params
*
* @return string string with URL parameters
*
* @access public
*/
public static function getCommonRaw(array $params = [], $divider = '?')
public static function getCommonRaw(array $params = [], $divider = '?', $encrypt = true)
{
global $PMA_Config;
$separator = self::getArgSeparator();
// avoid overwriting when creating navigation panel links to servers
if (isset($GLOBALS['server'])
&& $GLOBALS['server'] != $GLOBALS['cfg']['ServerDefault']
@ -227,7 +234,7 @@ class Url
$params['lang'] = $GLOBALS['lang'];
}
$query = http_build_query($params, '', $separator);
$query = self::buildHttpQuery($params, $encrypt);
if (($divider !== '?' && $divider !== '&') || strlen($query) > 0) {
return $divider . $query;
@ -236,6 +243,81 @@ class Url
return '';
}
/**
* @param array<string, mixed> $params
* @param bool $encrypt whether to encrypt URL params
*
* @return string
*/
public static function buildHttpQuery($params, $encrypt = true)
{
global $PMA_Config;
$separator = self::getArgSeparator();
if (! $encrypt || ! $PMA_Config->get('URLQueryEncryption')) {
return http_build_query($params, '', $separator);
}
$data = $params;
$keys = [
'db',
'table',
'field',
'sql_query',
'sql_signature',
'where_clause',
'goto',
'back',
'message_to_show',
'username',
'hostname',
'dbname',
'tablename',
'checkprivsdb',
'checkprivstable',
];
$paramsToEncrypt = [];
foreach ($params as $paramKey => $paramValue) {
if (! in_array($paramKey, $keys)) {
continue;
}
$paramsToEncrypt[$paramKey] = $paramValue;
unset($data[$paramKey]);
}
if ($paramsToEncrypt !== []) {
$data['eq'] = self::encryptQuery(json_encode($paramsToEncrypt));
}
return http_build_query($data, '', $separator);
}
/**
* @param string $query
*
* @return string
*/
public static function encryptQuery($query)
{
$crypto = new Crypto();
return strtr(base64_encode($crypto->encrypt($query)), '+/', '-_');
}
/**
* @param string $query
*
* @return string|null
*/
public static function decryptQuery($query)
{
$crypto = new Crypto();
return $crypto->decrypt(base64_decode(strtr($query, '-_', '+/')));
}
/**
* Returns url separator
*

View File

@ -3171,9 +3171,9 @@ class Util
$urlParams['tbl_group'] = $_REQUEST['tbl_group'];
}
$url = Url::getFromRoute('/database/structure', $urlParams);
$url = Url::getFromRoute('/database/structure');
return Generator::linkOrButton($url, $title . $orderImg, $orderLinkParams);
return Generator::linkOrButton($url, $urlParams, $title . $orderImg, $orderLinkParams);
}
/**

View File

@ -161,6 +161,8 @@ if (! defined('PMA_NO_SESSION')) {
Session::setUp($PMA_Config, $error_handler);
}
Core::populateRequestWithEncryptedQueryParams();
/**
* init some variables LABEL_variables_init
*/

View File

@ -606,6 +606,13 @@ $cfg['Servers'][$i]['tracking_add_drop_table'] = true;
*/
$cfg['Servers'][$i]['tracking_add_drop_database'] = true;
/**
* Whether to show or hide detailed MySQL/MariaDB connection errors on the login page.
*
* @global bool $cfg['Servers'][$i]['hide_connection_errors']
*/
$cfg['Servers'][$i]['hide_connection_errors'] = false;
/**
* Default server (0 = no default server)
*
@ -839,6 +846,20 @@ $cfg['UseDbSearch'] = true;
*/
$cfg['IgnoreMultiSubmitErrors'] = false;
/**
* Define whether phpMyAdmin will encrypt sensitive data from the URL query string.
*
* @global bool $cfg['URLQueryEncryption']
*/
$cfg['URLQueryEncryption'] = false;
/**
* A secret key used to encrypt/decrypt the URL query string. Should be 32 bytes long.
*
* @global string $cfg['URLQueryEncryptionSecretKey']
*/
$cfg['URLQueryEncryptionSecretKey'] = '';
/**
* allow login to any user entered server in cookie based authentication
*

View File

@ -507,7 +507,7 @@ parameters:
-
message: "#^Cannot call method real_connect\\(\\) on mysqli\\|false\\.$#"
count: 1
count: 2
path: libraries/classes/Dbal/DbiMysqli.php
-
@ -525,6 +525,16 @@ parameters:
count: 2
path: libraries/classes/Dbal/DbiMysqli.php
-
message: "#^Binary operation \"\\+\" between array\\|string\\|null and array\\{default_action\\: 'insert'\\} results in an error\\.$#"
count: 3
path: libraries/classes/Display/Results.php
-
message: "#^Binary operation \"\\+\" between array\\|string\\|null and array\\{default_action\\: 'update'\\} results in an error\\.$#"
count: 3
path: libraries/classes/Display/Results.php
-
message: "#^Call to an undefined method object\\:\\:getMIMESubtype\\(\\)\\.$#"
count: 1
@ -1220,6 +1230,16 @@ parameters:
count: 1
path: libraries/classes/Menu.php
-
message: "#^Cannot access offset 'path' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#"
count: 1
path: libraries/classes/Navigation/NavigationTree.php
-
message: "#^Cannot access offset 'query' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#"
count: 1
path: libraries/classes/Navigation/NavigationTree.php
-
message: "#^Property PhpMyAdmin\\\\Navigation\\\\NavigationTree\\:\\:\\$pos \\(int\\) in isset\\(\\) is not nullable\\.$#"
count: 1
@ -2545,6 +2565,16 @@ parameters:
count: 1
path: libraries/classes/Types.php
-
message: "#^Parameter \\#1 \\$params of static method PhpMyAdmin\\\\Url\\:\\:buildHttpQuery\\(\\) expects array\\<string, mixed\\>, array\\<int\\|string, mixed\\> given\\.$#"
count: 1
path: libraries/classes/Url.php
-
message: "#^Parameter \\#1 \\$query of static method PhpMyAdmin\\\\Url\\:\\:encryptQuery\\(\\) expects string, string\\|false given\\.$#"
count: 1
path: libraries/classes/Url.php
-
message: "#^Cannot use array destructuring on array\\|null\\.$#"
count: 1
@ -2620,6 +2650,16 @@ parameters:
count: 1
path: test/classes/Controllers/ExportTemplateControllerTest.php
-
message: "#^Method PhpMyAdmin\\\\Tests\\\\CoreTest\\:\\:providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam\\(\\) should return array\\<array\\<array\\<string\\>\\>\\> but returns array\\<int, array\\<int, array\\<string, array\\|string\\>\\>\\>\\.$#"
count: 1
path: test/classes/CoreTest.php
-
message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'URLQueryEncryptionS…' and array\\<string, mixed\\> will always evaluate to false\\.$#"
count: 1
path: test/classes/Crypto/CryptoTest.php
-
message: "#^Parameter \\#1 \\$result of method PhpMyAdmin\\\\Dbal\\\\DbiMysqli\\:\\:numRows\\(\\) expects mysqli_result, false given\\.$#"
count: 1
@ -2690,6 +2730,16 @@ parameters:
count: 1
path: test/classes/LanguageTest.php
-
message: "#^Cannot access offset 'query' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#"
count: 1
path: test/classes/Navigation/NavigationTreeTest.php
-
message: "#^Parameter \\#1 \\$actualJson of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertJson\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: test/classes/Navigation/NavigationTreeTest.php
-
message: "#^Parameter \\#2 \\$name of static method PhpMyAdmin\\\\Navigation\\\\NodeFactory\\:\\:getInstance\\(\\) expects string, array\\<string, string\\> given\\.$#"
count: 1
@ -2805,6 +2855,11 @@ parameters:
count: 1
path: test/classes/TrackingTest.php
-
message: "#^Parameter \\#1 \\$actualJson of method PHPUnit\\\\Framework\\\\Assert\\:\\:assertJson\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: test/classes/UrlTest.php
-
message: "#^Property PhpMyAdmin\\\\Tests\\\\Selenium\\\\ChangePasswordTest\\:\\:\\$verificationErrors is never written, only read\\.$#"
count: 1

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.17.0@6f4707aa41c9174353a6434bba3fc8840f981d9c">
<files psalm-version="4.18.1@dda05fa913f4dc6eb3386f2f7ce5a45d37a71bcb">
<file src="libraries/classes/Bookmark.php">
<RedundantCastGivenDocblockType occurrences="1">
<code>(int) $this-&gt;id</code>
@ -593,6 +593,14 @@
<code>$_POST['field_extra'][$i] ?? false</code>
</PossiblyFalseArgument>
</file>
<file src="libraries/classes/Crypto/Crypto.php">
<InvalidReturnStatement occurrences="1">
<code>$decrypted</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>string|null</code>
</InvalidReturnType>
</file>
<file src="libraries/classes/Database/CentralColumns.php">
<InvalidScalarArgument occurrences="1">
<code>$tn_pageNow</code>
@ -812,6 +820,14 @@
<code>$col_visib</code>
<code>$sortExpressionNoDirection</code>
</PossiblyInvalidArgument>
<PossiblyInvalidOperand occurrences="6">
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
</PossiblyInvalidOperand>
<PossiblyNullArgument occurrences="4">
<code>$col_visib_current</code>
<code>$col_visib_current</code>
@ -1312,6 +1328,10 @@
<PossiblyNullReference occurrences="1">
<code>addChild</code>
</PossiblyNullReference>
<PossiblyUndefinedArrayOffset occurrences="2">
<code>$url['path']</code>
<code>$url['query']</code>
</PossiblyUndefinedArrayOffset>
<RedundantCastGivenDocblockType occurrences="5">
<code>(string) $child-&gt;name</code>
<code>(string) $child-&gt;name</code>
@ -2752,6 +2772,9 @@
</RedundantCastGivenDocblockType>
</file>
<file src="libraries/classes/Url.php">
<InvalidScalarArgument occurrences="1">
<code>$params</code>
</InvalidScalarArgument>
<RedundantCastGivenDocblockType occurrences="2">
<code>(string) $db</code>
<code>(string) $table</code>

View File

@ -61,11 +61,12 @@
<td>
{% if has_privilege %}
{{ link_or_button(
url('/sql', {
url('/sql'),
{
'db': db,
'sql_query': 'DROP EVENT IF EXISTS %s'|format(backquote(event.name)),
'goto': url('/database/events', {'db': db})
}),
},
get_icon('b_drop', 'Drop'|trans),
{'class': 'ajax drop_anchor'}
) }}

View File

@ -36,12 +36,13 @@
<td>
{% if has_privilege %}
{{ link_or_button(
url('/sql', {
url('/sql'),
{
'db': db,
'table': table,
'sql_query': sql_drop,
'goto': url('/database/events', {'db': db})
}),
},
get_icon('b_drop', 'Drop'|trans),
{'class': 'ajax drop_anchor'}
) }}

View File

@ -90,7 +90,8 @@
<div class="card-body">
<div class="card-text">
{{ link_or_button(
url('/sql', {
url('/sql'),
{
'sql_query': 'DROP DATABASE ' ~ backquote(db),
'back': url('/database/operations'),
'goto': url('/'),
@ -98,7 +99,7 @@
'purge': true,
'message_to_show': 'Database %s has been dropped.'|trans|format(backquote(db))|e,
'db': null
}),
},
'Drop the database (DROP)'|trans,
{
'id': 'drop_db_anchor',

View File

@ -63,12 +63,13 @@
</td>
<td>
{{ link_or_button(
url('/sql', {
url('/sql'),
{
'db': db,
'table': table,
'sql_query': sql_drop,
'goto': url('/database/routines', {'db': db})
}),
},
get_icon('b_drop', 'Drop'|trans),
{'class': 'ajax drop_anchor'}
) }}

View File

@ -89,7 +89,7 @@
'back': url('/database/tracking'),
'table': version.table_name,
'delete_tracking': true
}, '') }}">
}, '', false) }}">
{{ get_icon('b_drop', 'Delete tracking'|trans) }}
</a>
</td>
@ -100,7 +100,7 @@
'goto': url('/table/tracking'),
'back': url('/database/tracking'),
'table': version.table_name
}, '') }}">
}, '', false) }}">
{{ get_icon('b_versions', 'Versions'|trans) }}
</a>
<a href="{{ url('/table/tracking') }}" data-post="
@ -111,7 +111,7 @@
'table': version.table_name,
'report': true,
'version': version.version
}, '') }}">
}, '', false) }}">
{{ get_icon('b_report', 'Tracking report'|trans) }}
</a>
<a href="{{ url('/table/tracking') }}" data-post="
@ -122,7 +122,7 @@
'table': version.table_name,
'snapshot': true,
'version': version.version
}, '') }}">
}, '', false) }}">
{{ get_icon('b_props', 'Structure snapshot'|trans) }}
</a>
</td>

View File

@ -38,12 +38,13 @@
<td>
{% if has_drop_privilege %}
{{ link_or_button(
url('/sql', {
url('/sql'),
{
'db': db,
'table': table,
'sql_query': trigger.drop,
'goto': url('/database/triggers', {'db': db})
}),
},
get_icon('b_drop', 'Drop'|trans),
{'class': 'ajax drop_anchor'}
) }}

View File

@ -10,7 +10,7 @@
{% if edit.url is not empty %}
<td class="text-center print_ignore edit_row_anchor{{ not edit.clause_is_unique ? ' nonunique' }}">
<span class="nowrap">
{{ link_or_button(edit.url, edit.string) }}
{{ link_or_button(edit.url, edit.params, edit.string) }}
{% if where_clause is not empty %}
<input type="hidden" class="where_clause" value="{{ where_clause }}">
{% endif %}
@ -21,7 +21,7 @@
{% if copy.url is not empty %}
<td class="text-center print_ignore">
<span class="nowrap">
{{ link_or_button(copy.url, copy.string) }}
{{ link_or_button(copy.url, copy.params, copy.string) }}
{% if where_clause is not empty %}
<input type="hidden" class="where_clause" value="{{ where_clause }}">
{% endif %}
@ -32,7 +32,7 @@
{% if delete.url is not empty %}
<td class="text-center print_ignore{{ is_ajax ? ' ajax' }}">
<span class="nowrap">
{{ link_or_button(delete.url, delete.string, {'class': 'delete_row requireConfirm' ~ (is_ajax ? ' ajax') }) }}
{{ link_or_button(delete.url, delete.params, delete.string, {'class': 'delete_row requireConfirm' ~ (is_ajax ? ' ajax') }) }}
{% if js_conf is not empty %}
<div class="hide">{{ js_conf }}</div>
{% endif %}
@ -43,7 +43,7 @@
{% if delete.url is not empty %}
<td class="text-center print_ignore{{ is_ajax ? ' ajax' }}">
<span class="nowrap">
{{ link_or_button(delete.url, delete.string, {'class': 'delete_row requireConfirm' ~ (is_ajax ? ' ajax') }) }}
{{ link_or_button(delete.url, delete.params, delete.string, {'class': 'delete_row requireConfirm' ~ (is_ajax ? ' ajax') }) }}
{% if js_conf is not empty %}
<div class="hide">{{ js_conf }}</div>
{% endif %}
@ -54,7 +54,7 @@
{% if copy.url is not empty %}
<td class="text-center print_ignore">
<span class="nowrap">
{{ link_or_button(copy.url, copy.string) }}
{{ link_or_button(copy.url, copy.params, copy.string) }}
{% if where_clause is not empty %}
<input type="hidden" class="where_clause" value="{{ where_clause }}">
{% endif %}
@ -65,7 +65,7 @@
{% if edit.url is not empty %}
<td class="text-center print_ignore edit_row_anchor{{ not edit.clause_is_unique ? ' nonunique' }}">
<span class="nowrap">
{{ link_or_button(edit.url, edit.string) }}
{{ link_or_button(edit.url, edit.params, edit.string) }}
{% if where_clause is not empty %}
<input type="hidden" class="where_clause" value="{{ where_clause }}">
{% endif %}

View File

@ -240,6 +240,7 @@
{% if operations.has_print_link %}
{{ link_or_button(
'#',
null,
get_icon('b_print', 'Print'|trans, true),
{'id': 'printView', 'class': 'btn'},
'print_view'
@ -247,6 +248,7 @@
{{ link_or_button(
'#',
null,
get_icon('b_insrow', 'Copy to clipboard'|trans, true),
{'id': 'copyToClipBoard', 'class': 'btn'}
) }}
@ -255,20 +257,23 @@
{% if not operations.has_procedure %}
{% if operations.has_export_link %}
{{ link_or_button(
url('/table/export', operations.url_params),
url('/table/export'),
operations.url_params,
get_icon('b_tblexport', 'Export'|trans, true),
{'class': 'btn'}
) }}
{{ link_or_button(
url('/table/chart', operations.url_params),
url('/table/chart'),
operations.url_params,
get_icon('b_chart', 'Display chart'|trans, true),
{'class': 'btn'}
) }}
{% if operations.has_geometry %}
{{ link_or_button(
url('/table/gis-visualization', operations.url_params),
url('/table/gis-visualization'),
operations.url_params,
get_icon('b_globe', 'Visualize GIS data'|trans, true),
{'class': 'btn'}
) }}
@ -277,7 +282,8 @@
<span>
{{ link_or_button(
url('/view/create', {'db': db, 'table': table, 'sql_query': sql_query, 'printview': true}),
url('/view/create'),
{'db': db, 'table': table, 'sql_query': sql_query, 'printview': true},
get_icon('b_view_add', 'Create view'|trans, true),
{'class': 'btn create_view ajax'}
) }}

View File

@ -29,12 +29,12 @@
{% set columns_count = index.getColumnCount() %}
<tr class="noclick">
<td rowspan="{{ columns_count }}" class="edit_index print_ignore ajax">
<a class="ajax" href="{{ url('/table/indexes') }}" data-post="{{ get_common(url_params|merge({'index': index.getName()}), '') }}">
<a class="ajax" href="{{ url('/table/indexes') }}" data-post="{{ get_common(url_params|merge({'index': index.getName()}), '', false) }}">
{{ get_icon('b_edit', 'Edit'|trans) }}
</a>
</td>
<td rowspan="{{ columns_count }}" class="rename_index print_ignore ajax" >
<a class="ajax" href="{{ url('/table/indexes/rename') }}" data-post="{{ get_common(url_params|merge({'index': index.getName()}), '') }}">
<a class="ajax" href="{{ url('/table/indexes/rename') }}" data-post="{{ get_common(url_params|merge({'index': index.getName()}), '', false) }}">
{{ get_icon('b_rename', 'Rename'|trans) }}
</a>
</td>
@ -53,7 +53,8 @@
<input type="hidden" class="drop_primary_key_index_msg" value="{{ index_params.sql_query }}">
{{ link_or_button(
url('/sql', url_params|merge(index_params)),
url('/sql'),
url_params|merge(index_params),
get_icon('b_drop', 'Drop'|trans),
{'class': 'drop_primary_key_index_anchor ajax'}
) }}

View File

@ -17,7 +17,7 @@
'itemType': type,
'itemName': item,
'dbName': database
}, '') }}">{{ get_icon('show', 'Unhide'|trans) }}</a>
}, '', false) }}">{{ get_icon('show', 'Unhide'|trans) }}</a>
</td>
</tr>
{% endfor %}

View File

@ -43,12 +43,12 @@
<td colspan="6" class="text-center">
{% if has_previous %}
{% if has_icons %}
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(previous_params, '') }}" title="
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(previous_params, '', false) }}" title="
{%- trans %}Previous{% context %}Previous page{% endtrans %}">
&laquo;
</a>
{% else %}
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(previous_params, '') }}">
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(previous_params, '', false) }}">
{% trans %}Previous{% context %}Previous page{% endtrans %} &laquo;
</a>
{% endif %}
@ -56,11 +56,11 @@
{% endif %}
{% if is_full_query %}
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(full_queries_params, '') }}" title="{% trans 'Truncate shown queries' %}">
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(full_queries_params, '', false) }}" title="{% trans 'Truncate shown queries' %}">
<img src="{{ image_path }}s_partialtext.png" alt="{% trans 'Truncate shown queries' %}">
</a>
{% else %}
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(full_queries_params, '') }}" title="{% trans 'Show full queries' %}">
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(full_queries_params, '', false) }}" title="{% trans 'Show full queries' %}">
<img src="{{ image_path }}s_fulltext.png" alt="{% trans 'Show full queries' %}">
</a>
{% endif %}
@ -68,12 +68,12 @@
{% if has_next %}
-
{% if has_icons %}
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(next_params, '') }}" title="
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(next_params, '', false) }}" title="
{%- trans %}Next{% context %}Next page{% endtrans %}">
&raquo;
</a>
{% else %}
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(next_params, '') }}">
<a href="{{ url('/server/binlog') }}" data-post="{{ get_common(next_params, '', false) }}">
{% trans %}Next{% context %}Next page{% endtrans %} &raquo;
</a>
{% endif %}

View File

@ -315,7 +315,7 @@
<div class="row">
<ul>
<li class="li_switch_dbstats">
<a href="{{ url('/server/databases') }}" data-post="{{ get_common({'statistics': '1'}, '') }}" title="{% trans 'Enable statistics' %}">
<a href="{{ url('/server/databases') }}" data-post="{{ get_common({'statistics': '1'}, '', false) }}" title="{% trans 'Enable statistics' %}">
<strong>{% trans 'Enable statistics' %}</strong>
</a>
</li>

View File

@ -17,7 +17,7 @@
<div class="card mb-2">
<div class="card-header">{% trans 'Master replication' %}</div>
<div class="card-body">
{% apply format('<a href="' ~ url('/server/replication') ~ '" data-post="' ~ get_common(url_params|merge({'mr_configure': true}), '') ~ '">', '</a>')|raw %}
{% apply format('<a href="' ~ url('/server/replication') ~ '" data-post="' ~ get_common(url_params|merge({'mr_configure': true}), '', false) ~ '">', '</a>')|raw %}
{% trans %}
This server is not configured as master in a replication process. Would you like to %sconfigure%s it?
{% endtrans %}

View File

@ -40,7 +40,7 @@
</div>
</li>
<li>
<a href="{{ url('/server/replication') }}" data-post="{{ get_common(url_params, '') }}" id="master_addslaveuser_href">
<a href="{{ url('/server/replication') }}" data-post="{{ get_common(url_params, '', false) }}" id="master_addslaveuser_href">
{% trans 'Add slave replication user' %}
</a>
</li>

View File

@ -103,7 +103,7 @@
{% apply format('<a href="' ~ url('/server/replication') ~ '" data-post="' ~ get_common(url_params|merge({
'sl_configure': true,
'repl_clear_scr': true
})) ~ '">', '</a>')|raw %}
}), '', false) ~ '">', '</a>')|raw %}
{% trans 'This server is not configured as slave in a replication process. Would you like to %sconfigure%s it?' %}
{% endapply %}
{% endif %}

View File

@ -5,7 +5,7 @@
<th>{% trans 'Processes' %}</th>
{% for column in columns %}
<th scope="col">
<a href="{{ url('/server/status/processes') }}" data-post="{{ get_common(column.params) }}" class="sortlink">
<a href="{{ url('/server/status/processes') }}" data-post="{{ get_common(column.params, '', false) }}" class="sortlink">
{{ column.name }}
{% if column.is_sorted %}
<img class="icon ic_s_desc soimg" alt="
@ -15,7 +15,7 @@
{% endif %}
</a>
{% if column.has_full_query %}
<a href="{{ url('/server/status/processes') }}" data-post="{{ get_common(refresh_params, '') }}">
<a href="{{ url('/server/status/processes') }}" data-post="{{ get_common(refresh_params, '', false) }}">
{% if column.is_full %}
{{ get_image(
's_partialtext',
@ -40,7 +40,7 @@
{% for row in rows %}
<tr>
<td>
<a class="ajax kill_process" href="{{ url('/server/status/processes/kill/' ~ row.id) }}" data-post="{{ get_common({'kill': row.id}, '') }}">
<a class="ajax kill_process" href="{{ url('/server/status/processes/kill/' ~ row.id) }}" data-post="{{ get_common({'kill': row.id}, '', false) }}">
{% trans 'Kill' %}
</a>
</td>

View File

@ -58,7 +58,7 @@
</a>
|
<a class="delete-server" href="{{ get_common(server.params.remove) }}" data-post="
{{- get_common({ token: server.params.token }, '') }}">
{{- get_common({ token: server.params.token }, '', false) }}">
{% trans 'Delete' %}
</a>
</small>

View File

@ -9,7 +9,8 @@
<legend>{% trans 'Query results operations' %}</legend>
<span>
{{ link_or_button(
url('/view/create', {'db': db, 'table': table, 'printview': '1', 'sql_query': sql_query}),
url('/view/create'),
{'db': db, 'table': table, 'printview': '1', 'sql_query': sql_query},
get_icon('b_view_add', 'Create view'|trans, true),
{'class': 'create_view ajax btn'}
) }}

View File

@ -1,4 +1,4 @@
<span class="curr_value">{{ current_value }}</span>
<a href="{{ url('/browse-foreigners') }}" data-post="{{ get_common(params, '') }}" class="ajax browse_foreign">
<a href="{{ url('/browse-foreigners') }}" data-post="{{ get_common(params, '', false) }}" class="ajax browse_foreign">
{% trans 'Browse foreign values' %}
</a>

View File

@ -313,7 +313,7 @@
<ul class="list-group list-group-flush" id="tbl_maintenance">
{% if storage_engine in ['MYISAM', 'ARIA', 'INNODB', 'BERKELEYDB', 'TOKUDB'] %}
<li class="list-group-item">
<a href="{{ url('/table/maintenance/analyze') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}) }}">
<a href="{{ url('/table/maintenance/analyze') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}, '', false) }}">
{% trans 'Analyze table' %}
</a>
{{ show_mysql_docu('ANALYZE_TABLE') }}
@ -322,7 +322,7 @@
{% if storage_engine in ['MYISAM', 'ARIA', 'INNODB', 'TOKUDB'] %}
<li class="list-group-item">
<a href="{{ url('/table/maintenance/check') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}) }}">
<a href="{{ url('/table/maintenance/check') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}, '', false) }}">
{% trans 'Check table' %}
</a>
{{ show_mysql_docu('CHECK_TABLE') }}
@ -330,7 +330,7 @@
{% endif %}
<li class="list-group-item">
<a href="{{ url('/table/maintenance/checksum') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}) }}">
<a href="{{ url('/table/maintenance/checksum') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}, '', false) }}">
{% trans 'Checksum table' %}
</a>
{{ show_mysql_docu('CHECKSUM_TABLE') }}
@ -338,7 +338,7 @@
{% if storage_engine == 'INNODB' %}
<li class="list-group-item">
<a class="maintain_action ajax" href="{{ url('/sql') }}" data-post="{{ get_common(url_params|merge({'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ ' ENGINE = InnoDB;'})) }}">
<a class="maintain_action ajax" href="{{ url('/sql') }}" data-post="{{ get_common(url_params|merge({'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ ' ENGINE = InnoDB;'}), '', false) }}">
{% trans 'Defragment table' %}
</a>
{{ show_mysql_docu('InnoDB_File_Defragmenting') }}
@ -350,7 +350,7 @@
'sql_query': 'FLUSH TABLE ' ~ backquote(table),
'message_to_show': 'Table %s has been flushed.'|trans|format(table|e),
'reload': true
})) }}">
}), '', false) }}">
{% trans 'Flush the table (FLUSH)' %}
</a>
{{ show_mysql_docu('FLUSH') }}
@ -358,7 +358,7 @@
{% if storage_engine in ['MYISAM', 'ARIA', 'INNODB', 'BERKELEYDB', 'TOKUDB'] %}
<li class="list-group-item">
<a href="{{ url('/table/maintenance/optimize') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}) }}">
<a href="{{ url('/table/maintenance/optimize') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}, '', false) }}">
{% trans 'Optimize table' %}
</a>
{{ show_mysql_docu('OPTIMIZE_TABLE') }}
@ -367,7 +367,7 @@
{% if storage_engine in ['MYISAM', 'ARIA'] %}
<li class="list-group-item">
<a href="{{ url('/table/maintenance/repair') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}) }}">
<a href="{{ url('/table/maintenance/repair') }}" data-post="{{ get_common({'db': db, 'table': table, 'selected_tbl': [table]}, '', false) }}">
{% trans 'Repair table' %}
</a>
{{ show_mysql_docu('REPAIR_TABLE') }}
@ -383,12 +383,13 @@
{% if not is_view %}
<li class="list-group-item">
{{ link_or_button(
url('/sql', url_params|merge({
url('/sql'),
url_params|merge({
'sql_query': 'TRUNCATE TABLE ' ~ backquote(db) ~ '.' ~ backquote(table),
'goto': url('/table/structure'),
'reload': true,
'message_to_show': 'Table %s has been emptied.'|trans|format(table)|e
})),
}),
'Empty the table (TRUNCATE)'|trans,
{
'id': 'truncate_tbl_anchor',
@ -400,14 +401,15 @@
{% endif %}
<li class="list-group-item">
{{ link_or_button(
url('/sql', url_params|merge({
url('/sql'),
url_params|merge({
'sql_query': 'DROP TABLE ' ~ backquote(db) ~ '.' ~ backquote(table),
'goto': url('/database/operations'),
'reload': true,
'purge': true,
'message_to_show': is_view ? 'View %s has been dropped.'|trans|format(table)|e : 'Table %s has been dropped.'|trans|format(table)|e,
'table': table
})),
}),
'Delete the table (DROP)'|trans,
{
'id': 'drop_tbl_anchor',

View File

@ -23,14 +23,15 @@
<div class="card-body">
<div class="card-text">
{{ link_or_button(
url('/sql', url_params|merge({
url('/sql'),
url_params|merge({
'sql_query': 'DROP VIEW ' ~ backquote(table),
'goto': url('/table/structure'),
'reload': true,
'purge': true,
'message_to_show': 'View %s has been dropped.'|trans|format(table)|e,
'table': table
})),
}),
'Delete the view (DROP)'|trans,
{
'id': 'drop_view_anchor',

View File

@ -22,9 +22,8 @@
{% if one_key['constraint'] is defined %}
<input type="hidden" class="drop_foreign_key_msg" value="
{{- js_msg }}">
{% set drop_url = url('/sql', this_params) %}
{% set drop_str = get_icon('b_drop', 'Drop'|trans) %}
{{ link_or_button(drop_url, drop_str, {'class': 'drop_foreign_key_anchor ajax'}) }}
{{ link_or_button(url('/sql'), this_params, drop_str, {'class': 'drop_foreign_key_anchor ajax'}) }}
{% endif %}
</td>
<td>

View File

@ -21,7 +21,7 @@
value="{{ criteria_values[column_index] }}"
{% endif %}>
<a class="ajax browse_foreign" href="{{ url('/browse-foreigners') }}" data-post="
{{- get_common({'db': db, 'table': table}, '') -}}
{{- get_common({'db': db, 'table': table}, '', false) -}}
&amp;field={{ column_name|url_encode }}&amp;fieldkey=
{{- column_index }}&amp;fromsearch=1">
{{ get_icon('b_browse', 'Browse foreign values'|trans) }}
@ -36,7 +36,7 @@
{% if in_fbs %}
{% set edit_str = get_icon('b_edit', 'Edit/Insert'|trans) %}
<span class="open_search_gis_editor">
{{ link_or_button(url('/gis-data-editor'), edit_str, [], '_blank') }}
{{ link_or_button(url('/gis-data-editor'), [], edit_str, [], '_blank') }}
</span>
{% endif %}
{% elseif column_type starts with 'enum'

View File

@ -80,7 +80,7 @@
'db': db,
'table': table,
'partition_name': partition.getName(),
}) }}">
}, '', false) }}">
{{ get_icon('b_search', 'Analyze'|trans) }}
</a>
</td>
@ -90,7 +90,7 @@
'db': db,
'table': table,
'partition_name': partition.getName(),
}) }}">
}, '', false) }}">
{{ get_icon('eye', 'Check'|trans) }}
</a>
</td>
@ -100,7 +100,7 @@
'db': db,
'table': table,
'partition_name': partition.getName(),
}) }}">
}, '', false) }}">
{{ get_icon('normalize', 'Optimize'|trans) }}
</a>
</td>
@ -110,7 +110,7 @@
'db': db,
'table': table,
'partition_name': partition.getName(),
}) }}">
}, '', false) }}">
{{ get_icon('s_tbl', 'Rebuild'|trans) }}
</a>
</td>
@ -120,7 +120,7 @@
'db': db,
'table': table,
'partition_name': partition.getName(),
}) }}">
}, '', false) }}">
{{ get_icon('b_tblops', 'Repair'|trans) }}
</a>
</td>
@ -130,7 +130,7 @@
'db': db,
'table': table,
'partition_name': partition.getName(),
}) }}">
}, '', false) }}">
{{ get_icon('b_empty', 'Truncate'|trans) }}
</a>
</td>
@ -141,7 +141,7 @@
'db': db,
'table': table,
'partition_name': partition.getName(),
}) }}">
}, '', false) }}">
{{ get_icon('b_drop', 'Drop'|trans) }}
</a>
</td>
@ -194,11 +194,12 @@
<input class="btn btn-secondary" type="submit" value="{% trans 'Partition table' %}">
{% else %}
{{ link_or_button(
url('/sql', {
url('/sql'),
{
'db': db,
'table': table,
'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ ' REMOVE PARTITIONING'
}),
},
'Remove partitioning'|trans, {
'class': 'btn btn-secondary ajax',
'id': 'remove_partitioning'

View File

@ -117,7 +117,7 @@
'dropped_column': row['Field'],
'purge': true,
'message_to_show': 'Column %s has been dropped.'|trans|format(row['Field']|e)
}) }}">
}, '', false) }}">
{{ get_icon('b_drop', 'Drop'|trans) }}
</a>
</td>
@ -142,7 +142,7 @@
'table': table,
'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ (primary ? ' DROP PRIMARY KEY,') ~ ' ADD PRIMARY KEY(' ~ backquote(row['Field']) ~ ');',
'message_to_show': 'A primary key has been added on %s.'|trans|format(row['Field']|e)
}) }}">
}, '', false) }}">
{{ get_icon('b_primary', 'Primary'|trans) }}
</a>
{% endif %}
@ -157,7 +157,7 @@
'table': table,
'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ ' ADD UNIQUE(' ~ backquote(row['Field']) ~ ');',
'message_to_show': 'An index has been added on %s.'|trans|format(row['Field']|e)
}) }}">
}, '', false) }}">
{{ get_icon('b_unique', 'Unique'|trans) }}
</a>
{% endif %}
@ -172,7 +172,7 @@
'table': table,
'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ ' ADD INDEX(' ~ backquote(row['Field']) ~ ');',
'message_to_show': 'An index has been added on %s.'|trans|format(row['Field']|e)
}) }}">
}, '', false) }}">
{{ get_icon('b_index', 'Index'|trans) }}
</a>
{% endif %}
@ -197,7 +197,7 @@
'table': table,
'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ ' ADD SPATIAL(' ~ backquote(row['Field']) ~ ');',
'message_to_show': 'An index has been added on %s.'|trans|format(row['Field']|e)
}) }}">
}, '', false) }}">
{{ get_icon('b_spatial', 'Spatial'|trans) }}
</a>
{% endif %}
@ -216,7 +216,7 @@
'table': table,
'sql_query': 'ALTER TABLE ' ~ backquote(table) ~ ' ADD FULLTEXT(' ~ backquote(row['Field']) ~ ');',
'message_to_show': 'An index has been added on %s.'|trans|format(row['Field']|e)
}) }}">
}, '', false) }}">
{{ get_icon('b_ftext', 'Fulltext'|trans) }}
</a>
{% else %}
@ -235,7 +235,7 @@
~ ' GROUP BY ' ~ backquote(row['Field'])
~ ' ORDER BY ' ~ backquote(row['Field']),
'is_browse_distinct': true
}) }}">
}, '', false) }}">
{{ get_icon('b_browse', 'Distinct values'|trans) }}
</a>
</li>
@ -326,7 +326,8 @@
<div id="structure-action-links">
{% if tbl_is_view and not db_is_system_schema %}
{{ link_or_button(
url('/view/create', {'db': db, 'table': table}),
url('/view/create'),
{'db': db, 'table': table},
get_icon('b_edit', 'Edit view'|trans, true)
) }}
{% endif %}
@ -339,7 +340,7 @@
'table': table,
'sql_query': 'SELECT * FROM ' ~ backquote(table) ~ ' PROCEDURE ANALYSE()',
'session_max_rows': 'all'
}) }}">
}, '', false) }}">
{{ get_icon(
'b_tblanalyse',
'Propose table structure'|trans,
@ -432,7 +433,7 @@
'db': db,
'table': table,
'index': index.getName()
}, '') }}">
}, '', false) }}">
{{ get_icon('b_edit', 'Edit'|trans) }}
</a>
</td>
@ -441,7 +442,7 @@
'db': db,
'table': table,
'index': index.getName()
}, '') }}">
}, '', false) }}">
{{ get_icon('b_rename', 'Rename'|trans) }}
</a>
</td>
@ -460,7 +461,8 @@
<input type="hidden" class="drop_primary_key_index_msg" value="{{ index_params.sql_query }}">
{{ link_or_button(
url('/sql', index_params|merge({'db': db, 'table': table})),
url('/sql'),
index_params|merge({'db': db, 'table': table}),
get_icon('b_drop', 'Drop'|trans),
{'class': 'drop_primary_key_index_anchor ajax'}
) }}

View File

@ -60,7 +60,7 @@
{{- get_common(url_params|merge({
'version': version['version'],
'submit_delete_version': true
}), '') }}">
}), '', false) }}">
{{ get_icon('b_drop', 'Delete version'|trans) }}
</a>
</td>
@ -69,14 +69,14 @@
{{- get_common(url_params|merge({
'version': version['version'],
'report': 'true'
}), '') }}">
}), '', false) }}">
{{ get_icon('b_report', 'Tracking report'|trans) }}
</a>
<a href="{{ url('/table/tracking') }}" data-post="
{{- get_common(url_params|merge({
'version': version['version'],
'snapshot': 'true'
}), '') }}">
}), '', false) }}">
{{ get_icon('b_props', 'Structure snapshot'|trans) }}
</a>
</td>

View File

@ -200,6 +200,7 @@ abstract class AbstractTestCase extends TestCase
global $PMA_Config;
$PMA_Config = new Config();
$PMA_Config->set('environment', 'development');
$PMA_Config->set('URLQueryEncryption', false);
}
protected function setTheme(): void

View File

@ -6,6 +6,7 @@ namespace PhpMyAdmin\Tests;
use PhpMyAdmin\Core;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Url;
use stdClass;
use function hash;
use function htmlspecialchars;
@ -15,6 +16,7 @@ use function ob_get_contents;
use function ob_start;
use function preg_quote;
use function serialize;
use function str_repeat;
class CoreTest extends AbstractNetworkTestCase
{
@ -33,6 +35,7 @@ class CoreTest extends AbstractNetworkTestCase
$GLOBALS['db'] = '';
$GLOBALS['table'] = '';
$GLOBALS['PMA_PHP_SELF'] = 'http://example.net/';
$GLOBALS['PMA_Config']->set('URLQueryEncryption', false);
}
/**
@ -1437,4 +1440,61 @@ class CoreTest extends AbstractNetworkTestCase
$this->assertArrayHasKey('test', $_POST);
$this->assertEquals('test', $_POST['test']);
}
public function testPopulateRequestWithEncryptedQueryParams(): void
{
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryption', true);
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$_GET = ['pos' => '0', 'eq' => Url::encryptQuery('{"db":"test_db","table":"test_table"}')];
$_REQUEST = $_GET;
Core::populateRequestWithEncryptedQueryParams();
$expected = ['pos' => '0', 'db' => 'test_db', 'table' => 'test_table'];
$this->assertEquals($expected, $_GET);
$this->assertEquals($expected, $_REQUEST);
}
/**
* @param string[] $encrypted
* @param string[] $decrypted
*
* @dataProvider providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam
*/
public function testPopulateRequestWithEncryptedQueryParamsWithInvalidParam(
array $encrypted,
array $decrypted
): void {
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryption', true);
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$_GET = $encrypted;
$_REQUEST = $encrypted;
Core::populateRequestWithEncryptedQueryParams();
$this->assertEquals($decrypted, $_GET);
$this->assertEquals($decrypted, $_REQUEST);
}
/**
* @return string[][][]
*/
public function providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam(): array
{
return [
[[], []],
[['eq' => []], []],
[['eq' => ''], []],
[['eq' => 'invalid'], []],
];
}
}

View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Tests\Crypto;
use PhpMyAdmin\Crypto\Crypto;
use PhpMyAdmin\Tests\AbstractTestCase;
use function str_repeat;
use function mb_strlen;
/**
* @covers \PhpMyAdmin\Crypto\Crypto
*/
class CryptoTest extends AbstractTestCase
{
public function testWithValidKeyFromConfig(): void
{
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$cryptoWithSodium = new Crypto();
$encrypted = $cryptoWithSodium->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithSodium->decrypt($encrypted));
$this->assertArrayNotHasKey('URLQueryEncryptionSecretKey', $_SESSION);
$cryptoWithPhpseclib = new Crypto(true);
$encrypted = $cryptoWithPhpseclib->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted));
$this->assertArrayNotHasKey('URLQueryEncryptionSecretKey', $_SESSION);
}
public function testWithValidKeyFromSession(): void
{
global $PMA_Config;
$_SESSION = ['URLQueryEncryptionSecretKey' => str_repeat('a', 32)];
$PMA_Config->set('URLQueryEncryptionSecretKey', '');
$cryptoWithSodium = new Crypto();
$encrypted = $cryptoWithSodium->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithSodium->decrypt($encrypted));
$this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION);
$cryptoWithPhpseclib = new Crypto(true);
$encrypted = $cryptoWithPhpseclib->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted));
$this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION);
}
public function testWithNewSessionKey(): void
{
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryptionSecretKey', '');
$cryptoWithSodium = new Crypto();
$encrypted = $cryptoWithSodium->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithSodium->decrypt($encrypted));
$this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION);
$this->assertEquals(32, mb_strlen($_SESSION['URLQueryEncryptionSecretKey'], '8bit'));
$cryptoWithPhpseclib = new Crypto(true);
$encrypted = $cryptoWithPhpseclib->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted));
$this->assertArrayHasKey('URLQueryEncryptionSecretKey', $_SESSION);
$this->assertEquals(32, mb_strlen($_SESSION['URLQueryEncryptionSecretKey'], '8bit'));
}
public function testDecryptWithInvalidKey(): void
{
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$cryptoWithSodium = new Crypto();
$encrypted = $cryptoWithSodium->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithSodium->decrypt($encrypted));
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('b', 32));
$cryptoWithSodium = new Crypto();
$this->assertNull($cryptoWithSodium->decrypt($encrypted));
$cryptoWithPhpseclib = new Crypto(true);
$encrypted = $cryptoWithPhpseclib->encrypt('test');
$this->assertNotSame('test', $encrypted);
$this->assertSame('test', $cryptoWithPhpseclib->decrypt($encrypted));
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$cryptoWithPhpseclib = new Crypto(true);
$this->assertNull($cryptoWithPhpseclib->decrypt($encrypted));
}
}

View File

@ -221,6 +221,7 @@ class GeneratorTest extends AbstractTestCase
[
[
'index.php',
null,
'text',
],
1000,
@ -228,15 +229,17 @@ class GeneratorTest extends AbstractTestCase
],
[
[
'index.php?some=parameter',
'index.php',
['some' => 'parameter'],
'text',
],
20,
'<a href="index.php" data-post="some=parameter">text</a>',
'<a href="index.php" data-post="some=parameter&lang=en">text</a>',
],
[
[
'index.php',
null,
'text',
[],
'target',
@ -247,6 +250,7 @@ class GeneratorTest extends AbstractTestCase
[
[
'https://mariadb.org/explain_analyzer/analyze/?client=phpMyAdmin&amp;raw_explain=%2B---%2B',
null,
'text',
[],
'target',
@ -259,6 +263,7 @@ class GeneratorTest extends AbstractTestCase
[
[
'https://mariadb.org/explain_analyzer/analyze/?client=phpMyAdmin&amp;raw_explain=%2B---%2B',
null,
'text',
[],
'target',
@ -271,6 +276,7 @@ class GeneratorTest extends AbstractTestCase
[
[
'url.php?url=http://phpmyadmin.net/',
null,
'text',
[],
'_blank',

View File

@ -7,6 +7,12 @@ namespace PhpMyAdmin\Tests\Navigation;
use PhpMyAdmin\Navigation\NavigationTree;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tests\AbstractTestCase;
use PhpMyAdmin\Url;
use ReflectionMethod;
use function str_repeat;
use function parse_url;
use function parse_str;
use function htmlspecialchars_decode;
class NavigationTreeTest extends AbstractTestCase
{
@ -80,4 +86,37 @@ class NavigationTreeTest extends AbstractTestCase
$result = $this->object->renderDbSelect();
$this->assertStringContainsString('pma_navigation_select_database', $result);
}
/**
* @return void
*/
public function testEncryptQueryParams()
{
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryption', false);
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$method = new ReflectionMethod($this->object, 'encryptQueryParams');
$method->setAccessible(true);
$link = 'tbl_structure.php?server=1&amp;db=test_db&amp;table=test_table&amp;pos=0';
$actual = $method->invoke($this->object, $link);
$this->assertEquals($link, $actual);
$PMA_Config->set('URLQueryEncryption', true);
$actual = $method->invoke($this->object, $link);
$this->assertStringStartsWith('tbl_structure.php?server=1&amp;pos=0&amp;eq=', $actual);
$url = parse_url($actual);
parse_str(htmlspecialchars_decode($url['query']), $query);
$this->assertRegExp('/^[a-zA-Z0-9-_=]+$/', $query['eq']);
$decrypted = Url::decryptQuery($query['eq']);
$this->assertJson($decrypted);
$this->assertSame('{"db":"test_db","table":"test_table"}', $decrypted);
}
}

View File

@ -118,7 +118,8 @@ class UserGroupsTest extends AbstractTestCase
'viewUsers' => 1,
'userGroup' => htmlspecialchars('usergroup'),
],
''
'',
false
);
$this->assertStringContainsString(
$url_tag,
@ -130,7 +131,8 @@ class UserGroupsTest extends AbstractTestCase
'editUserGroup' => 1,
'userGroup' => htmlspecialchars('usergroup'),
],
''
'',
false
);
$this->assertStringContainsString(
$url_tag,
@ -142,7 +144,8 @@ class UserGroupsTest extends AbstractTestCase
'deleteUserGroup' => 1,
'userGroup' => htmlspecialchars('usergroup'),
],
''
'',
false
);
$this->assertStringContainsString(
$url_tag,

View File

@ -6,6 +6,9 @@ namespace PhpMyAdmin\Tests;
use PhpMyAdmin\Url;
use function urldecode;
use function str_repeat;
use function parse_str;
use function is_string;
class UrlTest extends AbstractTestCase
{
@ -20,6 +23,7 @@ class UrlTest extends AbstractTestCase
parent::setUp();
parent::setLanguage();
unset($_COOKIE['pma_lang']);
$GLOBALS['PMA_Config']->set('URLQueryEncryption', false);
}
/**
@ -179,4 +183,61 @@ class UrlTest extends AbstractTestCase
Url::getHiddenFields([])
);
}
/**
* @return void
*/
public function testBuildHttpQueryWithUrlQueryEncryptionDisabled()
{
global $PMA_Config;
$PMA_Config->set('URLQueryEncryption', false);
$params = ['db' => 'test_db', 'table' => 'test_table', 'pos' => 0];
$this->assertEquals('db=test_db&table=test_table&pos=0', Url::buildHttpQuery($params));
}
/**
* @return void
*/
public function testBuildHttpQueryWithUrlQueryEncryptionEnabled()
{
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryption', true);
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$params = ['db' => 'test_db', 'table' => 'test_table', 'pos' => 0];
$query = Url::buildHttpQuery($params);
$this->assertStringStartsWith('pos=0&eq=', $query);
parse_str($query, $queryParams);
$this->assertCount(2, $queryParams);
$this->assertSame('0', $queryParams['pos']);
$this->assertTrue(is_string($queryParams['eq']));
$this->assertNotSame('', $queryParams['eq']);
$this->assertRegExp('/^[a-zA-Z0-9-_=]+$/', $queryParams['eq']);
$decrypted = Url::decryptQuery($queryParams['eq']);
$this->assertJson($decrypted);
$this->assertSame('{"db":"test_db","table":"test_table"}', $decrypted);
}
/**
* @return void
*/
public function testQueryEncryption()
{
global $PMA_Config;
$_SESSION = [];
$PMA_Config->set('URLQueryEncryption', true);
$PMA_Config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$query = '{"db":"test_db","table":"test_table"}';
$encrypted = Url::encryptQuery($query);
$this->assertNotSame($query, $encrypted);
$this->assertNotSame('', $encrypted);
$this->assertRegExp('/^[a-zA-Z0-9-_=]+$/', $encrypted);
$decrypted = Url::decryptQuery($encrypted);
$this->assertSame($query, $decrypted);
}
}