Merge branch 'QA_5_1-security' into master-security

Signed-off-by: Maurício Meneghini Fauth <mauricio@fauth.dev>
This commit is contained in:
Maurício Meneghini Fauth 2022-01-11 14:31:56 -03:00
commit 0bee94b27e
No known key found for this signature in database
GPG Key ID: 6A16FD38AFC89CC8
57 changed files with 1028 additions and 224 deletions

View File

@ -50,6 +50,7 @@
"composer/ca-bundle": "^1.2",
"google/recaptcha": "^1.1",
"nikic/fast-route": "^1.3",
"paragonie/sodium_compat": "^1.17",
"phpmyadmin/motranslator": "^5.0",
"phpmyadmin/shapefile": "^3.0.1",
"phpmyadmin/sql-parser": "^5.5",
@ -81,6 +82,7 @@
"ext-zip": "For zip import and export",
"ext-gd2": "For image transformations",
"ext-mbstring": "For best performance",
"ext-sodium": "Better encryption performance",
"tecnickcom/tcpdf": "For PDF support",
"pragmarx/google2fa-qrcode": "^2.1 - For 2FA authentication",
"bacon/bacon-qr-code": "^2.0 - For 2FA authentication",
@ -88,6 +90,7 @@
},
"require-dev": {
"bacon/bacon-qr-code": "^2.0",
"code-lts/u2f-php-server": "^1.2",
"php-webdriver/webdriver": "^1.11",
"phpmyadmin/coding-standard": "^3.0.0",
"phpstan/extension-installer": "^1.1",
@ -98,7 +101,6 @@
"pragmarx/google2fa-qrcode": "^2.1",
"psalm/plugin-phpunit": "^0.16.1",
"roave/security-advisories": "dev-latest",
"code-lts/u2f-php-server": "^1.2",
"symfony/console": "^5.2.3",
"tecnickcom/tcpdf": "^6.4.4",
"vimeo/psalm": "^4.17"

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

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

@ -120,6 +120,8 @@ final class Common
Session::setUp($config, $errorHandler);
}
$request = Core::populateRequestWithEncryptedQueryParams($request);
/**
* init some variables LABEL_variables_init
*/

View File

@ -370,6 +370,20 @@ final class Settings
*/
public $IgnoreMultiSubmitErrors;
/**
* Define whether phpMyAdmin will encrypt sensitive data from the URL query string.
*
* @var bool
*/
public $URLQueryEncryption;
/**
* A secret key used to encrypt/decrypt the URL query string. Should be 32 bytes long.
*
* @var string
*/
public $URLQueryEncryptionSecretKey;
/**
* allow login to any user entered server in cookie based authentication
*
@ -1603,6 +1617,8 @@ final class Settings
$this->LoginCookieDeleteAll = $this->setLoginCookieDeleteAll($settings);
$this->UseDbSearch = $this->setUseDbSearch($settings);
$this->IgnoreMultiSubmitErrors = $this->setIgnoreMultiSubmitErrors($settings);
$this->URLQueryEncryption = $this->setURLQueryEncryption($settings);
$this->URLQueryEncryptionSecretKey = $this->setURLQueryEncryptionSecretKey($settings);
$this->AllowArbitraryServer = $this->setAllowArbitraryServer($settings);
$this->ArbitraryServerRegexp = $this->setArbitraryServerRegexp($settings);
$this->CaptchaMethod = $this->setCaptchaMethod($settings);
@ -2338,6 +2354,30 @@ final class Settings
return (bool) $settings['IgnoreMultiSubmitErrors'];
}
/**
* @param array<int|string, mixed> $settings
*/
private function setURLQueryEncryption(array $settings): bool
{
if (! isset($settings['URLQueryEncryption'])) {
return false;
}
return (bool) $settings['URLQueryEncryption'];
}
/**
* @param array<int|string, mixed> $settings
*/
private function setURLQueryEncryptionSecretKey(array $settings): string
{
if (! isset($settings['URLQueryEncryptionSecretKey'])) {
return '';
}
return (string) $settings['URLQueryEncryptionSecretKey'];
}
/**
* @param array<int|string, mixed> $settings
*/

View File

@ -502,6 +502,13 @@ final class Server
*/
public $tracking_add_drop_database;
/**
* Whether to show or hide detailed MySQL/MariaDB connection errors on the login page.
*
* @var bool
*/
public $hide_connection_errors;
/**
* @param array<int|string, mixed> $server
*/
@ -565,6 +572,7 @@ final class Server
$this->tracking_add_drop_view = $this->setTrackingAddDropView($server);
$this->tracking_add_drop_table = $this->setTrackingAddDropTable($server);
$this->tracking_add_drop_database = $this->setTrackingAddDropDatabase($server);
$this->hide_connection_errors = $this->setHideConnectionErrors($server);
}
/**
@ -1375,4 +1383,16 @@ final class Server
return true;
}
/**
* @param array<int|string, mixed> $server
*/
private function setHideConnectionErrors(array $server): bool
{
if (isset($server['hide_connection_errors'])) {
return (bool) $server['hide_connection_errors'];
}
return false;
}
}

View File

@ -108,7 +108,8 @@ class UserGroups
'viewUsers' => 1,
'userGroup' => $groupName,
],
''
'',
false
);
$userGroupVal['viewUsersIcon'] = Generator::getIcon('b_usrlist', __('View users'));
@ -117,7 +118,8 @@ class UserGroups
'editUserGroup' => 1,
'userGroup' => $groupName,
],
''
'',
false
);
$userGroupVal['editUsersIcon'] = Generator::getIcon('b_edit', __('Edit'));
$userGroupsValues[] = $userGroupVal;

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Http\ServerRequest;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
@ -29,6 +30,7 @@ use function in_array;
use function intval;
use function is_array;
use function is_string;
use function json_decode;
use function json_encode;
use function mb_strlen;
use function mb_strpos;
@ -977,4 +979,64 @@ class Core
return $containerBuilder;
}
public static function populateRequestWithEncryptedQueryParams(ServerRequest $request): ServerRequest
{
$queryParams = $request->getQueryParams();
$parsedBody = $request->getParsedBody();
unset($_GET['eq'], $_POST['eq'], $_REQUEST['eq']);
if (! isset($queryParams['eq']) && (! is_array($parsedBody) || ! isset($parsedBody['eq']))) {
return $request;
}
$encryptedQuery = '';
if (
is_array($parsedBody)
&& isset($parsedBody['eq'])
&& is_string($parsedBody['eq'])
&& $parsedBody['eq'] !== ''
) {
$encryptedQuery = $parsedBody['eq'];
unset($parsedBody['eq'], $queryParams['eq']);
} elseif (isset($queryParams['eq']) && is_string($queryParams['eq']) && $queryParams['eq'] !== '') {
$encryptedQuery = $queryParams['eq'];
unset($queryParams['eq']);
}
$decryptedQuery = null;
if ($encryptedQuery !== '') {
$decryptedQuery = Url::decryptQuery($encryptedQuery);
}
if ($decryptedQuery === null) {
$request = $request->withQueryParams($queryParams);
if (is_array($parsedBody)) {
$request = $request->withParsedBody($parsedBody);
}
return $request;
}
$urlQueryParams = (array) json_decode($decryptedQuery);
foreach ($urlQueryParams as $urlQueryParamKey => $urlQueryParamValue) {
if (is_array($parsedBody)) {
$parsedBody[$urlQueryParamKey] = $urlQueryParamValue;
$_POST[$urlQueryParamKey] = $urlQueryParamValue;
} else {
$queryParams[$urlQueryParamKey] = $urlQueryParamValue;
$_GET[$urlQueryParamKey] = $urlQueryParamValue;
}
$_REQUEST[$urlQueryParamKey] = $urlQueryParamValue;
}
$request = $request->withQueryParams($queryParams);
if (is_array($parsedBody)) {
$request = $request->withParsedBody($parsedBody);
}
return $request;
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Crypto;
use Throwable;
use function is_string;
use function mb_strlen;
use function mb_substr;
use function random_bytes;
use function sodium_crypto_secretbox;
use function sodium_crypto_secretbox_open;
use const SODIUM_CRYPTO_SECRETBOX_KEYBYTES;
use const SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
final class Crypto
{
private function getEncryptionKey(): string
{
global $config;
$key = $config->get('URLQueryEncryptionSecretKey');
if (is_string($key) && mb_strlen($key, '8bit') === SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
return $key;
}
$key = $_SESSION['URLQueryEncryptionSecretKey'] ?? null;
if (is_string($key) && mb_strlen($key, '8bit') === SODIUM_CRYPTO_SECRETBOX_KEYBYTES) {
return $key;
}
$key = random_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES);
$_SESSION['URLQueryEncryptionSecretKey'] = $key;
return $key;
}
public function encrypt(string $plaintext): string
{
$key = $this->getEncryptionKey();
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $key);
return $nonce . $ciphertext;
}
public function decrypt(string $encrypted): ?string
{
$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 (! is_string($decrypted)) {
return null;
}
return $decrypted;
}
}

View File

@ -19,9 +19,11 @@ use function mysqli_connect_error;
use function mysqli_get_client_info;
use function mysqli_init;
use function mysqli_report;
use function sprintf;
use function stripos;
use function trigger_error;
use const E_USER_ERROR;
use const E_USER_WARNING;
use const MYSQLI_CLIENT_COMPRESS;
use const MYSQLI_CLIENT_SSL;
@ -105,15 +107,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) {
/*
@ -143,6 +157,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

@ -1475,9 +1475,8 @@ class Results
$tmpImage = '<img class="fulltext" src="'
. ($theme instanceof Theme ? $theme->getImgPath($tmpImageFile) : '')
. '" alt="' . $tmpTxt . '" title="' . $tmpTxt . '">';
$tmpUrl = Url::getFromRoute('/sql', $urlParamsFullText);
return Generator::linkOrButton($tmpUrl, $tmpImage);
return Generator::linkOrButton(Url::getFromRoute('/sql'), $urlParamsFullText, $tmpImage);
}
/**
@ -1591,12 +1590,10 @@ class Results
'session_max_rows' => $sessionMaxRows,
'is_browse_distinct' => $this->properties['is_browse_distinct'],
];
$singleOrderUrl = Url::getFromRoute('/sql', $singleUrlParams);
$multiOrderUrl = Url::getFromRoute('/sql', $multiUrlParams);
// Displays the sorting URL
// enable sort order swapping for image
$orderLink = $this->getSortOrderLink($orderImg, $fieldsMeta, $singleOrderUrl, $multiOrderUrl);
$orderLink = $this->getSortOrderLink($orderImg, $fieldsMeta, $singleUrlParams, $multiUrlParams);
$orderLink .= $this->getSortOrderHiddenInputs($multiUrlParams, $fieldsMeta->name);
@ -1857,23 +1854,32 @@ class Results
*
* @see getTableHeaders()
*
* @param string $orderImg the sort order image
* @param FieldMetadata $fieldsMeta set of field properties
* @param string $orderUrl the url for sort
* @param string $multiOrderUrl the url for sort
* @param string $orderImg the sort order image
* @param FieldMetadata $fieldsMeta set of field properties
* @param array<int|string, mixed> $orderUrlParams the url params for sort
* @param array<int|string, mixed> $multiOrderUrlParams the url params for sort
*
* @return string the sort order link
*/
private function getSortOrderLink(
string $orderImg,
FieldMetadata $fieldsMeta,
string $orderUrl,
string $multiOrderUrl
array $orderUrlParams,
array $multiOrderUrlParams
): string {
$urlPath = Url::getFromRoute('/sql');
$innerLinkContent = htmlspecialchars($fieldsMeta->name) . $orderImg
. '<input type="hidden" value="' . $multiOrderUrl . '">';
. '<input type="hidden" value="'
. $urlPath
. Url::getCommon($multiOrderUrlParams, str_contains($urlPath, '?') ? '&' : '?', false)
. '">';
return Generator::linkOrButton($orderUrl, $innerLinkContent, ['class' => 'sortlink']);
return Generator::linkOrButton(
Url::getFromRoute('/sql'),
$orderUrlParams,
$innerLinkContent,
['class' => 'sortlink']
);
}
private function getSortOrderHiddenInputs(
@ -2236,6 +2242,8 @@ class Results
$copyUrl = null;
$copyString = null;
$editUrl = null;
$editCopyUrlParams = null;
$delUrlParams = null;
// 1.2 Defines the URLs for the modify/delete link(s)
@ -2277,11 +2285,12 @@ class Results
$copyUrl,
$editString,
$copyString,
$editCopyUrlParams,
] = $this->getModifiedLinks($whereClause, $clauseIsUnique, $urlSqlQuery);
}
// 1.2.2 Delete/Kill link(s)
[$deleteUrl, $deleteString, $jsConf] = $this->getDeleteAndKillLinks(
[$deleteUrl, $deleteString, $jsConf, $delUrlParams] = $this->getDeleteAndKillLinks(
$whereClause,
$clauseIsUnique,
$urlSqlQuery,
@ -2297,9 +2306,18 @@ class Results
$tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
'position' => self::POSITION_LEFT,
'has_checkbox' => $deleteUrl && $displayParts['del_lnk'] !== self::KILL_PROCESS,
'edit' => ['url' => $editUrl, 'string' => $editString, 'clause_is_unique' => $clauseIsUnique],
'copy' => ['url' => $copyUrl, 'string' => $copyString],
'delete' => ['url' => $deleteUrl, 'string' => $deleteString],
'edit' => [
'url' => $editUrl,
'params' => $editCopyUrlParams + ['default_action' => 'update'],
'string' => $editString,
'clause_is_unique' => $clauseIsUnique,
],
'copy' => [
'url' => $copyUrl,
'params' => $editCopyUrlParams + ['default_action' => 'insert'],
'string' => $copyString,
],
'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
'row_number' => $rowNumber,
'where_clause' => $whereClause,
'condition' => json_encode($conditionArray),
@ -2310,9 +2328,18 @@ class Results
$tableBodyHtml .= $this->template->render('display/results/checkbox_and_links', [
'position' => self::POSITION_NONE,
'has_checkbox' => $deleteUrl && $displayParts['del_lnk'] !== self::KILL_PROCESS,
'edit' => ['url' => $editUrl, 'string' => $editString, 'clause_is_unique' => $clauseIsUnique],
'copy' => ['url' => $copyUrl, 'string' => $copyString],
'delete' => ['url' => $deleteUrl, 'string' => $deleteString],
'edit' => [
'url' => $editUrl,
'params' => $editCopyUrlParams + ['default_action' => 'update'],
'string' => $editString,
'clause_is_unique' => $clauseIsUnique,
],
'copy' => [
'url' => $copyUrl,
'params' => $editCopyUrlParams + ['default_action' => 'insert'],
'string' => $copyString,
],
'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
'row_number' => $rowNumber,
'where_clause' => $whereClause,
'condition' => json_encode($conditionArray),
@ -2350,11 +2377,16 @@ class Results
'has_checkbox' => $deleteUrl && $displayParts['del_lnk'] !== self::KILL_PROCESS,
'edit' => [
'url' => $editUrl,
'params' => $editCopyUrlParams + ['default_action' => 'update'],
'string' => $editString,
'clause_is_unique' => $clauseIsUnique ?? true,
],
'copy' => ['url' => $copyUrl, 'string' => $copyString],
'delete' => ['url' => $deleteUrl, 'string' => $deleteString],
'copy' => [
'url' => $copyUrl,
'params' => $editCopyUrlParams + ['default_action' => 'insert'],
'string' => $copyString,
],
'delete' => ['url' => $deleteUrl, 'params' => $delUrlParams, 'string' => $deleteString],
'row_number' => $rowNumber,
'where_clause' => $whereClause ?? '',
'condition' => json_encode($conditionArray ?? []),
@ -2904,8 +2936,7 @@ class Results
* @param bool $clauseIsUnique the unique condition of clause
* @param string $urlSqlQuery the analyzed sql query
*
* @return array<int,string> 5 element array - $edit_url, $copy_url,
* $edit_str, $copy_str
* @return array<int,string|array<string, bool|string>>
*/
private function getModifiedLinks(
$whereClause,
@ -2921,15 +2952,9 @@ class Results
'goto' => Url::getFromRoute('/sql'),
];
$editUrl = Url::getFromRoute(
'/table/change',
$urlParams + ['default_action' => 'update']
);
$editUrl = Url::getFromRoute('/table/change');
$copyUrl = Url::getFromRoute(
'/table/change',
$urlParams + ['default_action' => 'insert']
);
$copyUrl = Url::getFromRoute('/table/change');
$editStr = $this->getActionLinkContent(
'b_edit',
@ -2945,6 +2970,7 @@ class Results
$copyUrl,
$editStr,
$copyStr,
$urlParams,
];
}
@ -2994,7 +3020,7 @@ class Results
'message_to_show' => __('The row has been deleted.'),
'goto' => $linkGoto,
];
$deleteUrl = Url::getFromRoute('/sql', $urlParams);
$deleteUrl = Url::getFromRoute('/sql');
$jsConf = 'DELETE FROM ' . $this->properties['table']
. ' WHERE ' . $whereClause
@ -3019,20 +3045,21 @@ class Results
'goto' => $linkGoto,
];
$deleteUrl = Url::getFromRoute('/sql', $urlParams);
$deleteUrl = Url::getFromRoute('/sql');
$jsConf = $kill;
$deleteString = Generator::getIcon(
'b_drop',
__('Kill')
);
} else {
$deleteUrl = $deleteString = $jsConf = null;
$deleteUrl = $deleteString = $jsConf = $urlParams = null;
}
return [
$deleteUrl,
$deleteString,
$jsConf,
$urlParams,
];
}
@ -4441,7 +4468,8 @@ class Results
}
$value .= Generator::linkOrButton(
Url::getFromRoute('/sql', $urlParams),
Url::getFromRoute('/sql'),
$urlParams,
$displayedData,
$tagParams
);

View File

@ -203,6 +203,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

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

View File

@ -627,14 +627,16 @@ class Generator
$explainParams['sql_query'] = 'EXPLAIN ' . $sqlQuery;
$explainLink = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $explainParams),
Url::getFromRoute('/import'),
$explainParams,
__('Explain SQL')
) . '&nbsp;]';
} elseif (preg_match('@^EXPLAIN[[:space:]]+SELECT[[:space:]]+@i', $sqlQuery)) {
$explainParams['sql_query'] = mb_substr($sqlQuery, 8);
$explainLink = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $explainParams),
Url::getFromRoute('/import'),
$explainParams,
__('Skip Explain SQL')
) . ']';
$url = 'https://mariadb.org/explain_analyzer/analyze/'
@ -643,6 +645,7 @@ class Generator
$explainLink .= ' ['
. self::linkOrButton(
htmlspecialchars('url.php?url=' . urlencode($url)),
null,
sprintf(__('Analyze Explain at %s'), 'mariadb.org'),
[],
'_blank',
@ -657,9 +660,8 @@ class Generator
// even if the query is big and was truncated, offer the chance
// to edit it (unless it's enormous, see linkOrButton() )
if (! empty($cfg['SQLQuery']['Edit']) && empty($GLOBALS['show_as_php'])) {
$editLink .= Url::getCommon($urlParams, '&');
$editLink = ' [&nbsp;'
. self::linkOrButton($editLink, __('Edit'))
. self::linkOrButton($editLink, $urlParams, __('Edit'))
. '&nbsp;]';
} else {
$editLink = '';
@ -671,14 +673,16 @@ class Generator
if (! empty($GLOBALS['show_as_php'])) {
$phpLink = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $urlParams),
Url::getFromRoute('/import'),
$urlParams,
__('Without PHP code')
)
. '&nbsp;]';
$phpLink .= ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $urlParams),
Url::getFromRoute('/import'),
$urlParams,
__('Submit query')
)
. '&nbsp;]';
@ -687,7 +691,8 @@ class Generator
$phpParams['show_as_php'] = 1;
$phpLink = ' [&nbsp;'
. self::linkOrButton(
Url::getFromRoute('/import', $phpParams),
Url::getFromRoute('/import'),
$phpParams,
__('Create PHP code')
)
. '&nbsp;]';
@ -704,7 +709,7 @@ class Generator
) {
$refreshLink = Url::getFromRoute('/sql', $urlParams);
$refreshLink = ' [&nbsp;'
. self::linkOrButton($refreshLink, __('Refresh')) . '&nbsp;]';
. self::linkOrButton($refreshLink, $urlParams, __('Refresh')) . '&nbsp;]';
} else {
$refreshLink = '';
}
@ -737,6 +742,7 @@ class Generator
$inlineEditLink = ' [&nbsp;'
. self::linkOrButton(
'#',
null,
_pgettext('Inline edit query', 'Edit inline'),
['class' => 'inline_edit_sql']
)
@ -1038,21 +1044,28 @@ 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 $tagParams string: js confirmation; array: additional tag
* params (f.e. style="")
* @param string $target target
* @param string $urlPath the URL
* @param array<int|string, mixed>|null $urlParams URL parameters
* @param string $message the link message
* @param string|array<string, string> $tagParams 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,
$tagParams = [],
$target = '',
bool $respectUrlLengthLimit = true
): string {
$url = $urlPath;
if (is_array($urlParams)) {
$url = $urlPath . Url::getCommon($urlParams, str_contains($urlPath, '?') ? '&' : '?', false);
}
$urlLength = strlen($url);
if (! is_array($tagParams)) {
@ -1114,6 +1127,11 @@ class Generator
if (array_key_exists('class', $tagParams) && str_contains($tagParams['class'], 'create_view')) {
$url .= '?' . explode('&', $parts[1], 2)[0];
}
} else {
$url = $urlPath;
if (is_array($urlParams)) {
$url = $urlPath . Url::getCommon($urlParams, str_contains($urlPath, '?') ? '&' : '?');
}
}
foreach ($tagParams as $paramName => $paramValue) {

View File

@ -317,13 +317,13 @@ class InsertEdit
if (! $isShow) {
return ' : <a href="' . Url::getFromRoute('/table/change') . '" data-post="'
. Url::getCommon($thisUrlParams, '') . '">'
. Url::getCommon($thisUrlParams, '', false) . '">'
. $this->showTypeOrFunctionLabel($which)
. '</a>';
}
return '<th><a href="' . Url::getFromRoute('/table/change') . '" data-post="'
. Url::getCommon($thisUrlParams, '')
. Url::getCommon($thisUrlParams, '', false)
. '" title="' . __('Hide') . '">'
. $this->showTypeOrFunctionLabel($which)
. '</a></th>';

View File

@ -152,13 +152,13 @@ class NavigationTree
}
// 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'])) {
@ -180,8 +180,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;
@ -192,12 +192,12 @@ 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

View File

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

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

@ -139,7 +139,7 @@ class ReplicationGui
}
$urlParams['sr_replica_control_param'] = 'IO_THREAD';
$replicaControlIoLink = Url::getCommon($urlParams, '');
$replicaControlIoLink = Url::getCommon($urlParams, '', false);
if ($serverReplicaReplication[0]['Slave_SQL_Running'] === 'No') {
$urlParams['sr_replica_action'] = 'start';
@ -148,7 +148,7 @@ class ReplicationGui
}
$urlParams['sr_replica_control_param'] = 'SQL_THREAD';
$replicaControlSqlLink = Url::getCommon($urlParams, '');
$replicaControlSqlLink = Url::getCommon($urlParams, '', false);
if (
$serverReplicaReplication[0]['Slave_IO_Running'] === 'No'
@ -160,21 +160,21 @@ class ReplicationGui
}
$urlParams['sr_replica_control_param'] = null;
$replicaControlFullLink = Url::getCommon($urlParams, '');
$replicaControlFullLink = Url::getCommon($urlParams, '', false);
$urlParams['sr_replica_action'] = 'reset';
$replicaControlResetLink = Url::getCommon($urlParams, '');
$replicaControlResetLink = Url::getCommon($urlParams, '', false);
$urlParams = $GLOBALS['urlParams'];
$urlParams['sr_take_action'] = true;
$urlParams['sr_replica_skip_error'] = true;
$replicaSkipErrorLink = Url::getCommon($urlParams, '');
$replicaSkipErrorLink = Url::getCommon($urlParams, '', false);
$urlParams = $GLOBALS['urlParams'];
$urlParams['replica_configure'] = true;
$urlParams['repl_clear_scr'] = true;
$reconfigurePrimaryLink = Url::getCommon($urlParams, '');
$reconfigurePrimaryLink = Url::getCommon($urlParams, '', false);
$replicaStatusTable = $this->getHtmlForReplicationStatusTable('replica', true, false);

View File

@ -7,13 +7,20 @@ declare(strict_types=1);
namespace PhpMyAdmin;
use PhpMyAdmin\Crypto\Crypto;
use function base64_decode;
use function base64_encode;
use function htmlentities;
use function htmlspecialchars;
use function http_build_query;
use function in_array;
use function ini_get;
use function is_array;
use function json_encode;
use function str_contains;
use function strlen;
use function strtr;
/**
* Static methods for URL/hidden inputs generating
@ -164,12 +171,13 @@ 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
*/
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);
}
/**
@ -197,15 +205,14 @@ 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
*/
public static function getCommonRaw(array $params = [], $divider = '?')
public static function getCommonRaw(array $params = [], $divider = '?', $encrypt = true)
{
global $config;
$separator = self::getArgSeparator();
// avoid overwriting when creating navigation panel links to servers
if (
isset($GLOBALS['server'])
@ -221,7 +228,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;
@ -230,6 +237,71 @@ class Url
return '';
}
/**
* @param array<int|string, mixed> $params
* @param bool $encrypt whether to encrypt URL params
*
* @return string
*/
public static function buildHttpQuery($params, $encrypt = true)
{
global $config;
$separator = self::getArgSeparator();
if (! $encrypt || ! $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((string) json_encode($paramsToEncrypt));
}
return http_build_query($data, '', $separator);
}
public static function encryptQuery(string $query): string
{
$crypto = new Crypto();
return strtr(base64_encode($crypto->encrypt($query)), '+/', '-_');
}
public static function decryptQuery(string $query): ?string
{
$crypto = new Crypto();
return $crypto->decrypt(base64_decode(strtr($query, '-_', '+/')));
}
/**
* Returns url separator
*

View File

@ -2631,9 +2631,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

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

@ -385,6 +385,11 @@ parameters:
count: 1
path: libraries/classes/Config/Form.php
-
message: "#^Unreachable statement \\- code above always terminates\\.$#"
count: 2
path: libraries/classes/Config/Form.php
-
message: "#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\.$#"
count: 1
@ -632,7 +637,7 @@ parameters:
-
message: "#^Cannot cast mixed to string\\.$#"
count: 43
count: 44
path: libraries/classes/Config/Settings.php
-
@ -2880,6 +2885,16 @@ parameters:
count: 1
path: libraries/classes/Display/Results.php
-
message: "#^Binary operation \"\\+\" between array\\<string, bool\\|string\\>\\|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, bool\\|string\\>\\|string\\|null and array\\{default_action\\: 'update'\\} results in an error\\.$#"
count: 3
path: libraries/classes/Display/Results.php
-
message: "#^Cannot access offset int on mixed\\.$#"
count: 2
@ -3225,11 +3240,21 @@ parameters:
count: 1
path: libraries/classes/Display/Results.php
-
message: "#^Offset 3 does not exist on array\\{string\\|null, string\\|null, string\\|null\\}\\.$#"
count: 1
path: libraries/classes/Display/Results.php
-
message: "#^Parameter \\#1 \\$offset of method PhpMyAdmin\\\\Dbal\\\\ResultInterface\\:\\:seek\\(\\) expects int, float\\|int given\\.$#"
count: 2
path: libraries/classes/Display/Results.php
-
message: "#^Parameter \\#1 \\$params of static method PhpMyAdmin\\\\Url\\:\\:getCommon\\(\\) expects array\\<string, bool\\|int\\|string\\>, array\\<int\\|string, mixed\\> given\\.$#"
count: 1
path: libraries/classes/Display/Results.php
-
message: "#^Parameter \\#3 \\$colOrder of method PhpMyAdmin\\\\Display\\\\Results\\:\\:getRowValues\\(\\) expects array\\|false, mixed given\\.$#"
count: 1
@ -4341,8 +4366,8 @@ parameters:
path: libraries/classes/Html/Generator.php
-
message: "#^Parameter \\#1 \\$string of static method PhpMyAdmin\\\\Sanitize\\:\\:escapeJsString\\(\\) expects string, mixed given\\.$#"
count: 1
message: "#^Parameter \\#1 \\$params of static method PhpMyAdmin\\\\Url\\:\\:getCommon\\(\\) expects array\\<string, bool\\|int\\|string\\>, array\\<int\\|string, mixed\\> given\\.$#"
count: 2
path: libraries/classes/Html/Generator.php
-
@ -8410,6 +8435,16 @@ parameters:
count: 1
path: libraries/classes/Theme.php
-
message: "#^Unreachable statement \\- code above always terminates\\.$#"
count: 1
path: libraries/classes/Theme.php
-
message: "#^If condition is always true\\.$#"
count: 1
path: libraries/classes/ThemeManager.php
-
message: "#^Method PhpMyAdmin\\\\ThemeManager\\:\\:getThemesArray\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
@ -8425,6 +8460,11 @@ parameters:
count: 1
path: libraries/classes/ThemeManager.php
-
message: "#^Unreachable statement \\- code above always terminates\\.$#"
count: 2
path: libraries/classes/ThemeManager.php
-
message: "#^Cannot access property \\$table on PhpMyAdmin\\\\SqlParser\\\\Components\\\\Expression\\|string\\.$#"
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="index.php">
<InvalidGlobal occurrences="1">
<code>global $route, $containerBuilder, $request;</code>
@ -4127,15 +4127,13 @@
<MixedArgumentTypeCoercion occurrences="1">
<code>['db' =&gt; $db]</code>
</MixedArgumentTypeCoercion>
<MixedArrayAccess occurrences="8">
<MixedArrayAccess occurrences="6">
<code>$_POST['view']['as']</code>
<code>$_POST['view']['as']</code>
<code>$_POST['view']['column_names']</code>
<code>$_POST['view']['column_names']</code>
<code>$_POST['view']['name']</code>
<code>$_POST['view']['name']</code>
<code>$_POST['view']['with']</code>
<code>$_POST['view']['with']</code>
</MixedArrayAccess>
<MixedArrayAssignment occurrences="4">
<code>$urlParams['back']</code>
@ -4218,16 +4216,22 @@
<MixedArrayOffset occurrences="1">
<code>$GLOBALS['cfg']['TrustedProxies'][$direct_ip]</code>
</MixedArrayOffset>
<MixedAssignment occurrences="10">
<MixedAssignment occurrences="16">
<code>$GLOBALS[$post_key]</code>
<code>$_GET[$urlQueryParamKey]</code>
<code>$_POST[$urlQueryParamKey]</code>
<code>$_REQUEST[$urlQueryParamKey]</code>
<code>$a</code>
<code>$a[$last_key]</code>
<code>$direct_ip</code>
<code>$one_post_pattern</code>
<code>$parsedBody[$urlQueryParamKey]</code>
<code>$path[$depth]</code>
<code>$query</code>
<code>$queryParams[$urlQueryParamKey]</code>
<code>$secret</code>
<code>$secret</code>
<code>$urlQueryParamValue</code>
<code>$value</code>
</MixedAssignment>
<MixedInferredReturnType occurrences="2">
@ -4360,6 +4364,15 @@
<code>$_POST['field_extra'][$i] ?? false</code>
</PossiblyFalseArgument>
</file>
<file src="libraries/classes/Crypto/Crypto.php">
<MixedAssignment occurrences="2">
<code>$key</code>
<code>$key</code>
</MixedAssignment>
<MixedMethodCall occurrences="1">
<code>get</code>
</MixedMethodCall>
</file>
<file src="libraries/classes/Database/CentralColumns.php">
<InvalidScalarArgument occurrences="1">
<code>$tn_pageNow</code>
@ -5622,8 +5635,10 @@
</MixedAssignment>
</file>
<file src="libraries/classes/Dbal/DbiMysqli.php">
<MixedArgument occurrences="7">
<MixedArgument occurrences="9">
<code>$host</code>
<code>$host</code>
<code>$server['port']</code>
<code>$server['port']</code>
<code>$server['ssl_ca'] ?? ''</code>
<code>$server['ssl_ca_path'] ?? ''</code>
@ -5685,9 +5700,13 @@
<code>$displayParts['nav_bar'] == '1' &amp;&amp; $statement !== null &amp;&amp; empty($statement-&gt;limit)</code>
<code>[]</code>
</DocblockTypeContradiction>
<InvalidArgument occurrences="1">
<InvalidArgument occurrences="2">
<code>$added[$orgFullTableName]</code>
<code>$multiOrderUrlParams</code>
</InvalidArgument>
<InvalidArrayOffset occurrences="1">
<code>$delUrlParams</code>
</InvalidArrayOffset>
<MixedArgument occurrences="53">
<code>$_SESSION['tmpval']['max_rows']</code>
<code>$_SESSION['tmpval']['pos'] / $_SESSION['tmpval']['max_rows']</code>
@ -5994,14 +6013,28 @@
<code>$this-&gt;properties['num_rows'] &gt; 0 ? $this-&gt;properties['num_rows'] - 1 : 0</code>
<code>$this-&gt;properties['num_rows'] &gt; 0 ? $this-&gt;properties['num_rows'] - 1 : 0</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="1">
<code>$colVisibCurrent</code>
</PossiblyNullArgument>
<PossiblyNullArrayAccess occurrences="1">
<code>$firstStatement-&gt;order[$key]</code>
</PossiblyNullArrayAccess>
<PossiblyNullOperand occurrences="2">
<PossiblyNullOperand occurrences="8">
<code>$dispval</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$editCopyUrlParams</code>
<code>$this-&gt;getSortedColumnMessage($dtResult, $sortExpressionNoDirection[$i])</code>
</PossiblyNullOperand>
<RedundantCast occurrences="3">
@ -7306,18 +7339,21 @@
</TypeDoesNotContainNull>
</file>
<file src="libraries/classes/Html/Generator.php">
<InvalidArgument occurrences="2">
<code>$urlParams</code>
<code>$urlParams</code>
</InvalidArgument>
<InvalidArrayOffset occurrences="2">
<code>$server['ssl']</code>
<code>$server['ssl_verify']</code>
</InvalidArrayOffset>
<MixedArgument occurrences="23">
<MixedArgument occurrences="20">
<code>$GLOBALS['special_message']</code>
<code>$GLOBALS['special_message']</code>
<code>$alt</code>
<code>$cfg['MaxCharactersInDisplayedSQL']</code>
<code>$defaultFunction</code>
<code>$field['True_Type']</code>
<code>$paramValue</code>
<code>$queryBase</code>
<code>$queryBase</code>
<code>$queryBase</code>
@ -7330,15 +7366,12 @@
<code>$sqlQuery</code>
<code>$sqlQuery</code>
<code>$subvalue</code>
<code>$tagParams['class']</code>
<code>$title</code>
<code>$tmp</code>
<code>$value</code>
</MixedArgument>
<MixedArgumentTypeCoercion occurrences="3">
<MixedArgumentTypeCoercion occurrences="2">
<code>$classes</code>
<code>$key</code>
<code>$urlParams</code>
</MixedArgumentTypeCoercion>
<MixedArrayAccess occurrences="3">
<code>$cfg['DefaultFunctions']['FUNC_' . $currentClass]</code>
@ -7351,17 +7384,15 @@
<code>$_SESSION['tmpval']['max_rows']</code>
<code>$_SESSION['tmpval']['pos']</code>
</MixedArrayAssignment>
<MixedAssignment occurrences="14">
<MixedAssignment occurrences="12">
<code>$alt</code>
<code>$defaultFunction</code>
<code>$defaultFunction</code>
<code>$defaultFunction</code>
<code>$paramValue</code>
<code>$queryBase</code>
<code>$sqlQuery</code>
<code>$subvalue</code>
<code>$title</code>
<code>$tmp</code>
<code>$urlParams['sql_query']</code>
<code>$value</code>
<code>$value</code>
@ -8104,10 +8135,10 @@
<code>isset($this-&gt;pos)</code>
</DocblockTypeContradiction>
<MixedArgument occurrences="34">
<code>$_POST['aPath']</code>
<code>$_POST['n' . $count . '_aPath']</code>
<code>$_POST['n' . $count . '_vPath']</code>
<code>$_REQUEST['aPath']</code>
<code>$_REQUEST['vPath']</code>
<code>$_POST['vPath']</code>
<code>$container-&gt;children</code>
<code>$container-&gt;realName</code>
<code>$db</code>
@ -9014,12 +9045,12 @@
<code>$propertyItem-&gt;getName()</code>
<code>$subgroup_header-&gt;getName()</code>
</PossiblyNullOperand>
<RedundantCast occurrences="1">
<code>strtolower</code>
</RedundantCast>
<RedundantConditionGivenDocblockType occurrences="1">
<code>$properties != null</code>
</RedundantConditionGivenDocblockType>
<RedundantFunctionCall occurrences="1">
<code>strtolower</code>
</RedundantFunctionCall>
<UnnecessaryVarAnnotation occurrences="3">
<code>RadioPropertyItem</code>
<code>SelectPropertyItem</code>
@ -13401,12 +13432,11 @@
<MixedArgumentTypeCoercion occurrences="1">
<code>$values</code>
</MixedArgumentTypeCoercion>
<MixedArrayAccess occurrences="5">
<MixedArrayAccess occurrences="4">
<code>$_POST['criteriaColumnNames'][$column_index]</code>
<code>$_POST['criteriaColumnTypes'][$column_index]</code>
<code>$_POST['criteriaValues'][$column_index]</code>
<code>$_POST['geom_func'][$column_index]</code>
<code>$criteriaValues[0]</code>
</MixedArrayAccess>
<MixedArrayOffset occurrences="4">
<code>$_POST['criteriaColumnNames'][$column_index]</code>
@ -13798,7 +13828,9 @@
<MixedArrayOffset occurrences="1">
<code>$params[$skipping]</code>
</MixedArrayOffset>
<MixedAssignment occurrences="3">
<MixedAssignment occurrences="5">
<code>$paramValue</code>
<code>$paramsToEncrypt[$paramKey]</code>
<code>$skipping</code>
<code>$value</code>
<code>$values['token']</code>
@ -13806,7 +13838,8 @@
<MixedInferredReturnType occurrences="1">
<code>string</code>
</MixedInferredReturnType>
<MixedMethodCall occurrences="3">
<MixedMethodCall occurrences="4">
<code>get</code>
<code>get</code>
<code>getCookie</code>
<code>getCookie</code>
@ -14666,6 +14699,9 @@
</UndefinedMethod>
</file>
<file src="test/classes/CoreTest.php">
<InvalidArgument occurrences="1">
<code>testPopulateRequestWithEncryptedQueryParamsWithInvalidParam</code>
</InvalidArgument>
<MixedArgument occurrences="9">
<code>$arr['arr']</code>
<code>$arr['arr']</code>
@ -14703,6 +14739,12 @@
<code>array</code>
<code>array</code>
</MixedInferredReturnType>
<MixedMethodCall occurrences="4">
<code>set</code>
<code>set</code>
<code>set</code>
<code>set</code>
</MixedMethodCall>
<PossiblyNullArgument occurrences="1">
<code>$page</code>
</PossiblyNullArgument>
@ -14717,6 +14759,21 @@
<code>testGetColumnCreationQuery</code>
</PossiblyInvalidArgument>
</file>
<file src="test/classes/Crypto/CryptoTest.php">
<EmptyArrayAccess occurrences="1">
<code>$_SESSION['URLQueryEncryptionSecretKey']</code>
</EmptyArrayAccess>
<MixedArgument occurrences="1">
<code>$_SESSION['URLQueryEncryptionSecretKey']</code>
</MixedArgument>
<MixedMethodCall occurrences="5">
<code>set</code>
<code>set</code>
<code>set</code>
<code>set</code>
<code>set</code>
</MixedMethodCall>
</file>
<file src="test/classes/Database/CentralColumnsTest.php">
<MixedArgument occurrences="1"/>
<MixedArrayAccess occurrences="1">
@ -16060,6 +16117,15 @@
<code>array</code>
</MixedInferredReturnType>
</file>
<file src="test/classes/UrlTest.php">
<MixedMethodCall occurrences="5">
<code>set</code>
<code>set</code>
<code>set</code>
<code>set</code>
<code>set</code>
</MixedMethodCall>
</file>
<file src="test/classes/UserPreferencesTest.php">
<MixedArgument occurrences="1">
<code>$_SESSION['userconfig']</code>

View File

@ -89,11 +89,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

@ -39,12 +39,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

@ -84,7 +84,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('/'),
@ -92,7 +93,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

@ -69,12 +69,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

@ -44,12 +44,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 d-print-none edit_row_anchor{{ not edit.clause_is_unique ? ' nonunique' }}">
<span class="text-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 d-print-none">
<span class="text-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 d-print-none{{ is_ajax ? ' ajax' }}">
<span class="text-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 d-print-none{{ is_ajax ? ' ajax' }}">
<span class="text-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 d-print-none">
<span class="text-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 d-print-none edit_row_anchor{{ not edit.clause_is_unique ? ' nonunique' }}">
<span class="text-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

@ -259,6 +259,7 @@
{{ link_or_button(
'#',
null,
get_icon('b_insrow', 'Copy to clipboard'|trans, true),
{'id': 'copyToClipBoard', 'class': 'btn'}
) }}
@ -267,20 +268,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'}
) }}
@ -289,7 +293,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

@ -30,12 +30,12 @@
{% set columns_count = index.getColumnCount() %}
<tr class="noclick">
<td rowspan="{{ columns_count }}" class="edit_index d-print-none 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 d-print-none 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>
@ -54,7 +54,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('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('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

@ -297,7 +297,7 @@
<div class="alert alert-info" role="alert">
{{ get_icon('s_notice', 'Note: Enabling the database statistics here might cause heavy traffic between the web server and the MySQL server.'|trans) }}
</div>
<a class="card-link" href="{{ url('/server/databases') }}" data-post="{{ get_common({'statistics': '1'}, '') }}" title="{% trans 'Enable statistics' %}">
<a class="card-link" href="{{ url('/server/databases') }}" data-post="{{ get_common({'statistics': '1'}, '', false) }}" title="{% trans 'Enable statistics' %}">
{% trans 'Enable statistics' %}
</a>
</div>

View File

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

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

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

@ -119,7 +119,7 @@
{% endif %}
{% if column.pma_type in gis_data_types %}
<span class="open_gis_editor" data-row-id="{{ row_id }}">{{ link_or_button('#', get_icon('b_edit', 'Edit/Insert'|trans), [], '_blank') }}</span>
<span class="open_gis_editor" data-row-id="{{ row_id }}">{{ link_or_button('#', null, get_icon('b_edit', 'Edit/Insert'|trans), [], '_blank') }}</span>
{% endif %}
{% endif %}
</td>

View File

@ -335,7 +335,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') }}
@ -344,7 +344,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') }}
@ -352,7 +352,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') }}
@ -360,7 +360,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') }}
@ -372,7 +372,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') }}
@ -380,7 +380,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') }}
@ -389,7 +389,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') }}
@ -405,12 +405,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',
@ -422,12 +423,13 @@
</li>
<li class="list-group-item">
{{ link_or_button(
url('/sql', url_params|merge({
url('/sql'),
url_params|merge({
'sql_query': 'DELETE FROM ' ~ 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 (DELETE FROM)'|trans,
{
'id': 'delete_tbl_anchor',
@ -440,14 +442,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

@ -27,14 +27,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

@ -116,7 +116,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>
@ -141,7 +141,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 %}
@ -156,7 +156,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 %}
@ -171,7 +171,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 %}
@ -196,7 +196,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 %}
@ -215,7 +215,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 %}
@ -234,7 +234,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>
@ -368,7 +368,8 @@
<div id="structure-action-links" class="d-print-none">
{% 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 %}
@ -381,7 +382,7 @@
'table': table,
'sql_query': 'SELECT * FROM ' ~ backquote(table) ~ ' PROCEDURE ANALYSE()',
'session_max_rows': 'all'
}) }}">
}, '', false) }}">
{{ get_icon(
'b_tblanalyse',
'Propose table structure'|trans,
@ -475,7 +476,7 @@
'db': db,
'table': table,
'index': index.getName()
}, '') }}">
}, '', false) }}">
{{ get_icon('b_edit', 'Edit'|trans) }}
</a>
</td>
@ -484,7 +485,7 @@
'db': db,
'table': table,
'index': index.getName()
}, '') }}">
}, '', false) }}">
{{ get_icon('b_rename', 'Rename'|trans) }}
</a>
</td>
@ -503,7 +504,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

@ -77,6 +77,7 @@ class ServerTest extends TestCase
'tracking_add_drop_view' => true,
'tracking_add_drop_table' => true,
'tracking_add_drop_database' => true,
'hide_connection_errors' => false,
];
/**
@ -172,6 +173,7 @@ class ServerTest extends TestCase
['tracking_add_drop_view', null, true],
['tracking_add_drop_table', null, true],
['tracking_add_drop_database', null, true],
['hide_connection_errors', null, false],
],
],
'valid values' => [
@ -234,6 +236,7 @@ class ServerTest extends TestCase
['tracking_add_drop_view', true, true],
['tracking_add_drop_table', true, true],
['tracking_add_drop_database', true, true],
['hide_connection_errors', true, true],
],
],
'valid values 2' => [
@ -271,6 +274,7 @@ class ServerTest extends TestCase
['tracking_add_drop_view', false, false],
['tracking_add_drop_table', false, false],
['tracking_add_drop_database', false, false],
['hide_connection_errors', false, false],
],
],
'valid values 3' => [
@ -346,6 +350,7 @@ class ServerTest extends TestCase
['tracking_add_drop_view', 0, false],
['tracking_add_drop_table', 0, false],
['tracking_add_drop_database', 0, false],
['hide_connection_errors', 1, true],
],
],
'invalid values' => [

View File

@ -78,6 +78,8 @@ class SettingsTest extends TestCase
'LoginCookieDeleteAll' => true,
'UseDbSearch' => true,
'IgnoreMultiSubmitErrors' => false,
'URLQueryEncryption' => false,
'URLQueryEncryptionSecretKey' => '',
'AllowArbitraryServer' => false,
'ArbitraryServerRegexp' => '',
'CaptchaMethod' => 'invisible',
@ -415,6 +417,8 @@ class SettingsTest extends TestCase
['LoginCookieDeleteAll', null, true],
['UseDbSearch', null, true],
['IgnoreMultiSubmitErrors', null, false],
['URLQueryEncryption', null, false],
['URLQueryEncryptionSecretKey', null, ''],
['AllowArbitraryServer', null, false],
['ArbitraryServerRegexp', null, ''],
['CaptchaMethod', null, 'invisible'],
@ -608,6 +612,8 @@ class SettingsTest extends TestCase
['LoginCookieDeleteAll', false, false],
['UseDbSearch', false, false],
['IgnoreMultiSubmitErrors', true, true],
['URLQueryEncryption', true, true],
['URLQueryEncryptionSecretKey', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'],
['AllowArbitraryServer', true, true],
['ArbitraryServerRegexp', 'test', 'test'],
['CaptchaMethod', 'checkbox', 'checkbox'],
@ -927,6 +933,7 @@ class SettingsTest extends TestCase
['LoginCookieDeleteAll', 0, false],
['UseDbSearch', 0, false],
['IgnoreMultiSubmitErrors', 1, true],
['URLQueryEncryption', 1, true],
['AllowArbitraryServer', 1, true],
['ArbitraryServerRegexp', 1234, '1234'],
['CaptchaApi', 1234, '1234'],

View File

@ -5,8 +5,10 @@ declare(strict_types=1);
namespace PhpMyAdmin\Tests;
use PhpMyAdmin\Core;
use PhpMyAdmin\Http\ServerRequest;
use PhpMyAdmin\ResponseRenderer;
use PhpMyAdmin\Sanitize;
use PhpMyAdmin\Url;
use stdClass;
use function __;
@ -19,6 +21,7 @@ use function ob_get_contents;
use function ob_start;
use function preg_quote;
use function serialize;
use function str_repeat;
/**
* @covers \PhpMyAdmin\Core
@ -38,6 +41,7 @@ class CoreTest extends AbstractNetworkTestCase
$GLOBALS['db'] = '';
$GLOBALS['table'] = '';
$GLOBALS['PMA_PHP_SELF'] = 'http://example.net/';
$GLOBALS['config']->set('URLQueryEncryption', false);
}
/**
@ -924,4 +928,73 @@ class CoreTest extends AbstractNetworkTestCase
// Must work now, (good secret and blowfish_secret)
$this->assertTrue(Core::checkSqlQuerySignature($sqlQuery, $hmac));
}
public function testPopulateRequestWithEncryptedQueryParams(): void
{
global $config;
$_SESSION = [];
$config->set('URLQueryEncryption', true);
$config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$_GET = ['pos' => '0', 'eq' => Url::encryptQuery('{"db":"test_db","table":"test_table"}')];
$_REQUEST = $_GET;
$request = $this->createStub(ServerRequest::class);
$request->method('getQueryParams')->willReturn($_GET);
$request->method('getParsedBody')->willReturn(null);
$request->method('withQueryParams')->willReturnSelf();
$request->method('withParsedBody')->willReturnSelf();
Core::populateRequestWithEncryptedQueryParams($request);
$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 $config;
$_SESSION = [];
$config->set('URLQueryEncryption', true);
$config->set('URLQueryEncryptionSecretKey', str_repeat('a', 32));
$_GET = $encrypted;
$_REQUEST = $encrypted;
$request = $this->createStub(ServerRequest::class);
$request->method('getQueryParams')->willReturn($_GET);
$request->method('getParsedBody')->willReturn(null);
$request->method('withQueryParams')->willReturnSelf();
$request->method('withParsedBody')->willReturnSelf();
Core::populateRequestWithEncryptedQueryParams($request);
$this->assertEquals($decrypted, $_GET);
$this->assertEquals($decrypted, $_REQUEST);
}
/**
* @return array<int, array<int, array<string, string|mixed[]>>>
*/
public function providerForTestPopulateRequestWithEncryptedQueryParamsWithInvalidParam(): array
{
return [
[[], []],
[['eq' => []], []],
[['eq' => ''], []],
[['eq' => 'invalid'], []],
];
}
}

View File

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

View File

@ -1439,11 +1439,12 @@ class ResultsTest extends AbstractTestCase
'columns' => [
[
'column_name' => 'id',
'order_link' => '<a href="index.php?route=/sql&db=test_db&table=test_table'
'order_link' => '<a href="index.php?route=/sql&server=0&lang=en&db=test_db&table=test_table'
. '&sql_query=SELECT+%2A+FROM+%60test_db%60.%60test_table%60++%0AORDER+BY+%60id%60+ASC'
. '&sql_signature=dcfe20b407b35309f6af81f745e77a10f723d39b082d2a8f9cb8e75b17c4d3ce'
. '&session_max_rows=25&is_browse_distinct=0&server=0&lang=en" class="sortlink">id'
. '<input type="hidden" value="index.php?route=/sql&db=test_db&table=test_table'
. '<input type="hidden" value="'
. 'index.php?route=/sql&server=0&lang=en&db=test_db&table=test_table'
. '&sql_query=SELECT+%2A+FROM+%60test_db%60.%60test_table%60++%0AORDER+BY+%60id%60+ASC'
. '&sql_signature=dcfe20b407b35309f6af81f745e77a10f723d39b082d2a8f9cb8e75b17c4d3ce'
. '&session_max_rows=25&is_browse_distinct=0&server=0&lang=en"></a>'
@ -1465,11 +1466,12 @@ class ResultsTest extends AbstractTestCase
],
[
'column_name' => 'name',
'order_link' => '<a href="index.php?route=/sql&db=test_db&table=test_table'
'order_link' => '<a href="index.php?route=/sql&server=0&lang=en&db=test_db&table=test_table'
. '&sql_query=SELECT+%2A+FROM+%60test_db%60.%60test_table%60++%0AORDER+BY+%60name%60+ASC'
. '&sql_signature=0d06fa8d6795b1c69892cca27d6213c08401bd434145d16cb35c365ab3e03039'
. '&session_max_rows=25&is_browse_distinct=0&server=0&lang=en" class="sortlink">name'
. '<input type="hidden" value="index.php?route=/sql&db=test_db&table=test_table'
. '<input type="hidden" value="'
. 'index.php?route=/sql&server=0&lang=en&db=test_db&table=test_table'
. '&sql_query=SELECT+%2A+FROM+%60test_db%60.%60test_table%60++%0AORDER+BY+%60name%60+ASC'
. '&sql_signature=0d06fa8d6795b1c69892cca27d6213c08401bd434145d16cb35c365ab3e03039'
. '&session_max_rows=25&is_browse_distinct=0&server=0&lang=en"></a>'
@ -1491,12 +1493,13 @@ class ResultsTest extends AbstractTestCase
],
[
'column_name' => 'datetimefield',
'order_link' => '<a href="index.php?route=/sql&db=test_db&table=test_table'
'order_link' => '<a href="index.php?route=/sql&server=0&lang=en&db=test_db&table=test_table'
. '&sql_query=SELECT+%2A+FROM+%60test_db%60.%60test_table%60++%0A'
. 'ORDER+BY+%60datetimefield%60+DESC'
. '&sql_signature=1c46f7e3c625f9e0846fb2de844ca1732319e5fb7fb93e96c89a4b6218579358'
. '&session_max_rows=25&is_browse_distinct=0&server=0&lang=en" class="sortlink">datetimefield'
. '<input type="hidden" value="index.php?route=/sql&db=test_db&table=test_table'
. '<input type="hidden" value="'
. 'index.php?route=/sql&server=0&lang=en&db=test_db&table=test_table'
. '&sql_query=SELECT+%2A+FROM+%60test_db%60.%60test_table%60++%0A'
. 'ORDER+BY+%60datetimefield%60+DESC'
. '&sql_signature=1c46f7e3c625f9e0846fb2de844ca1732319e5fb7fb93e96c89a4b6218579358'

View File

@ -6,6 +6,7 @@ namespace PhpMyAdmin\Tests\Html;
use PhpMyAdmin\Html\Generator;
use PhpMyAdmin\Tests\AbstractTestCase;
use PhpMyAdmin\Url;
use PhpMyAdmin\Util;
use function __;
@ -222,10 +223,13 @@ class GeneratorTest extends AbstractTestCase
*/
public function linksOrButtons(): array
{
parent::setGlobalConfig();
return [
[
[
'index.php',
null,
'text',
],
1000,
@ -233,15 +237,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',
@ -252,6 +258,7 @@ class GeneratorTest extends AbstractTestCase
[
[
'https://mariadb.org/explain_analyzer/analyze/?client=phpMyAdmin&amp;raw_explain=%2B---%2B',
null,
'text',
[],
'target',
@ -264,6 +271,7 @@ class GeneratorTest extends AbstractTestCase
[
[
'https://mariadb.org/explain_analyzer/analyze/?client=phpMyAdmin&amp;raw_explain=%2B---%2B',
null,
'text',
[],
'target',
@ -276,6 +284,7 @@ class GeneratorTest extends AbstractTestCase
[
[
'url.php?url=http://phpmyadmin.net/',
null,
'text',
[],
'_blank',
@ -283,6 +292,42 @@ class GeneratorTest extends AbstractTestCase
1000,
'<a href="url.php?url=http://phpmyadmin.net/" target="_blank" rel="noopener noreferrer">text</a>',
],
[
[
Url::getFromRoute('/server/databases'),
['some' => 'parameter'],
'text',
],
20,
'<a href="index.php" data-post="route=/server/databases&some=parameter&lang=en">text</a>',
],
[
[
Url::getFromRoute('/server/databases'),
null,
'text',
],
20,
'<a href="index.php" data-post="route=/server/databases">text</a>',
],
[
[
Url::getFromRoute('/server/databases'),
['some' => 'parameter'],
'text',
],
100,
'<a href="index.php?route=/server/databases&some=parameter&lang=en" >text</a>',
],
[
[
Url::getFromRoute('/server/databases'),
null,
'text',
],
100,
'<a href="index.php?route=/server/databases" >text</a>',
],
];
}

View File

@ -6,6 +6,9 @@ namespace PhpMyAdmin\Tests;
use PhpMyAdmin\Url;
use function is_string;
use function parse_str;
use function str_repeat;
use function urldecode;
/**
@ -22,6 +25,7 @@ class UrlTest extends AbstractTestCase
parent::setUp();
parent::setLanguage();
unset($_COOKIE['pma_lang']);
$GLOBALS['config']->set('URLQueryEncryption', false);
}
/**
@ -177,4 +181,62 @@ class UrlTest extends AbstractTestCase
Url::getHiddenFields([])
);
}
/**
* @return void
*/
public function testBuildHttpQueryWithUrlQueryEncryptionDisabled()
{
global $config;
$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 $config;
$_SESSION = [];
$config->set('URLQueryEncryption', true);
$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->assertNotNull($decrypted);
$this->assertJson($decrypted);
$this->assertSame('{"db":"test_db","table":"test_table"}', $decrypted);
}
/**
* @return void
*/
public function testQueryEncryption()
{
global $config;
$_SESSION = [];
$config->set('URLQueryEncryption', true);
$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);
}
}