diff options
Diffstat (limited to 'srcs/phpmyadmin/libraries/classes/Sql.php')
| -rw-r--r-- | srcs/phpmyadmin/libraries/classes/Sql.php | 2328 |
1 files changed, 2328 insertions, 0 deletions
diff --git a/srcs/phpmyadmin/libraries/classes/Sql.php b/srcs/phpmyadmin/libraries/classes/Sql.php new file mode 100644 index 0000000..fb4b07f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Sql.php @@ -0,0 +1,2328 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Set of functions for the SQL executor + * + * @package PhpMyAdmin + */ +declare(strict_types=1); + +namespace PhpMyAdmin; + +use PhpMyAdmin\Bookmark; +use PhpMyAdmin\Core; +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Display\Results as DisplayResults; +use PhpMyAdmin\Index; +use PhpMyAdmin\Message; +use PhpMyAdmin\Operations; +use PhpMyAdmin\ParseAnalyze; +use PhpMyAdmin\Relation; +use PhpMyAdmin\RelationCleanup; +use PhpMyAdmin\Response; +use PhpMyAdmin\SqlParser\Statements\AlterStatement; +use PhpMyAdmin\SqlParser\Statements\DropStatement; +use PhpMyAdmin\SqlParser\Statements\SelectStatement; +use PhpMyAdmin\SqlParser\Utils\Query; +use PhpMyAdmin\Table; +use PhpMyAdmin\Transformations; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * Set of functions for the SQL executor + * + * @package PhpMyAdmin + */ +class Sql +{ + /** + * @var Relation + */ + private $relation; + + /** + * @var RelationCleanup + */ + private $relationCleanup; + + /** + * @var Transformations + */ + private $transformations; + + /** + * @var Operations + */ + private $operations; + + /** + * @var Template + */ + private $template; + + /** + * Constructor + */ + public function __construct() + { + $this->relation = new Relation($GLOBALS['dbi']); + $this->relationCleanup = new RelationCleanup($GLOBALS['dbi'], $this->relation); + $this->operations = new Operations($GLOBALS['dbi'], $this->relation); + $this->transformations = new Transformations(); + $this->template = new Template(); + } + + /** + * Parses and analyzes the given SQL query. + * + * @param string $sql_query SQL query + * @param string $db DB name + * + * @return mixed + */ + public function parseAndAnalyze($sql_query, $db = null) + { + if ($db === null && isset($GLOBALS['db']) && strlen($GLOBALS['db'])) { + $db = $GLOBALS['db']; + } + list($analyzed_sql_results,,) = ParseAnalyze::sqlQuery($sql_query, $db); + return $analyzed_sql_results; + } + + /** + * Handle remembered sorting order, only for single table query + * + * @param string $db database name + * @param string $table table name + * @param array $analyzed_sql_results the analyzed query results + * @param string $full_sql_query SQL query + * + * @return void + */ + private function handleSortOrder( + $db, + $table, + array &$analyzed_sql_results, + &$full_sql_query + ) { + $pmatable = new Table($table, $db); + + if (empty($analyzed_sql_results['order'])) { + // Retrieving the name of the column we should sort after. + $sortCol = $pmatable->getUiProp(Table::PROP_SORTED_COLUMN); + if (empty($sortCol)) { + return; + } + + // Remove the name of the table from the retrieved field name. + $sortCol = str_replace( + Util::backquote($table) . '.', + '', + $sortCol + ); + + // Create the new query. + $full_sql_query = Query::replaceClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'ORDER BY ' . $sortCol + ); + + // TODO: Avoid reparsing the query. + $analyzed_sql_results = Query::getAll($full_sql_query); + } else { + // Store the remembered table into session. + $pmatable->setUiProp( + Table::PROP_SORTED_COLUMN, + Query::getClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'ORDER BY' + ) + ); + } + } + + /** + * Append limit clause to SQL query + * + * @param array $analyzed_sql_results the analyzed query results + * + * @return string limit clause appended SQL query + */ + private function getSqlWithLimitClause(array &$analyzed_sql_results) + { + return Query::replaceClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'LIMIT ' . $_SESSION['tmpval']['pos'] . ', ' + . $_SESSION['tmpval']['max_rows'] + ); + } + + /** + * Verify whether the result set has columns from just one table + * + * @param array $fields_meta meta fields + * + * @return boolean whether the result set has columns from just one table + */ + private function resultSetHasJustOneTable(array $fields_meta) + { + $just_one_table = true; + $prev_table = ''; + foreach ($fields_meta as $one_field_meta) { + if ($one_field_meta->table != '' + && $prev_table != '' + && $one_field_meta->table != $prev_table + ) { + $just_one_table = false; + } + if ($one_field_meta->table != '') { + $prev_table = $one_field_meta->table; + } + } + return $just_one_table && $prev_table != ''; + } + + /** + * Verify whether the result set contains all the columns + * of at least one unique key + * + * @param string $db database name + * @param string $table table name + * @param array $fields_meta meta fields + * + * @return boolean whether the result set contains a unique key + */ + private function resultSetContainsUniqueKey($db, $table, array $fields_meta) + { + $columns = $GLOBALS['dbi']->getColumns($db, $table); + $resultSetColumnNames = []; + foreach ($fields_meta as $oneMeta) { + $resultSetColumnNames[] = $oneMeta->name; + } + foreach (Index::getFromTable($table, $db) as $index) { + if ($index->isUnique()) { + $indexColumns = $index->getColumns(); + $numberFound = 0; + foreach ($indexColumns as $indexColumnName => $dummy) { + if (in_array($indexColumnName, $resultSetColumnNames)) { + $numberFound++; + } elseif (! in_array($indexColumnName, $columns)) { + $numberFound++; + } elseif (strpos($columns[$indexColumnName]['Extra'], 'INVISIBLE') !== false) { + $numberFound++; + } + } + if ($numberFound == count($indexColumns)) { + return true; + } + } + } + return false; + } + + /** + * Get the HTML for relational column dropdown + * During grid edit, if we have a relational field, returns the html for the + * dropdown + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $curr_value current selected value + * + * @return string html for the dropdown + */ + private function getHtmlForRelationalColumnDropdown($db, $table, $column, $curr_value) + { + $foreigners = $this->relation->getForeigners($db, $table, $column); + + $foreignData = $this->relation->getForeignData( + $foreigners, + $column, + false, + '', + '' + ); + + if ($foreignData['disp_row'] == null) { + //Handle the case when number of values + //is more than $cfg['ForeignKeyMaxLimit'] + $_url_params = [ + 'db' => $db, + 'table' => $table, + 'field' => $column, + ]; + + $dropdown = $this->template->render('sql/relational_column_dropdown', [ + 'current_value' => $_POST['curr_value'], + 'params' => $_url_params, + ]); + } else { + $dropdown = $this->relation->foreignDropdown( + $foreignData['disp_row'], + $foreignData['foreign_field'], + $foreignData['foreign_display'], + $curr_value, + $GLOBALS['cfg']['ForeignKeyMaxLimit'] + ); + $dropdown = '<select>' . $dropdown . '</select>'; + } + + return $dropdown; + } + + /** + * Get the HTML for the profiling table and accompanying chart if profiling is set. + * Otherwise returns null + * + * @param string|null $urlQuery url query + * @param string $database current database + * @param array $profilingResults array containing the profiling info + * + * @return string html for the profiling table and chart + */ + private function getHtmlForProfilingChart($urlQuery, $database, $profilingResults): string + { + if (! empty($profilingResults)) { + $urlQuery = isset($urlQuery) ? $urlQuery : Url::getCommon(['db' => $database]); + + list( + $detailedTable, + $chartJson, + $profilingStats + ) = $this->analyzeAndGetTableHtmlForProfilingResults($profilingResults); + + return $this->template->render('sql/profiling_chart', [ + 'url_query' => $urlQuery, + 'detailed_table' => $detailedTable, + 'states' => $profilingStats['states'], + 'total_time' => $profilingStats['total_time'], + 'chart_json' => $chartJson, + ]); + } + return ''; + } + + /** + * Function to get HTML for detailed profiling results table, profiling stats, and + * $chart_json for displaying the chart. + * + * @param array $profiling_results profiling results + * + * @return mixed + */ + private function analyzeAndGetTableHtmlForProfilingResults( + $profiling_results + ) { + $profiling_stats = [ + 'total_time' => 0, + 'states' => [], + ]; + $chart_json = []; + $i = 1; + $table = ''; + foreach ($profiling_results as $one_result) { + if (! isset($profiling_stats['states'][ucwords($one_result['Status'])])) { + $profiling_stats['states'][ucwords($one_result['Status'])] = [ + 'total_time' => $one_result['Duration'], + 'calls' => 1, + ]; + } + $profiling_stats['total_time'] += $one_result['Duration']; + + $table .= $this->template->render('sql/detailed_table', [ + 'index' => $i++, + 'status' => $one_result['Status'], + 'duration' => $one_result['Duration'], + ]); + + if (isset($chart_json[ucwords($one_result['Status'])])) { + $chart_json[ucwords($one_result['Status'])] + += $one_result['Duration']; + } else { + $chart_json[ucwords($one_result['Status'])] + = $one_result['Duration']; + } + } + return [ + $table, + $chart_json, + $profiling_stats, + ]; + } + + /** + * Get the HTML for the enum column dropdown + * During grid edit, if we have a enum field, returns the html for the + * dropdown + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $curr_value currently selected value + * + * @return string html for the dropdown + */ + private function getHtmlForEnumColumnDropdown($db, $table, $column, $curr_value) + { + $values = $this->getValuesForColumn($db, $table, $column); + return $this->template->render('sql/enum_column_dropdown', [ + 'values' => $values, + 'selected_values' => [$curr_value], + ]); + } + + /** + * Get value of a column for a specific row (marked by $where_clause) + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $where_clause where clause to select a particular row + * + * @return string with value + */ + private function getFullValuesForSetColumn($db, $table, $column, $where_clause) + { + $result = $GLOBALS['dbi']->fetchSingleRow( + "SELECT `$column` FROM `$db`.`$table` WHERE $where_clause" + ); + + return $result[$column]; + } + + /** + * Get the HTML for the set column dropdown + * During grid edit, if we have a set field, returns the html for the + * dropdown + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * @param string $curr_value currently selected value + * + * @return string html for the set column + */ + private function getHtmlForSetColumn($db, $table, $column, $curr_value): string + { + $values = $this->getValuesForColumn($db, $table, $column); + + $full_values = isset($_POST['get_full_values']) ? $_POST['get_full_values'] : false; + $where_clause = isset($_POST['where_clause']) ? $_POST['where_clause'] : null; + + // If the $curr_value was truncated, we should + // fetch the correct full values from the table + if ($full_values && ! empty($where_clause)) { + $curr_value = $this->getFullValuesForSetColumn( + $db, + $table, + $column, + $where_clause + ); + } + + //converts characters of $curr_value to HTML entities + $converted_curr_value = htmlentities( + $curr_value, + ENT_COMPAT, + "UTF-8" + ); + + $selected_values = explode(',', $converted_curr_value); + $select_size = (count($values) > 10) ? 10 : count($values); + + return $this->template->render('sql/set_column', [ + 'size' => $select_size, + 'values' => $values, + 'selected_values' => $selected_values, + ]); + } + + /** + * Get all the values for a enum column or set column in a table + * + * @param string $db current database + * @param string $table current table + * @param string $column current column + * + * @return array array containing the value list for the column + */ + private function getValuesForColumn($db, $table, $column) + { + $field_info_query = $GLOBALS['dbi']->getColumnsSql($db, $table, $column); + + $field_info_result = $GLOBALS['dbi']->fetchResult( + $field_info_query, + null, + null, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + return Util::parseEnumSetValues($field_info_result[0]['Type']); + } + + /** + * Function to get html for bookmark support if bookmarks are enabled. Else will + * return null + * + * @param array $displayParts the parts to display + * @param array $cfgBookmark configuration setting for bookmarking + * @param string $sql_query sql query + * @param string $db current database + * @param string $table current table + * @param string|null $complete_query complete query + * @param string $bkm_user bookmarking user + * + * @return string + */ + public function getHtmlForBookmark( + array $displayParts, + array $cfgBookmark, + $sql_query, + $db, + $table, + ?string $complete_query, + $bkm_user + ): string { + if ($displayParts['bkm_form'] == '1' + && (! empty($cfgBookmark) && empty($_GET['id_bookmark'])) + && ! empty($sql_query) + ) { + return $this->template->render('sql/bookmark', [ + 'db' => $db, + 'goto' => 'sql.php' . Url::getCommon([ + 'db' => $db, + 'table' => $table, + 'sql_query' => $sql_query, + 'id_bookmark' => 1, + ]), + 'user' => $bkm_user, + 'sql_query' => isset($complete_query) ? $complete_query : $sql_query, + ]); + } + return ''; + } + + /** + * Function to check whether to remember the sorting order or not + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return boolean + */ + private function isRememberSortingOrder(array $analyzed_sql_results) + { + return $GLOBALS['cfg']['RememberSorting'] + && ! ($analyzed_sql_results['is_count'] + || $analyzed_sql_results['is_export'] + || $analyzed_sql_results['is_func'] + || $analyzed_sql_results['is_analyse']) + && $analyzed_sql_results['select_from'] + && isset($analyzed_sql_results['select_expr']) + && isset($analyzed_sql_results['select_tables']) + && (empty($analyzed_sql_results['select_expr']) + || ((count($analyzed_sql_results['select_expr']) === 1) + && ($analyzed_sql_results['select_expr'][0] == '*'))) + && count($analyzed_sql_results['select_tables']) === 1; + } + + /** + * Function to check whether the LIMIT clause should be appended or not + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return boolean + */ + private function isAppendLimitClause(array $analyzed_sql_results) + { + // Assigning LIMIT clause to an syntactically-wrong query + // is not needed. Also we would want to show the true query + // and the true error message to the query executor + + return (isset($analyzed_sql_results['parser']) + && count($analyzed_sql_results['parser']->errors) === 0) + && ($_SESSION['tmpval']['max_rows'] != 'all') + && ! ($analyzed_sql_results['is_export'] + || $analyzed_sql_results['is_analyse']) + && ($analyzed_sql_results['select_from'] + || $analyzed_sql_results['is_subquery']) + && empty($analyzed_sql_results['limit']); + } + + /** + * Function to check whether this query is for just browsing + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * @param boolean|null $find_real_end whether the real end should be found + * + * @return boolean + */ + public function isJustBrowsing(array $analyzed_sql_results, ?bool $find_real_end): bool + { + return ! $analyzed_sql_results['is_group'] + && ! $analyzed_sql_results['is_func'] + && empty($analyzed_sql_results['union']) + && empty($analyzed_sql_results['distinct']) + && $analyzed_sql_results['select_from'] + && (count($analyzed_sql_results['select_tables']) === 1) + && (empty($analyzed_sql_results['statement']->where) + || (count($analyzed_sql_results['statement']->where) === 1 + && $analyzed_sql_results['statement']->where[0]->expr === '1')) + && empty($analyzed_sql_results['group']) + && ! isset($find_real_end) + && ! $analyzed_sql_results['is_subquery'] + && ! $analyzed_sql_results['join'] + && empty($analyzed_sql_results['having']); + } + + /** + * Function to check whether the related transformation information should be deleted + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return boolean + */ + private function isDeleteTransformationInfo(array $analyzed_sql_results) + { + return ! empty($analyzed_sql_results['querytype']) + && (($analyzed_sql_results['querytype'] == 'ALTER') + || ($analyzed_sql_results['querytype'] == 'DROP')); + } + + /** + * Function to check whether the user has rights to drop the database + * + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * @param boolean $allowUserDropDatabase whether the user is allowed to drop db + * @param boolean $is_superuser whether this user is a superuser + * + * @return boolean + */ + public function hasNoRightsToDropDatabase( + array $analyzed_sql_results, + $allowUserDropDatabase, + $is_superuser + ) { + return ! $allowUserDropDatabase + && isset($analyzed_sql_results['drop_database']) + && $analyzed_sql_results['drop_database'] + && ! $is_superuser; + } + + /** + * Function to set a column property + * + * @param Table $pmatable Table instance + * @param string $request_index col_order|col_visib + * + * @return boolean + */ + private function setColumnProperty($pmatable, $request_index) + { + $property_value = array_map('intval', explode(',', $_POST[$request_index])); + switch ($request_index) { + case 'col_order': + $property_to_set = Table::PROP_COLUMN_ORDER; + break; + case 'col_visib': + $property_to_set = Table::PROP_COLUMN_VISIB; + break; + default: + $property_to_set = ''; + } + $retval = $pmatable->setUiProp( + $property_to_set, + $property_value, + isset($_POST['table_create_time']) ? $_POST['table_create_time'] : null + ); + if (gettype($retval) != 'boolean') { + $response = Response::getInstance(); + $response->setRequestStatus(false); + $response->addJSON('message', $retval->getString()); + exit; + } + + return $retval; + } + + /** + * Function to check the request for setting the column order or visibility + * + * @param string $table the current table + * @param string $db the current database + * + * @return void + */ + public function setColumnOrderOrVisibility($table, $db) + { + $pmatable = new Table($table, $db); + $retval = false; + + // set column order + if (isset($_POST['col_order'])) { + $retval = $this->setColumnProperty($pmatable, 'col_order'); + } + + // set column visibility + if ($retval === true && isset($_POST['col_visib'])) { + $retval = $this->setColumnProperty($pmatable, 'col_visib'); + } + + $response = Response::getInstance(); + $response->setRequestStatus($retval === true); + exit; + } + + /** + * Function to add a bookmark + * + * @param string $goto goto page URL + * + * @return void + */ + public function addBookmark($goto) + { + $bookmark = Bookmark::createBookmark( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $_POST['bkm_fields'], + (isset($_POST['bkm_all_users']) + && $_POST['bkm_all_users'] == 'true' ? true : false + ) + ); + $result = $bookmark->save(); + $response = Response::getInstance(); + if ($response->isAjax()) { + if ($result) { + $msg = Message::success(__('Bookmark %s has been created.')); + $msg->addParam($_POST['bkm_fields']['bkm_label']); + $response->addJSON('message', $msg); + } else { + $msg = Message::error(__('Bookmark not created!')); + $response->setRequestStatus(false); + $response->addJSON('message', $msg); + } + exit; + } else { + // go back to sql.php to redisplay query; do not use & in this case: + /** + * @todo In which scenario does this happen? + */ + Core::sendHeaderLocation( + './' . $goto + . '&label=' . $_POST['bkm_fields']['bkm_label'] + ); + } + } + + /** + * Function to find the real end of rows + * + * @param string $db the current database + * @param string $table the current table + * + * @return mixed the number of rows if "retain" param is true, otherwise true + */ + public function findRealEndOfRows($db, $table) + { + $unlim_num_rows = $GLOBALS['dbi']->getTable($db, $table)->countRecords(true); + $_SESSION['tmpval']['pos'] = $this->getStartPosToDisplayRow($unlim_num_rows); + + return $unlim_num_rows; + } + + /** + * Function to get values for the relational columns + * + * @param string $db the current database + * @param string $table the current table + * + * @return void + */ + public function getRelationalValues($db, $table) + { + $column = $_POST['column']; + if ($_SESSION['tmpval']['relational_display'] == 'D' + && isset($_POST['relation_key_or_display_column']) + && $_POST['relation_key_or_display_column'] + ) { + $curr_value = $_POST['relation_key_or_display_column']; + } else { + $curr_value = $_POST['curr_value']; + } + $dropdown = $this->getHtmlForRelationalColumnDropdown( + $db, + $table, + $column, + $curr_value + ); + $response = Response::getInstance(); + $response->addJSON('dropdown', $dropdown); + exit; + } + + /** + * Function to get values for Enum or Set Columns + * + * @param string $db the current database + * @param string $table the current table + * @param string $columnType whether enum or set + * + * @return void + */ + public function getEnumOrSetValues($db, $table, $columnType) + { + $column = $_POST['column']; + $curr_value = $_POST['curr_value']; + $response = Response::getInstance(); + if ($columnType == "enum") { + $dropdown = $this->getHtmlForEnumColumnDropdown( + $db, + $table, + $column, + $curr_value + ); + $response->addJSON('dropdown', $dropdown); + } else { + $select = $this->getHtmlForSetColumn( + $db, + $table, + $column, + $curr_value + ); + $response->addJSON('select', $select); + } + exit; + } + + /** + * Function to get the default sql query for browsing page + * + * @param string $db the current database + * @param string $table the current table + * + * @return string the default $sql_query for browse page + */ + public function getDefaultSqlQueryForBrowse($db, $table) + { + $bookmark = Bookmark::get( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $db, + $table, + 'label', + false, + true + ); + + if (! empty($bookmark) && ! empty($bookmark->getQuery())) { + $GLOBALS['using_bookmark_message'] = Message::notice( + __('Using bookmark "%s" as default browse query.') + ); + $GLOBALS['using_bookmark_message']->addParam($table); + $GLOBALS['using_bookmark_message']->addHtml( + Util::showDocu('faq', 'faq6-22') + ); + $sql_query = $bookmark->getQuery(); + } else { + $defaultOrderByClause = ''; + + if (isset($GLOBALS['cfg']['TablePrimaryKeyOrder']) + && ($GLOBALS['cfg']['TablePrimaryKeyOrder'] !== 'NONE') + ) { + $primaryKey = null; + $primary = Index::getPrimary($table, $db); + + if ($primary !== false) { + $primarycols = $primary->getColumns(); + + foreach ($primarycols as $col) { + $primaryKey = $col->getName(); + break; + } + + if ($primaryKey != null) { + $defaultOrderByClause = ' ORDER BY ' + . Util::backquote($table) . '.' + . Util::backquote($primaryKey) . ' ' + . $GLOBALS['cfg']['TablePrimaryKeyOrder']; + } + } + } + + $sql_query = 'SELECT * FROM ' . Util::backquote($table) + . $defaultOrderByClause; + } + + return $sql_query; + } + + /** + * Responds an error when an error happens when executing the query + * + * @param boolean $is_gotofile whether goto file or not + * @param string $error error after executing the query + * @param string $full_sql_query full sql query + * + * @return void + */ + private function handleQueryExecuteError($is_gotofile, $error, $full_sql_query) + { + if ($is_gotofile) { + $message = Message::rawError($error); + $response = Response::getInstance(); + $response->setRequestStatus(false); + $response->addJSON('message', $message); + } else { + Util::mysqlDie($error, $full_sql_query, '', ''); + } + exit; + } + + /** + * Function to store the query as a bookmark + * + * @param string $db the current database + * @param string $bkm_user the bookmarking user + * @param string $sql_query_for_bookmark the query to be stored in bookmark + * @param string $bkm_label bookmark label + * @param boolean|null $bkm_replace whether to replace existing bookmarks + * + * @return void + */ + public function storeTheQueryAsBookmark( + $db, + $bkm_user, + $sql_query_for_bookmark, + $bkm_label, + ?bool $bkm_replace + ) { + $bfields = [ + 'bkm_database' => $db, + 'bkm_user' => $bkm_user, + 'bkm_sql_query' => $sql_query_for_bookmark, + 'bkm_label' => $bkm_label, + ]; + + // Should we replace bookmark? + if (isset($bkm_replace)) { + $bookmarks = Bookmark::getList( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $db + ); + foreach ($bookmarks as $bookmark) { + if ($bookmark->getLabel() == $bkm_label) { + $bookmark->delete(); + } + } + } + + $bookmark = Bookmark::createBookmark( + $GLOBALS['dbi'], + $GLOBALS['cfg']['Server']['user'], + $bfields, + isset($_POST['bkm_all_users']) + ); + $bookmark->save(); + } + + /** + * Executes the SQL query and measures its execution time + * + * @param string $full_sql_query the full sql query + * + * @return array ($result, $querytime) + */ + private function executeQueryAndMeasureTime($full_sql_query) + { + // close session in case the query takes too long + session_write_close(); + + // Measure query time. + $querytime_before = array_sum(explode(' ', microtime())); + + $result = @$GLOBALS['dbi']->tryQuery( + $full_sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $querytime_after = array_sum(explode(' ', microtime())); + + // reopen session + session_start(); + + return [ + $result, + $querytime_after - $querytime_before, + ]; + } + + /** + * Function to get the affected or changed number of rows after executing a query + * + * @param boolean $is_affected whether the query affected a table + * @param mixed $result results of executing the query + * + * @return int number of rows affected or changed + */ + private function getNumberOfRowsAffectedOrChanged($is_affected, $result) + { + if (! $is_affected) { + $num_rows = $result ? @$GLOBALS['dbi']->numRows($result) : 0; + } else { + $num_rows = @$GLOBALS['dbi']->affectedRows(); + } + + return $num_rows; + } + + /** + * Checks if the current database has changed + * This could happen if the user sends a query like "USE `database`;" + * + * @param string $db the database in the query + * + * @return bool whether to reload the navigation(1) or not(0) + */ + private function hasCurrentDbChanged($db): bool + { + if (strlen($db) > 0) { + $current_db = $GLOBALS['dbi']->fetchValue('SELECT DATABASE()'); + // $current_db is false, except when a USE statement was sent + return ($current_db != false) && ($db !== $current_db); + } + + return false; + } + + /** + * If a table, database or column gets dropped, clean comments. + * + * @param string $db current database + * @param string $table current table + * @param string|null $column current column + * @param bool $purge whether purge set or not + * + * @return void + */ + private function cleanupRelations($db, $table, ?string $column, $purge) + { + if (! empty($purge) && strlen($db) > 0) { + if (strlen($table) > 0) { + if (isset($column) && strlen($column) > 0) { + $this->relationCleanup->column($db, $table, $column); + } else { + $this->relationCleanup->table($db, $table); + } + } else { + $this->relationCleanup->database($db); + } + } + } + + /** + * Function to count the total number of rows for the same 'SELECT' query without + * the 'LIMIT' clause that may have been programatically added + * + * @param int $num_rows number of rows affected/changed by the query + * @param bool $justBrowsing whether just browsing or not + * @param string $db the current database + * @param string $table the current table + * @param array $analyzed_sql_results the analyzed query and other variables set + * after analyzing the query + * + * @return int unlimited number of rows + */ + private function countQueryResults( + $num_rows, + $justBrowsing, + $db, + $table, + array $analyzed_sql_results + ) { + + /* Shortcut for not analyzed/empty query */ + if (empty($analyzed_sql_results)) { + return 0; + } + + if (! $this->isAppendLimitClause($analyzed_sql_results)) { + // if we did not append a limit, set this to get a correct + // "Showing rows..." message + // $_SESSION['tmpval']['max_rows'] = 'all'; + $unlim_num_rows = $num_rows; + } elseif ($this->isAppendLimitClause($analyzed_sql_results) && $_SESSION['tmpval']['max_rows'] > $num_rows) { + // When user has not defined a limit in query and total rows in + // result are less than max_rows to display, there is no need + // to count total rows for that query again + $unlim_num_rows = $_SESSION['tmpval']['pos'] + $num_rows; + } elseif ($analyzed_sql_results['querytype'] == 'SELECT' + || $analyzed_sql_results['is_subquery'] + ) { + // c o u n t q u e r y + + // If we are "just browsing", there is only one table (and no join), + // and no WHERE clause (or just 'WHERE 1 '), + // we do a quick count (which uses MaxExactCount) because + // SQL_CALC_FOUND_ROWS is not quick on large InnoDB tables + + // However, do not count again if we did it previously + // due to $find_real_end == true + if ($justBrowsing) { + // Get row count (is approximate for InnoDB) + $unlim_num_rows = $GLOBALS['dbi']->getTable($db, $table)->countRecords(); + /** + * @todo Can we know at this point that this is InnoDB, + * (in this case there would be no need for getting + * an exact count)? + */ + if ($unlim_num_rows < $GLOBALS['cfg']['MaxExactCount']) { + // Get the exact count if approximate count + // is less than MaxExactCount + /** + * @todo In countRecords(), MaxExactCount is also verified, + * so can we avoid checking it twice? + */ + $unlim_num_rows = $GLOBALS['dbi']->getTable($db, $table) + ->countRecords(true); + } + } else { + // The SQL_CALC_FOUND_ROWS option of the SELECT statement is used. + + // For UNION statements, only a SQL_CALC_FOUND_ROWS is required + // after the first SELECT. + + $count_query = Query::replaceClause( + $analyzed_sql_results['statement'], + $analyzed_sql_results['parser']->list, + 'SELECT SQL_CALC_FOUND_ROWS', + null, + true + ); + + // Another LIMIT clause is added to avoid long delays. + // A complete result will be returned anyway, but the LIMIT would + // stop the query as soon as the result that is required has been + // computed. + + if (empty($analyzed_sql_results['union'])) { + $count_query .= ' LIMIT 1'; + } + + // Running the count query. + $GLOBALS['dbi']->tryQuery($count_query); + + $unlim_num_rows = $GLOBALS['dbi']->fetchValue('SELECT FOUND_ROWS()'); + } // end else "just browsing" + } else {// not $is_select + $unlim_num_rows = 0; + } + + return $unlim_num_rows; + } + + /** + * Function to handle all aspects relating to executing the query + * + * @param array $analyzed_sql_results analyzed sql results + * @param string $full_sql_query full sql query + * @param boolean $is_gotofile whether to go to a file + * @param string $db current database + * @param string $table current table + * @param boolean|null $find_real_end whether to find the real end + * @param string $sql_query_for_bookmark sql query to be stored as bookmark + * @param array $extra_data extra data + * + * @return mixed + */ + private function executeTheQuery( + array $analyzed_sql_results, + $full_sql_query, + $is_gotofile, + $db, + $table, + ?bool $find_real_end, + $sql_query_for_bookmark, + $extra_data + ) { + $response = Response::getInstance(); + $response->getHeader()->getMenu()->setTable($table); + + // Only if we ask to see the php code + if (isset($GLOBALS['show_as_php'])) { + $result = null; + $num_rows = 0; + $unlim_num_rows = 0; + } else { // If we don't ask to see the php code + if (isset($_SESSION['profiling']) + && Util::profilingSupported() + ) { + $GLOBALS['dbi']->query('SET PROFILING=1;'); + } + + list( + $result, + $GLOBALS['querytime'] + ) = $this->executeQueryAndMeasureTime($full_sql_query); + + // Displays an error message if required and stop parsing the script + $error = $GLOBALS['dbi']->getError(); + if ($error && $GLOBALS['cfg']['IgnoreMultiSubmitErrors']) { + $extra_data['error'] = $error; + } elseif ($error) { + $this->handleQueryExecuteError($is_gotofile, $error, $full_sql_query); + } + + // If there are no errors and bookmarklabel was given, + // store the query as a bookmark + if (! empty($_POST['bkm_label']) && ! empty($sql_query_for_bookmark)) { + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + $this->storeTheQueryAsBookmark( + $db, + $cfgBookmark['user'], + $sql_query_for_bookmark, + $_POST['bkm_label'], + isset($_POST['bkm_replace']) ? $_POST['bkm_replace'] : null + ); + } // end store bookmarks + + // Gets the number of rows affected/returned + // (This must be done immediately after the query because + // mysql_affected_rows() reports about the last query done) + $num_rows = $this->getNumberOfRowsAffectedOrChanged( + $analyzed_sql_results['is_affected'], + $result + ); + + // Grabs the profiling results + if (isset($_SESSION['profiling']) + && Util::profilingSupported() + ) { + $profiling_results = $GLOBALS['dbi']->fetchResult('SHOW PROFILE;'); + } + + $justBrowsing = $this->isJustBrowsing( + $analyzed_sql_results, + isset($find_real_end) ? $find_real_end : null + ); + + $unlim_num_rows = $this->countQueryResults( + $num_rows, + $justBrowsing, + $db, + $table, + $analyzed_sql_results + ); + + $this->cleanupRelations( + isset($db) ? $db : '', + isset($table) ? $table : '', + isset($_POST['dropped_column']) ? $_POST['dropped_column'] : null, + isset($_POST['purge']) ? $_POST['purge'] : null + ); + + if (isset($_POST['dropped_column']) + && strlen($db) > 0 + && strlen($table) > 0 + ) { + // to refresh the list of indexes (Ajax mode) + $extra_data['indexes_list'] = Index::getHtmlForIndexes( + $table, + $db + ); + } + } + + return [ + $result, + $num_rows, + $unlim_num_rows, + isset($profiling_results) ? $profiling_results : null, + $extra_data, + ]; + } + /** + * Delete related transformation information + * + * @param string $db current database + * @param string $table current table + * @param array $analyzed_sql_results analyzed sql results + * + * @return void + */ + private function deleteTransformationInfo($db, $table, array $analyzed_sql_results) + { + if (! isset($analyzed_sql_results['statement'])) { + return; + } + $statement = $analyzed_sql_results['statement']; + if ($statement instanceof AlterStatement) { + if (! empty($statement->altered[0]) + && $statement->altered[0]->options->has('DROP') + ) { + if (! empty($statement->altered[0]->field->column)) { + $this->transformations->clear( + $db, + $table, + $statement->altered[0]->field->column + ); + } + } + } elseif ($statement instanceof DropStatement) { + $this->transformations->clear($db, $table); + } + } + + /** + * Function to get the message for the no rows returned case + * + * @param string $message_to_show message to show + * @param array $analyzed_sql_results analyzed sql results + * @param int $num_rows number of rows + * + * @return Message + */ + private function getMessageForNoRowsReturned( + $message_to_show, + array $analyzed_sql_results, + $num_rows + ) { + if ($analyzed_sql_results['querytype'] == 'DELETE"') { + $message = Message::getMessageForDeletedRows($num_rows); + } elseif ($analyzed_sql_results['is_insert']) { + if ($analyzed_sql_results['querytype'] == 'REPLACE') { + // For REPLACE we get DELETED + INSERTED row count, + // so we have to call it affected + $message = Message::getMessageForAffectedRows($num_rows); + } else { + $message = Message::getMessageForInsertedRows($num_rows); + } + $insert_id = $GLOBALS['dbi']->insertId(); + if ($insert_id != 0) { + // insert_id is id of FIRST record inserted in one insert, + // so if we inserted multiple rows, we had to increment this + $message->addText('[br]'); + // need to use a temporary because the Message class + // currently supports adding parameters only to the first + // message + $_inserted = Message::notice(__('Inserted row id: %1$d')); + $_inserted->addParam($insert_id + $num_rows - 1); + $message->addMessage($_inserted); + } + } elseif ($analyzed_sql_results['is_affected']) { + $message = Message::getMessageForAffectedRows($num_rows); + + // Ok, here is an explanation for the !$is_select. + // The form generated by PhpMyAdmin\SqlQueryForm + // and db_sql.php has many submit buttons + // on the same form, and some confusion arises from the + // fact that $message_to_show is sent for every case. + // The $message_to_show containing a success message and sent with + // the form should not have priority over errors + } elseif (! empty($message_to_show) + && $analyzed_sql_results['querytype'] != 'SELECT' + ) { + $message = Message::rawSuccess(htmlspecialchars($message_to_show)); + } elseif (! empty($GLOBALS['show_as_php'])) { + $message = Message::success(__('Showing as PHP code')); + } elseif (isset($GLOBALS['show_as_php'])) { + /* User disable showing as PHP, query is only displayed */ + $message = Message::notice(__('Showing SQL query')); + } else { + $message = Message::success( + __('MySQL returned an empty result set (i.e. zero rows).') + ); + } + + if (isset($GLOBALS['querytime'])) { + $_querytime = Message::notice( + '(' . __('Query took %01.4f seconds.') . ')' + ); + $_querytime->addParam($GLOBALS['querytime']); + $message->addMessage($_querytime); + } + + // In case of ROLLBACK, notify the user. + if (isset($_POST['rollback_query'])) { + $message->addText(__('[ROLLBACK occurred.]')); + } + + return $message; + } + + /** + * Function to respond back when the query returns zero rows + * This method is called + * 1-> When browsing an empty table + * 2-> When executing a query on a non empty table which returns zero results + * 3-> When executing a query on an empty table + * 4-> When executing an INSERT, UPDATE, DELETE query from the SQL tab + * 5-> When deleting a row from BROWSE tab + * 6-> When searching using the SEARCH tab which returns zero results + * 7-> When changing the structure of the table except change operation + * + * @param array $analyzed_sql_results analyzed sql results + * @param string $db current database + * @param string $table current table + * @param string|null $message_to_show message to show + * @param int $num_rows number of rows + * @param DisplayResults $displayResultsObject DisplayResult instance + * @param array|null $extra_data extra data + * @param string $pmaThemeImage uri of the theme image + * @param array|null $profiling_results profiling results + * @param object $result executed query results + * @param string $sql_query sql query + * @param string|null $complete_query complete sql query + * + * @return string html + */ + private function getQueryResponseForNoResultsReturned( + array $analyzed_sql_results, + $db, + $table, + ?string $message_to_show, + $num_rows, + $displayResultsObject, + ?array $extra_data, + $pmaThemeImage, + ?array $profiling_results, + $result, + $sql_query, + ?string $complete_query + ) { + global $url_query; + if ($this->isDeleteTransformationInfo($analyzed_sql_results)) { + $this->deleteTransformationInfo($db, $table, $analyzed_sql_results); + } + + if (isset($extra_data['error'])) { + $message = Message::rawError($extra_data['error']); + } else { + $message = $this->getMessageForNoRowsReturned( + isset($message_to_show) ? $message_to_show : null, + $analyzed_sql_results, + $num_rows + ); + } + + $html_output = ''; + $html_message = Util::getMessage( + $message, + $GLOBALS['sql_query'], + 'success' + ); + $html_output .= $html_message; + if (! isset($GLOBALS['show_as_php'])) { + if (! empty($GLOBALS['reload'])) { + $extra_data['reload'] = 1; + $extra_data['db'] = $GLOBALS['db']; + } + + // For ajax requests add message and sql_query as JSON + if (empty($_REQUEST['ajax_page_request'])) { + $extra_data['message'] = $message; + if ($GLOBALS['cfg']['ShowSQL']) { + $extra_data['sql_query'] = $html_message; + } + } + + $response = Response::getInstance(); + $response->addJSON(isset($extra_data) ? $extra_data : []); + + if (! empty($analyzed_sql_results['is_select']) && + ! isset($extra_data['error'])) { + $url_query = isset($url_query) ? $url_query : null; + + $displayParts = [ + 'edit_lnk' => null, + 'del_lnk' => null, + 'sort_lnk' => '1', + 'nav_bar' => '0', + 'bkm_form' => '1', + 'text_btn' => '1', + 'pview_lnk' => '1', + ]; + + $html_output .= $this->getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + $displayParts, + false, + 0, + $num_rows, + true, + $result, + $analyzed_sql_results, + true + ); + + if (is_array($profiling_results)) { + $header = $response->getHeader(); + $scripts = $header->getScripts(); + $scripts->addFile('sql.js'); + $html_output .= $this->getHtmlForProfilingChart( + $url_query, + $db, + $profiling_results + ); + } + + $html_output .= $displayResultsObject->getCreateViewQueryResultOp( + $analyzed_sql_results + ); + + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + if ($cfgBookmark) { + $html_output .= $this->getHtmlForBookmark( + $displayParts, + $cfgBookmark, + $sql_query, + $db, + $table, + isset($complete_query) ? $complete_query : $sql_query, + $cfgBookmark['user'] + ); + } + } + } + + return $html_output; + } + + /** + * Function to send response for ajax grid edit + * + * @param object $result result of the executed query + * + * @return void + */ + private function sendResponseForGridEdit($result) + { + $row = $GLOBALS['dbi']->fetchRow($result); + $field_flags = $GLOBALS['dbi']->fieldFlags($result, 0); + if (false !== stripos($field_flags, DisplayResults::BINARY_FIELD)) { + $row[0] = bin2hex($row[0]); + } + $response = Response::getInstance(); + $response->addJSON('value', $row[0]); + exit; + } + + /** + * Returns a message for successful creation of a bookmark or null if a bookmark + * was not created + * + * @return string + */ + private function getBookmarkCreatedMessage(): string + { + $output = ''; + if (isset($_GET['label'])) { + $message = Message::success( + __('Bookmark %s has been created.') + ); + $message->addParam($_GET['label']); + $output = $message->getDisplay(); + } + + return $output; + } + + /** + * Function to get html for the sql query results table + * + * @param DisplayResults $displayResultsObject instance of DisplayResult + * @param string $pmaThemeImage theme image uri + * @param string $url_query url query + * @param array $displayParts the parts to display + * @param bool $editable whether the result table is + * editable or not + * @param int $unlim_num_rows unlimited number of rows + * @param int $num_rows number of rows + * @param bool $showtable whether to show table or not + * @param object|null $result result of the executed query + * @param array $analyzed_sql_results analyzed sql results + * @param bool $is_limited_display Show only limited operations or not + * + * @return string + */ + private function getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + array $displayParts, + $editable, + $unlim_num_rows, + $num_rows, + $showtable, + $result, + array $analyzed_sql_results, + $is_limited_display = false + ) { + $printview = isset($_POST['printview']) && $_POST['printview'] == '1' ? '1' : null; + $table_html = ''; + $browse_dist = ! empty($_POST['is_browse_distinct']); + + if ($analyzed_sql_results['is_procedure']) { + do { + if (! isset($result)) { + $result = $GLOBALS['dbi']->storeResult(); + } + $num_rows = $GLOBALS['dbi']->numRows($result); + + if ($result !== false && $num_rows > 0) { + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + if (! is_array($fields_meta)) { + $fields_cnt = 0; + } else { + $fields_cnt = count($fields_meta); + } + + $displayResultsObject->setProperties( + $num_rows, + $fields_meta, + $analyzed_sql_results['is_count'], + $analyzed_sql_results['is_export'], + $analyzed_sql_results['is_func'], + $analyzed_sql_results['is_analyse'], + $num_rows, + $fields_cnt, + $GLOBALS['querytime'], + $pmaThemeImage, + $GLOBALS['text_dir'], + $analyzed_sql_results['is_maint'], + $analyzed_sql_results['is_explain'], + $analyzed_sql_results['is_show'], + $showtable, + $printview, + $url_query, + $editable, + $browse_dist + ); + + $displayParts = [ + 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'sort_lnk' => '1', + 'nav_bar' => '1', + 'bkm_form' => '1', + 'text_btn' => '1', + 'pview_lnk' => '1', + ]; + + $table_html .= $displayResultsObject->getTable( + $result, + $displayParts, + $analyzed_sql_results, + $is_limited_display + ); + } + + $GLOBALS['dbi']->freeResult($result); + } while ($GLOBALS['dbi']->moreResults() && $GLOBALS['dbi']->nextResult()); + } else { + $fields_meta = []; + if (isset($result) && ! is_bool($result)) { + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + } + $fields_cnt = count($fields_meta); + $_SESSION['is_multi_query'] = false; + $displayResultsObject->setProperties( + $unlim_num_rows, + $fields_meta, + $analyzed_sql_results['is_count'], + $analyzed_sql_results['is_export'], + $analyzed_sql_results['is_func'], + $analyzed_sql_results['is_analyse'], + $num_rows, + $fields_cnt, + $GLOBALS['querytime'], + $pmaThemeImage, + $GLOBALS['text_dir'], + $analyzed_sql_results['is_maint'], + $analyzed_sql_results['is_explain'], + $analyzed_sql_results['is_show'], + $showtable, + $printview, + $url_query, + $editable, + $browse_dist + ); + + if (! is_bool($result)) { + $table_html .= $displayResultsObject->getTable( + $result, + $displayParts, + $analyzed_sql_results, + $is_limited_display + ); + } + $GLOBALS['dbi']->freeResult($result); + } + + return $table_html; + } + + /** + * Function to get html for the previous query if there is such. If not will return + * null + * + * @param string|null $displayQuery display query + * @param bool $showSql whether to show sql + * @param array $sqlData sql data + * @param Message|string $displayMessage display message + * + * @return string + */ + private function getHtmlForPreviousUpdateQuery( + ?string $displayQuery, + bool $showSql, + $sqlData, + $displayMessage + ): string { + $output = ''; + if (isset($displayQuery) && ($showSql === true) && empty($sqlData)) { + $output = Util::getMessage( + $displayMessage, + $displayQuery, + 'success' + ); + } + + return $output; + } + + /** + * To get the message if a column index is missing. If not will return null + * + * @param string $table current table + * @param string $database current database + * @param boolean $editable whether the results table can be editable or not + * @param boolean $hasUniqueKey whether there is a unique key + * + * @return string + */ + private function getMessageIfMissingColumnIndex($table, $database, $editable, $hasUniqueKey): string + { + $output = ''; + if (! empty($table) && ($GLOBALS['dbi']->isSystemSchema($database) || ! $editable)) { + $output = Message::notice( + sprintf( + __( + 'Current selection does not contain a unique column.' + . ' Grid edit, checkbox, Edit, Copy and Delete features' + . ' are not available. %s' + ), + Util::showDocu( + 'config', + 'cfg_RowActionLinksWithoutUnique' + ) + ) + )->getDisplay(); + } elseif (! empty($table) && ! $hasUniqueKey) { + $output = Message::notice( + sprintf( + __( + 'Current selection does not contain a unique column.' + . ' Grid edit, Edit, Copy and Delete features may result in' + . ' undesired behavior. %s' + ), + Util::showDocu( + 'config', + 'cfg_RowActionLinksWithoutUnique' + ) + ) + )->getDisplay(); + } + + return $output; + } + + /** + * Function to get html to display problems in indexes + * + * @param string|null $queryType query type + * @param array|null $selectedTables array of table names selected from the + * database structure page, for an action + * like check table, optimize table, + * analyze table or repair table + * @param string $database current database + * + * @return string + */ + private function getHtmlForIndexesProblems(?string $queryType, ?array $selectedTables, string $database): string + { + // BEGIN INDEX CHECK See if indexes should be checked. + $output = ''; + if (isset($queryType) + && $queryType == 'check_tbl' + && isset($selectedTables) + && is_array($selectedTables) + ) { + foreach ($selectedTables as $table) { + $check = Index::findDuplicates($table, $database); + if (! empty($check)) { + $output .= sprintf( + __('Problems with indexes of table `%s`'), + $table + ); + $output .= $check; + } + } + } + + return $output; + } + + /** + * Function to display results when the executed query returns non empty results + * + * @param object|null $result executed query results + * @param array $analyzed_sql_results analysed sql results + * @param string $db current database + * @param string $table current table + * @param string|null $message message to show + * @param array|null $sql_data sql data + * @param DisplayResults $displayResultsObject Instance of DisplayResults + * @param string $pmaThemeImage uri of the theme image + * @param int $unlim_num_rows unlimited number of rows + * @param int $num_rows number of rows + * @param string|null $disp_query display query + * @param Message|string|null $disp_message display message + * @param array|null $profiling_results profiling results + * @param string|null $query_type query type + * @param array|null $selectedTables array of table names selected + * from the database structure page, for + * an action like check table, + * optimize table, analyze table or + * repair table + * @param string $sql_query sql query + * @param string|null $complete_query complete sql query + * + * @return string html + */ + private function getQueryResponseForResultsReturned( + $result, + array $analyzed_sql_results, + $db, + $table, + ?string $message, + ?array $sql_data, + $displayResultsObject, + $pmaThemeImage, + $unlim_num_rows, + $num_rows, + ?string $disp_query, + $disp_message, + ?array $profiling_results, + ?string $query_type, + $selectedTables, + $sql_query, + ?string $complete_query + ) { + global $showtable, $url_query; + // If we are retrieving the full value of a truncated field or the original + // value of a transformed field, show it here + if (isset($_POST['grid_edit']) && $_POST['grid_edit'] == true) { + $this->sendResponseForGridEdit($result); + // script has exited at this point + } + + // Gets the list of fields properties + if (isset($result) && $result) { + $fields_meta = $GLOBALS['dbi']->getFieldsMeta($result); + } else { + $fields_meta = []; + } + + // Should be initialized these parameters before parsing + $showtable = isset($showtable) ? $showtable : null; + $url_query = isset($url_query) ? $url_query : null; + + $response = Response::getInstance(); + $header = $response->getHeader(); + $scripts = $header->getScripts(); + + $just_one_table = $this->resultSetHasJustOneTable($fields_meta); + + // hide edit and delete links: + // - for information_schema + // - if the result set does not contain all the columns of a unique key + // (unless this is an updatable view) + // - if the SELECT query contains a join or a subquery + + $updatableView = false; + + $statement = isset($analyzed_sql_results['statement']) ? $analyzed_sql_results['statement'] : null; + if ($statement instanceof SelectStatement) { + if (! empty($statement->expr)) { + if ($statement->expr[0]->expr === '*') { + $_table = new Table($table, $db); + $updatableView = $_table->isUpdatableView(); + } + } + + if ($analyzed_sql_results['join'] + || $analyzed_sql_results['is_subquery'] + || count($analyzed_sql_results['select_tables']) !== 1 + ) { + $just_one_table = false; + } + } + + $has_unique = $this->resultSetContainsUniqueKey( + $db, + $table, + $fields_meta + ); + + $editable = ($has_unique + || $GLOBALS['cfg']['RowActionLinksWithoutUnique'] + || $updatableView) + && $just_one_table; + + $_SESSION['tmpval']['possible_as_geometry'] = $editable; + + $displayParts = [ + 'edit_lnk' => $displayResultsObject::UPDATE_ROW, + 'del_lnk' => $displayResultsObject::DELETE_ROW, + 'sort_lnk' => '1', + 'nav_bar' => '1', + 'bkm_form' => '1', + 'text_btn' => '0', + 'pview_lnk' => '1', + ]; + + if ($GLOBALS['dbi']->isSystemSchema($db) || ! $editable) { + $displayParts = [ + 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'sort_lnk' => '1', + 'nav_bar' => '1', + 'bkm_form' => '1', + 'text_btn' => '1', + 'pview_lnk' => '1', + ]; + } + if (isset($_POST['printview']) && $_POST['printview'] == '1') { + $displayParts = [ + 'edit_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'del_lnk' => $displayResultsObject::NO_EDIT_OR_DELETE, + 'sort_lnk' => '0', + 'nav_bar' => '0', + 'bkm_form' => '0', + 'text_btn' => '0', + 'pview_lnk' => '0', + ]; + } + + $tableMaintenanceHtml = ''; + if (isset($_POST['table_maintenance'])) { + $scripts->addFile('makegrid.js'); + $scripts->addFile('sql.js'); + if (isset($message)) { + $message = Message::success($message); + $tableMaintenanceHtml = Util::getMessage( + $message, + $GLOBALS['sql_query'], + 'success' + ); + } + $tableMaintenanceHtml .= $this->getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + $displayParts, + false, + $unlim_num_rows, + $num_rows, + $showtable, + $result, + $analyzed_sql_results + ); + if (empty($sql_data) || ($sql_data['valid_queries'] = 1)) { + $response->addHTML($tableMaintenanceHtml); + exit; + } + } + + if (! isset($_POST['printview']) || $_POST['printview'] != '1') { + $scripts->addFile('makegrid.js'); + $scripts->addFile('sql.js'); + unset($GLOBALS['message']); + //we don't need to buffer the output in getMessage here. + //set a global variable and check against it in the function + $GLOBALS['buffer_message'] = false; + } + + $previousUpdateQueryHtml = $this->getHtmlForPreviousUpdateQuery( + isset($disp_query) ? $disp_query : null, + (bool) $GLOBALS['cfg']['ShowSQL'], + isset($sql_data) ? $sql_data : null, + isset($disp_message) ? $disp_message : null + ); + + $profilingChartHtml = $this->getHtmlForProfilingChart( + $url_query, + $db, + isset($profiling_results) ? $profiling_results : [] + ); + + $missingUniqueColumnMessage = $this->getMessageIfMissingColumnIndex( + $table, + $db, + $editable, + $has_unique + ); + + $bookmarkCreatedMessage = $this->getBookmarkCreatedMessage(); + + $tableHtml = $this->getHtmlForSqlQueryResultsTable( + $displayResultsObject, + $pmaThemeImage, + $url_query, + $displayParts, + $editable, + $unlim_num_rows, + $num_rows, + $showtable, + $result, + $analyzed_sql_results + ); + + $indexesProblemsHtml = $this->getHtmlForIndexesProblems( + isset($query_type) ? $query_type : null, + isset($selectedTables) ? $selectedTables : null, + $db + ); + + $cfgBookmark = Bookmark::getParams($GLOBALS['cfg']['Server']['user']); + $bookmarkSupportHtml = ''; + if ($cfgBookmark) { + $bookmarkSupportHtml = $this->getHtmlForBookmark( + $displayParts, + $cfgBookmark, + $sql_query, + $db, + $table, + isset($complete_query) ? $complete_query : $sql_query, + $cfgBookmark['user'] + ); + } + + return $this->template->render('sql/sql_query_results', [ + 'table_maintenance' => $tableMaintenanceHtml, + 'previous_update_query' => $previousUpdateQueryHtml, + 'profiling_chart' => $profilingChartHtml, + 'missing_unique_column_message' => $missingUniqueColumnMessage, + 'bookmark_created_message' => $bookmarkCreatedMessage, + 'table' => $tableHtml, + 'indexes_problems' => $indexesProblemsHtml, + 'bookmark_support' => $bookmarkSupportHtml, + ]); + } + + /** + * Function to execute the query and send the response + * + * @param array $analyzed_sql_results analysed sql results + * @param bool $is_gotofile whether goto file or not + * @param string $db current database + * @param string $table current table + * @param bool|null $find_real_end whether to find real end or not + * @param string $sql_query_for_bookmark the sql query to be stored as bookmark + * @param array|null $extra_data extra data + * @param string $message_to_show message to show + * @param string $message message + * @param array|null $sql_data sql data + * @param string $goto goto page url + * @param string $pmaThemeImage uri of the PMA theme image + * @param string $disp_query display query + * @param Message|string $disp_message display message + * @param string $query_type query type + * @param string $sql_query sql query + * @param array|null $selectedTables array of table names selected from the + * database structure page, for an action + * like check table, optimize table, + * analyze table or repair table + * @param string $complete_query complete query + * + * @return void + */ + public function executeQueryAndSendQueryResponse( + $analyzed_sql_results, + $is_gotofile, + $db, + $table, + $find_real_end, + $sql_query_for_bookmark, + $extra_data, + $message_to_show, + $message, + $sql_data, + $goto, + $pmaThemeImage, + $disp_query, + $disp_message, + $query_type, + $sql_query, + $selectedTables, + $complete_query + ) { + if ($analyzed_sql_results == null) { + // Parse and analyze the query + list( + $analyzed_sql_results, + $db, + $table_from_sql + ) = ParseAnalyze::sqlQuery($sql_query, $db); + // @todo: possibly refactor + extract($analyzed_sql_results); + + if ($table != $table_from_sql && ! empty($table_from_sql)) { + $table = $table_from_sql; + } + } + + $html_output = $this->executeQueryAndGetQueryResponse( + $analyzed_sql_results, // analyzed_sql_results + $is_gotofile, // is_gotofile + $db, // db + $table, // table + $find_real_end, // find_real_end + $sql_query_for_bookmark, // sql_query_for_bookmark + $extra_data, // extra_data + $message_to_show, // message_to_show + $message, // message + $sql_data, // sql_data + $goto, // goto + $pmaThemeImage, // pmaThemeImage + $disp_query, // disp_query + $disp_message, // disp_message + $query_type, // query_type + $sql_query, // sql_query + $selectedTables, // selectedTables + $complete_query // complete_query + ); + + $response = Response::getInstance(); + $response->addHTML($html_output); + } + + /** + * Function to execute the query and send the response + * + * @param array $analyzed_sql_results analysed sql results + * @param bool $is_gotofile whether goto file or not + * @param string $db current database + * @param string $table current table + * @param bool|null $find_real_end whether to find real end or not + * @param string|null $sql_query_for_bookmark the sql query to be stored as bookmark + * @param array|null $extra_data extra data + * @param string|null $message_to_show message to show + * @param Message|string|null $message message + * @param array|null $sql_data sql data + * @param string $goto goto page url + * @param string $pmaThemeImage uri of the PMA theme image + * @param string|null $disp_query display query + * @param Message|string|null $disp_message display message + * @param string|null $query_type query type + * @param string $sql_query sql query + * @param array|null $selectedTables array of table names selected from the + * database structure page, for an action + * like check table, optimize table, + * analyze table or repair table + * @param string|null $complete_query complete query + * + * @return string html + */ + public function executeQueryAndGetQueryResponse( + array $analyzed_sql_results, + $is_gotofile, + $db, + $table, + $find_real_end, + ?string $sql_query_for_bookmark, + $extra_data, + ?string $message_to_show, + $message, + $sql_data, + $goto, + $pmaThemeImage, + ?string $disp_query, + $disp_message, + ?string $query_type, + $sql_query, + $selectedTables, + ?string $complete_query + ) { + // Handle disable/enable foreign key checks + $default_fk_check = Util::handleDisableFKCheckInit(); + + // Handle remembered sorting order, only for single table query. + // Handling is not required when it's a union query + // (the parser never sets the 'union' key to 0). + // Handling is also not required if we came from the "Sort by key" + // drop-down. + if (! empty($analyzed_sql_results) + && $this->isRememberSortingOrder($analyzed_sql_results) + && empty($analyzed_sql_results['union']) + && ! isset($_POST['sort_by_key']) + ) { + if (! isset($_SESSION['sql_from_query_box'])) { + $this->handleSortOrder($db, $table, $analyzed_sql_results, $sql_query); + } else { + unset($_SESSION['sql_from_query_box']); + } + } + + $displayResultsObject = new DisplayResults( + $GLOBALS['db'], + $GLOBALS['table'], + $GLOBALS['server'], + $goto, + $sql_query + ); + $displayResultsObject->setConfigParamsForDisplayTable(); + + // assign default full_sql_query + $full_sql_query = $sql_query; + + // Do append a "LIMIT" clause? + if ($this->isAppendLimitClause($analyzed_sql_results)) { + $full_sql_query = $this->getSqlWithLimitClause($analyzed_sql_results); + } + + $GLOBALS['reload'] = $this->hasCurrentDbChanged($db); + $GLOBALS['dbi']->selectDb($db); + + list( + $result, + $num_rows, + $unlim_num_rows, + $profiling_results, + $extra_data + ) = $this->executeTheQuery( + $analyzed_sql_results, + $full_sql_query, + $is_gotofile, + $db, + $table, + isset($find_real_end) ? $find_real_end : null, + isset($sql_query_for_bookmark) ? $sql_query_for_bookmark : null, + isset($extra_data) ? $extra_data : null + ); + + if ($GLOBALS['dbi']->moreResults()) { + $GLOBALS['dbi']->nextResult(); + } + + $warning_messages = $this->operations->getWarningMessagesArray(); + + // No rows returned -> move back to the calling page + if ((0 == $num_rows && 0 == $unlim_num_rows) + || $analyzed_sql_results['is_affected'] + ) { + $html_output = $this->getQueryResponseForNoResultsReturned( + $analyzed_sql_results, + $db, + $table, + isset($message_to_show) ? $message_to_show : null, + $num_rows, + $displayResultsObject, + $extra_data, + $pmaThemeImage, + $profiling_results, + isset($result) ? $result : null, + $sql_query, + isset($complete_query) ? $complete_query : null + ); + } else { + // At least one row is returned -> displays a table with results + $html_output = $this->getQueryResponseForResultsReturned( + isset($result) ? $result : null, + $analyzed_sql_results, + $db, + $table, + isset($message) ? $message : null, + isset($sql_data) ? $sql_data : null, + $displayResultsObject, + $pmaThemeImage, + $unlim_num_rows, + $num_rows, + isset($disp_query) ? $disp_query : null, + isset($disp_message) ? $disp_message : null, + $profiling_results, + isset($query_type) ? $query_type : null, + isset($selectedTables) ? $selectedTables : null, + $sql_query, + isset($complete_query) ? $complete_query : null + ); + } + + // Handle disable/enable foreign key checks + Util::handleDisableFKCheckCleanup($default_fk_check); + + foreach ($warning_messages as $warning) { + $message = Message::notice(Message::sanitize($warning)); + $html_output .= $message->getDisplay(); + } + + return $html_output; + } + + /** + * Function to define pos to display a row + * + * @param int $number_of_line Number of the line to display + * @param int $max_rows Number of rows by page + * + * @return int Start position to display the line + */ + private function getStartPosToDisplayRow($number_of_line, $max_rows = null) + { + if (null === $max_rows) { + $max_rows = $_SESSION['tmpval']['max_rows']; + } + + return @((ceil($number_of_line / $max_rows) - 1) * $max_rows); + } + + /** + * Function to calculate new pos if pos is higher than number of rows + * of displayed table + * + * @param string $db Database name + * @param string $table Table name + * @param int|null $pos Initial position + * + * @return int Number of pos to display last page + */ + public function calculatePosForLastPage($db, $table, $pos) + { + if (null === $pos) { + $pos = $_SESSION['tmpval']['pos']; + } + + $_table = new Table($table, $db); + $unlim_num_rows = $_table->countRecords(true); + //If position is higher than number of rows + if ($unlim_num_rows <= $pos && 0 != $pos) { + $pos = $this->getStartPosToDisplayRow($unlim_num_rows); + } + + return $pos; + } +} |
