diff --git a/composer.json b/composer.json index f373bf4ff8..c980a03675 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/config.sample.inc.php b/config.sample.inc.php index be32136f4f..34b6a9dabf 100644 --- a/config.sample.inc.php +++ b/config.sample.inc.php @@ -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 . diff --git a/doc/config.rst b/doc/config.rst index ea402618d9..156c45de25 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -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 ----------------------------- diff --git a/js/src/navigation.js b/js/src/navigation.js index fc64a872d0..d2e7d59ff7 100644 --- a/js/src/navigation.js +++ b/js/src/navigation.js @@ -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) { diff --git a/libraries/classes/Core.php b/libraries/classes/Core.php index f3c6bce589..75a4a7c6ad 100644 --- a/libraries/classes/Core.php +++ b/libraries/classes/Core.php @@ -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; + } + } } diff --git a/libraries/classes/Crypto/Crypto.php b/libraries/classes/Crypto/Crypto.php new file mode 100644 index 0000000000..fc15c74353 --- /dev/null +++ b/libraries/classes/Crypto/Crypto.php @@ -0,0 +1,175 @@ +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; + } +} diff --git a/libraries/classes/Dbal/DbiMysqli.php b/libraries/classes/Dbal/DbiMysqli.php index 0a2592e1ec..66c8f7ee01 100644 --- a/libraries/classes/Dbal/DbiMysqli.php +++ b/libraries/classes/Dbal/DbiMysqli.php @@ -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; } diff --git a/libraries/classes/Display/Results.php b/libraries/classes/Display/Results.php index c5910c95b4..e511386bab 100644 --- a/libraries/classes/Display/Results.php +++ b/libraries/classes/Display/Results.php @@ -1559,9 +1559,8 @@ class Results $tmp_image = ''
                      . $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 - . ''; + . ''; 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 5 element array - $edit_url, $copy_url, + * @return 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 ); diff --git a/libraries/classes/ErrorReport.php b/libraries/classes/ErrorReport.php index ebde9fff25..6ddd7c02f3 100644 --- a/libraries/classes/ErrorReport.php +++ b/libraries/classes/ErrorReport.php @@ -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 = ''; diff --git a/libraries/classes/Export.php b/libraries/classes/Export.php index 6b9ef60a01..35130c6170 100644 --- a/libraries/classes/Export.php +++ b/libraries/classes/Export.php @@ -565,16 +565,18 @@ class Export */ $back_button = '

[ $par_value) { diff --git a/libraries/classes/InsertEdit.php b/libraries/classes/InsertEdit.php index 6d7e5f4fd3..abc3c5f93c 100644 --- a/libraries/classes/InsertEdit.php +++ b/libraries/classes/InsertEdit.php @@ -329,13 +329,13 @@ class InsertEdit if (! $is_show) { return ' : ' + . Url::getCommon($this_url_params, '', false) . '">' . $this->showTypeOrFunctionLabel($which) . ''; } return '' . $this->showTypeOrFunctionLabel($which) . ''; @@ -920,7 +920,8 @@ class InsertEdit 'rownumber' => $rownumber, 'data' => $data, ], - '' + '', + false ) . '">' . Generator::getIcon('b_browse', __('Browse foreign values')) . ''; @@ -1788,6 +1789,7 @@ class InsertEdit return '' . Generator::linkOrButton( '#', + null, $edit_str, [], '_blank' diff --git a/libraries/classes/Navigation/NavigationTree.php b/libraries/classes/Navigation/NavigationTree.php index 09aeed50ca..8fec38cc30 100644 --- a/libraries/classes/Navigation/NavigationTree.php +++ b/libraries/classes/Navigation/NavigationTree.php @@ -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 .= ""; $retval .= '' . $icon . ''; @@ -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 .= " "; @@ -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)); + } } diff --git a/libraries/classes/Navigation/Nodes/NodeDatabase.php b/libraries/classes/Navigation/Nodes/NodeDatabase.php index 851c757011..179ac3f649 100644 --- a/libraries/classes/Navigation/Nodes/NodeDatabase.php +++ b/libraries/classes/Navigation/Nodes/NodeDatabase.php @@ -702,7 +702,7 @@ class NodeDatabase extends Node ]; $ret = '' . '' . Generator::getImage( 'show', diff --git a/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php b/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php index dde0e1bfa5..deb66ec6c1 100644 --- a/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php +++ b/libraries/classes/Navigation/Nodes/NodeDatabaseChild.php @@ -46,7 +46,7 @@ abstract class NodeDatabaseChild extends Node $ret = '' . '' . Generator::getImage('hide', __('Hide')) . ''; diff --git a/libraries/classes/Relation.php b/libraries/classes/Relation.php index d0dfd0ff92..0d21c0d18d 100644 --- a/libraries/classes/Relation.php +++ b/libraries/classes/Relation.php @@ -2223,7 +2223,8 @@ class Relation ); } $message->addParamHtml( - '' + '' ); $message->addParamHtml(''); diff --git a/libraries/classes/ReplicationGui.php b/libraries/classes/ReplicationGui.php index 9939331a37..c54b16ce26 100644 --- a/libraries/classes/ReplicationGui.php +++ b/libraries/classes/ReplicationGui.php @@ -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); diff --git a/libraries/classes/Server/Privileges.php b/libraries/classes/Server/Privileges.php index f7709b233e..3c59fbd3f3 100644 --- a/libraries/classes/Server/Privileges.php +++ b/libraries/classes/Server/Privileges.php @@ -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, '&'); } diff --git a/libraries/classes/Server/UserGroups.php b/libraries/classes/Server/UserGroups.php index 72b47bc949..06f826ab46 100644 --- a/libraries/classes/Server/UserGroups.php +++ b/libraries/classes/Server/UserGroups.php @@ -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; diff --git a/libraries/classes/Url.php b/libraries/classes/Url.php index 3d4091b01f..3e564eaa08 100644 --- a/libraries/classes/Url.php +++ b/libraries/classes/Url.php @@ -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 $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 $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 $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 * diff --git a/libraries/classes/Util.php b/libraries/classes/Util.php index 48c496aec0..cd3ea0c969 100644 --- a/libraries/classes/Util.php +++ b/libraries/classes/Util.php @@ -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); } /** diff --git a/libraries/common.inc.php b/libraries/common.inc.php index e3d341558c..38aa00a6d3 100644 --- a/libraries/common.inc.php +++ b/libraries/common.inc.php @@ -161,6 +161,8 @@ if (! defined('PMA_NO_SESSION')) { Session::setUp($PMA_Config, $error_handler); } +Core::populateRequestWithEncryptedQueryParams(); + /** * init some variables LABEL_variables_init */ diff --git a/libraries/config.default.php b/libraries/config.default.php index 412530573b..5fca8f1e63 100644 --- a/libraries/config.default.php +++ b/libraries/config.default.php @@ -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 * diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index eda9a2a85f..4f47d81dfc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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\\, array\\ 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\\\\>\\> but returns array\\\\>\\>\\.$#" + count: 1 + path: test/classes/CoreTest.php + + - + message: "#^Call to method PHPUnit\\\\Framework\\\\Assert\\:\\:assertArrayHasKey\\(\\) with 'URLQueryEncryptionS…' and array\\ 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\\ 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 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 650080eeee..3dba41f892 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + (int) $this->id @@ -593,6 +593,14 @@ $_POST['field_extra'][$i] ?? false + + + $decrypted + + + string|null + + $tn_pageNow @@ -812,6 +820,14 @@ $col_visib $sortExpressionNoDirection + + $editCopyUrlParams + $editCopyUrlParams + $editCopyUrlParams + $editCopyUrlParams + $editCopyUrlParams + $editCopyUrlParams + $col_visib_current $col_visib_current @@ -1312,6 +1328,10 @@ addChild + + $url['path'] + $url['query'] + (string) $child->name (string) $child->name @@ -2752,6 +2772,9 @@ + + $params + (string) $db (string) $table diff --git a/templates/database/events/index.twig b/templates/database/events/index.twig index 9e3b64c196..eaa65801e0 100644 --- a/templates/database/events/index.twig +++ b/templates/database/events/index.twig @@ -61,11 +61,12 @@ {% 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'} ) }} diff --git a/templates/database/events/row.twig b/templates/database/events/row.twig index 31388613e1..e9aa9855ec 100644 --- a/templates/database/events/row.twig +++ b/templates/database/events/row.twig @@ -36,12 +36,13 @@ {% 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'} ) }} diff --git a/templates/database/operations/index.twig b/templates/database/operations/index.twig index 510936fede..dd88146a83 100644 --- a/templates/database/operations/index.twig +++ b/templates/database/operations/index.twig @@ -90,7 +90,8 @@

{{ 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', diff --git a/templates/database/routines/row.twig b/templates/database/routines/row.twig index 62765d21ef..957ea022b5 100644 --- a/templates/database/routines/row.twig +++ b/templates/database/routines/row.twig @@ -63,12 +63,13 @@ {{ 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'} ) }} diff --git a/templates/database/tracking/tables.twig b/templates/database/tracking/tables.twig index 21c6d01c4a..bff99bb29a 100644 --- a/templates/database/tracking/tables.twig +++ b/templates/database/tracking/tables.twig @@ -89,7 +89,7 @@ 'back': url('/database/tracking'), 'table': version.table_name, 'delete_tracking': true - }, '') }}"> + }, '', false) }}"> {{ get_icon('b_drop', 'Delete tracking'|trans) }} @@ -100,7 +100,7 @@ 'goto': url('/table/tracking'), 'back': url('/database/tracking'), 'table': version.table_name - }, '') }}"> + }, '', false) }}"> {{ get_icon('b_versions', 'Versions'|trans) }} + }, '', false) }}"> {{ get_icon('b_report', 'Tracking report'|trans) }} + }, '', false) }}"> {{ get_icon('b_props', 'Structure snapshot'|trans) }} diff --git a/templates/database/triggers/row.twig b/templates/database/triggers/row.twig index 8da7826fc1..df1ae4a9fa 100644 --- a/templates/database/triggers/row.twig +++ b/templates/database/triggers/row.twig @@ -38,12 +38,13 @@ {% 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'} ) }} diff --git a/templates/display/results/checkbox_and_links.twig b/templates/display/results/checkbox_and_links.twig index e210fc21c9..9fb7bad6d0 100644 --- a/templates/display/results/checkbox_and_links.twig +++ b/templates/display/results/checkbox_and_links.twig @@ -10,7 +10,7 @@ {% if edit.url is not empty %} - {{ link_or_button(edit.url, edit.string) }} + {{ link_or_button(edit.url, edit.params, edit.string) }} {% if where_clause is not empty %} {% endif %} @@ -21,7 +21,7 @@ {% if copy.url is not empty %} - {{ link_or_button(copy.url, copy.string) }} + {{ link_or_button(copy.url, copy.params, copy.string) }} {% if where_clause is not empty %} {% endif %} @@ -32,7 +32,7 @@ {% if delete.url is not empty %} - {{ 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 %}
{{ js_conf }}
{% endif %} @@ -43,7 +43,7 @@ {% if delete.url is not empty %} - {{ 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 %}
{{ js_conf }}
{% endif %} @@ -54,7 +54,7 @@ {% if copy.url is not empty %} - {{ link_or_button(copy.url, copy.string) }} + {{ link_or_button(copy.url, copy.params, copy.string) }} {% if where_clause is not empty %} {% endif %} @@ -65,7 +65,7 @@ {% if edit.url is not empty %} - {{ link_or_button(edit.url, edit.string) }} + {{ link_or_button(edit.url, edit.params, edit.string) }} {% if where_clause is not empty %} {% endif %} diff --git a/templates/display/results/table.twig b/templates/display/results/table.twig index 283fb7930e..baa7655ac8 100644 --- a/templates/display/results/table.twig +++ b/templates/display/results/table.twig @@ -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 @@ {{ 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'} ) }} diff --git a/templates/indexes.twig b/templates/indexes.twig index 18ea3f61f0..cf95e9bbca 100644 --- a/templates/indexes.twig +++ b/templates/indexes.twig @@ -29,12 +29,12 @@ {% set columns_count = index.getColumnCount() %} - + {{ get_icon('b_edit', 'Edit'|trans) }} - + {{ get_icon('b_rename', 'Rename'|trans) }} @@ -53,7 +53,8 @@ {{ 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'} ) }} diff --git a/templates/navigation/item_unhide_dialog.twig b/templates/navigation/item_unhide_dialog.twig index d54d98b9e5..fdfb838240 100644 --- a/templates/navigation/item_unhide_dialog.twig +++ b/templates/navigation/item_unhide_dialog.twig @@ -17,7 +17,7 @@ 'itemType': type, 'itemName': item, 'dbName': database - }, '') }}">{{ get_icon('show', 'Unhide'|trans) }} + }, '', false) }}">{{ get_icon('show', 'Unhide'|trans) }} {% endfor %} diff --git a/templates/server/binlog/index.twig b/templates/server/binlog/index.twig index f6c27e2d41..2243f39ff8 100644 --- a/templates/server/binlog/index.twig +++ b/templates/server/binlog/index.twig @@ -43,12 +43,12 @@ {% if has_previous %} {% if has_icons %} - « {% else %} - + {% trans %}Previous{% context %}Previous page{% endtrans %} « {% endif %} @@ -56,11 +56,11 @@ {% endif %} {% if is_full_query %} - + {% trans 'Truncate shown queries' %} {% else %} - + {% trans 'Show full queries' %} {% endif %} @@ -68,12 +68,12 @@ {% if has_next %} - {% if has_icons %} - » {% else %} - + {% trans %}Next{% context %}Next page{% endtrans %} » {% endif %} diff --git a/templates/server/databases/index.twig b/templates/server/databases/index.twig index cc74b53fec..d2ac19448e 100644 --- a/templates/server/databases/index.twig +++ b/templates/server/databases/index.twig @@ -315,7 +315,7 @@
  • - + {% trans 'Enable statistics' %}
  • diff --git a/templates/server/replication/index.twig b/templates/server/replication/index.twig index d4a40bcbd7..879bb5418c 100644 --- a/templates/server/replication/index.twig +++ b/templates/server/replication/index.twig @@ -17,7 +17,7 @@
    {% trans 'Master replication' %}
    - {% apply format('', '')|raw %} + {% apply format('', '')|raw %} {% trans %} This server is not configured as master in a replication process. Would you like to %sconfigure%s it? {% endtrans %} diff --git a/templates/server/replication/master_replication.twig b/templates/server/replication/master_replication.twig index 32a89c2584..090d6c1639 100644 --- a/templates/server/replication/master_replication.twig +++ b/templates/server/replication/master_replication.twig @@ -40,7 +40,7 @@
  • - + {% trans 'Add slave replication user' %}
  • diff --git a/templates/server/replication/slave_configuration.twig b/templates/server/replication/slave_configuration.twig index f7a9234605..233a244529 100644 --- a/templates/server/replication/slave_configuration.twig +++ b/templates/server/replication/slave_configuration.twig @@ -103,7 +103,7 @@ {% apply format('', '')|raw %} + }), '', false) ~ '">', '')|raw %} {% trans 'This server is not configured as slave in a replication process. Would you like to %sconfigure%s it?' %} {% endapply %} {% endif %} diff --git a/templates/server/status/processes/list.twig b/templates/server/status/processes/list.twig index adb4fcc41a..862bba67a9 100644 --- a/templates/server/status/processes/list.twig +++ b/templates/server/status/processes/list.twig @@ -5,7 +5,7 @@ {% trans 'Processes' %} {% for column in columns %} - + {{ column.name }} {% if column.is_sorted %} 
@@ -15,7 +15,7 @@
               {% endif %}
             </a>
             {% if column.has_full_query %}
-              <a href= + {% if column.is_full %} {{ get_image( 's_partialtext', @@ -40,7 +40,7 @@ {% for row in rows %} - + {% trans 'Kill' %} diff --git a/templates/setup/home/index.twig b/templates/setup/home/index.twig index ad4232a539..7ee73c6b6f 100644 --- a/templates/setup/home/index.twig +++ b/templates/setup/home/index.twig @@ -58,7 +58,7 @@ | + {{- get_common({ token: server.params.token }, '', false) }}"> {% trans 'Delete' %} diff --git a/templates/sql/no_results_returned.twig b/templates/sql/no_results_returned.twig index 3ab07ef061..9fa06e115a 100644 --- a/templates/sql/no_results_returned.twig +++ b/templates/sql/no_results_returned.twig @@ -9,7 +9,8 @@ {% trans 'Query results operations' %} {{ 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'} ) }} diff --git a/templates/sql/relational_column_dropdown.twig b/templates/sql/relational_column_dropdown.twig index e89d2512e7..4c7b00ac84 100644 --- a/templates/sql/relational_column_dropdown.twig +++ b/templates/sql/relational_column_dropdown.twig @@ -1,4 +1,4 @@ {{ current_value }} - + {% trans 'Browse foreign values' %} diff --git a/templates/table/operations/index.twig b/templates/table/operations/index.twig index 725d2ac3ba..9a1356ecb6 100644 --- a/templates/table/operations/index.twig +++ b/templates/table/operations/index.twig @@ -313,7 +313,7 @@
      {% if storage_engine in ['MYISAM', 'ARIA', 'INNODB', 'BERKELEYDB', 'TOKUDB'] %}
    • - + {% trans 'Analyze table' %} {{ show_mysql_docu('ANALYZE_TABLE') }} @@ -322,7 +322,7 @@ {% if storage_engine in ['MYISAM', 'ARIA', 'INNODB', 'TOKUDB'] %}
    • - + {% trans 'Check table' %} {{ show_mysql_docu('CHECK_TABLE') }} @@ -330,7 +330,7 @@ {% endif %}
    • - + {% trans 'Checksum table' %} {{ show_mysql_docu('CHECKSUM_TABLE') }} @@ -338,7 +338,7 @@ {% if storage_engine == 'INNODB' %}
    • - + {% trans 'Defragment table' %} {{ 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)' %} {{ show_mysql_docu('FLUSH') }} @@ -358,7 +358,7 @@ {% if storage_engine in ['MYISAM', 'ARIA', 'INNODB', 'BERKELEYDB', 'TOKUDB'] %}
    • - + {% trans 'Optimize table' %} {{ show_mysql_docu('OPTIMIZE_TABLE') }} @@ -367,7 +367,7 @@ {% if storage_engine in ['MYISAM', 'ARIA'] %}
    • - + {% trans 'Repair table' %} {{ show_mysql_docu('REPAIR_TABLE') }} @@ -383,12 +383,13 @@ {% if not is_view %}
    • {{ 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 %}
    • {{ 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', diff --git a/templates/table/operations/view.twig b/templates/table/operations/view.twig index 917fc86472..1cc5df00b8 100644 --- a/templates/table/operations/view.twig +++ b/templates/table/operations/view.twig @@ -23,14 +23,15 @@
      {{ 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', diff --git a/templates/table/relation/foreign_key_row.twig b/templates/table/relation/foreign_key_row.twig index c73004cddc..a011255f5a 100644 --- a/templates/table/relation/foreign_key_row.twig +++ b/templates/table/relation/foreign_key_row.twig @@ -22,9 +22,8 @@ {% if one_key['constraint'] is defined %} - {% 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 %} diff --git a/templates/table/search/input_box.twig b/templates/table/search/input_box.twig index e5ac9624d2..7915cc77ae 100644 --- a/templates/table/search/input_box.twig +++ b/templates/table/search/input_box.twig @@ -21,7 +21,7 @@ value="{{ criteria_values[column_index] }}" {% endif %}> {{ 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) %} - {{ link_or_button(url('/gis-data-editor'), edit_str, [], '_blank') }} + {{ link_or_button(url('/gis-data-editor'), [], edit_str, [], '_blank') }} {% endif %} {% elseif column_type starts with 'enum' diff --git a/templates/table/structure/display_partitions.twig b/templates/table/structure/display_partitions.twig index 2f9a6683a1..826c363262 100644 --- a/templates/table/structure/display_partitions.twig +++ b/templates/table/structure/display_partitions.twig @@ -80,7 +80,7 @@ 'db': db, 'table': table, 'partition_name': partition.getName(), - }) }}"> + }, '', false) }}"> {{ get_icon('b_search', 'Analyze'|trans) }} @@ -90,7 +90,7 @@ 'db': db, 'table': table, 'partition_name': partition.getName(), - }) }}"> + }, '', false) }}"> {{ get_icon('eye', 'Check'|trans) }} @@ -100,7 +100,7 @@ 'db': db, 'table': table, 'partition_name': partition.getName(), - }) }}"> + }, '', false) }}"> {{ get_icon('normalize', 'Optimize'|trans) }} @@ -110,7 +110,7 @@ 'db': db, 'table': table, 'partition_name': partition.getName(), - }) }}"> + }, '', false) }}"> {{ get_icon('s_tbl', 'Rebuild'|trans) }} @@ -120,7 +120,7 @@ 'db': db, 'table': table, 'partition_name': partition.getName(), - }) }}"> + }, '', false) }}"> {{ get_icon('b_tblops', 'Repair'|trans) }} @@ -130,7 +130,7 @@ 'db': db, 'table': table, 'partition_name': partition.getName(), - }) }}"> + }, '', false) }}"> {{ get_icon('b_empty', 'Truncate'|trans) }} @@ -141,7 +141,7 @@ 'db': db, 'table': table, 'partition_name': partition.getName(), - }) }}"> + }, '', false) }}"> {{ get_icon('b_drop', 'Drop'|trans) }} @@ -194,11 +194,12 @@ {% 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' diff --git a/templates/table/structure/display_structure.twig b/templates/table/structure/display_structure.twig index 8d089f75e3..492ec788f5 100644 --- a/templates/table/structure/display_structure.twig +++ b/templates/table/structure/display_structure.twig @@ -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) }} @@ -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) }} {% 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) }} {% 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) }} {% 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) }} {% 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) }} {% 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) }}
    • @@ -326,7 +326,8 @@