From 04d6d5ca99ebfd1cebb8ce06618fb3811fc1a8aa Mon Sep 17 00:00:00 2001 From: Charles Date: Thu, 9 Jan 2020 10:55:03 +0100 Subject: phpmyadmin working --- .../classes/Controllers/AbstractController.php | 51 + .../classes/Controllers/AjaxController.php | 97 ++ .../Controllers/BrowseForeignersController.php | 82 + .../Controllers/Database/AbstractController.php | 42 + .../Database/CentralColumnsController.php | 195 +++ .../Database/DataDictionaryController.php | 156 ++ .../Controllers/Database/EventsController.php | 43 + .../Database/MultiTableQueryController.php | 61 + .../Controllers/Database/RoutinesController.php | 44 + .../classes/Controllers/Database/SqlController.php | 49 + .../Controllers/Database/StructureController.php | 1088 +++++++++++++ .../Controllers/Database/TriggersController.php | 43 + .../classes/Controllers/HomeController.php | 517 ++++++ .../Controllers/Server/BinlogController.php | 149 ++ .../Controllers/Server/CollationsController.php | 100 ++ .../Controllers/Server/DatabasesController.php | 424 +++++ .../Controllers/Server/EnginesController.php | 69 + .../Controllers/Server/PluginsController.php | 77 + .../Controllers/Server/ReplicationController.php | 72 + .../classes/Controllers/Server/SqlController.php | 34 + .../Server/Status/AbstractController.php | 42 + .../Server/Status/AdvisorController.php | 60 + .../Server/Status/MonitorController.php | 146 ++ .../Server/Status/ProcessesController.php | 240 +++ .../Server/Status/QueriesController.php | 75 + .../Controllers/Server/Status/StatusController.php | 260 +++ .../Server/Status/VariablesController.php | 639 ++++++++ .../Controllers/Server/VariablesController.php | 238 +++ .../Controllers/Setup/AbstractController.php | 70 + .../classes/Controllers/Setup/ConfigController.php | 55 + .../classes/Controllers/Setup/FormController.php | 50 + .../classes/Controllers/Setup/HomeController.php | 228 +++ .../Controllers/Setup/ServersController.php | 66 + .../Controllers/Table/AbstractController.php | 54 + .../classes/Controllers/Table/ChartController.php | 261 ++++ .../Table/GisVisualizationController.php | 227 +++ .../Controllers/Table/IndexesController.php | 179 +++ .../Controllers/Table/RelationController.php | 398 +++++ .../classes/Controllers/Table/SearchController.php | 1244 +++++++++++++++ .../classes/Controllers/Table/SqlController.php | 53 + .../Controllers/Table/StructureController.php | 1648 ++++++++++++++++++++ .../TransformationOverviewController.php | 80 + 42 files changed, 9706 insertions(+) create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php create mode 100644 srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php (limited to 'srcs/phpmyadmin/libraries/classes/Controllers') diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php new file mode 100644 index 0000000..601e79f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/AbstractController.php @@ -0,0 +1,51 @@ +response = $response; + $this->dbi = $dbi; + $this->template = $template; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php b/srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php new file mode 100644 index 0000000..0662277 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/AjaxController.php @@ -0,0 +1,97 @@ +config = $config; + } + + /** + * @return array JSON + */ + public function databases(): array + { + global $dblist; + + return ['databases' => $dblist->databases]; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function tables(array $params): array + { + return ['tables' => $this->dbi->getTables($params['db'])]; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function columns(array $params): array + { + return [ + 'columns' => $this->dbi->getColumnNames( + $params['db'], + $params['table'] + ), + ]; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function getConfig(array $params): array + { + return ['value' => $this->config->get($params['key'])]; + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function setConfig(array $params) + { + return $this->config->setUserValue( + null, + $params['key'], + json_decode($params['value']) + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php b/srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php new file mode 100644 index 0000000..24441cd --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/BrowseForeignersController.php @@ -0,0 +1,82 @@ +browseForeigners = $browseForeigners; + $this->relation = $relation; + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function index(array $params): string + { + $foreigners = $this->relation->getForeigners( + $params['db'], + $params['table'] + ); + $foreignLimit = $this->browseForeigners->getForeignLimit( + $params['foreign_showAll'] + ); + $foreignData = $this->relation->getForeignData( + $foreigners, + $params['field'], + true, + $params['foreign_filter'] ?? '', + $foreignLimit ?? null, + true + ); + + return $this->browseForeigners->getHtmlForRelationalFieldSelection( + $params['db'], + $params['table'], + $params['field'], + $foreignData, + $params['fieldkey'] ?? '', + $params['data'] ?? '' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php new file mode 100644 index 0000000..31b752b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/AbstractController.php @@ -0,0 +1,42 @@ +db = $db; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php new file mode 100644 index 0000000..97798ea --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/CentralColumnsController.php @@ -0,0 +1,195 @@ +centralColumns = $centralColumns; + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function index(array $params): string + { + global $pmaThemeImage, $text_dir; + + if (! empty($params['total_rows']) + && Core::isValid($params['total_rows'], 'integer') + ) { + $totalRows = (int) $params['total_rows']; + } else { + $totalRows = $this->centralColumns->getCount($this->db); + } + + $pos = 0; + if (Core::isValid($params['pos'], 'integer')) { + $pos = (int) $params['pos']; + } + + return $this->centralColumns->getHtmlForMain( + $this->db, + $totalRows, + $pos, + $pmaThemeImage, + $text_dir + ); + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function getColumnList(array $params): array + { + return $this->centralColumns->getListRaw( + $this->db, + $params['cur_table'] ?? '' + ); + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function populateColumns(array $params): string + { + return $this->centralColumns->getHtmlForColumnDropdown( + $this->db, + $params['selectedTable'] + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function editSave(array $params) + { + $columnDefault = $params['col_default']; + if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') { + $columnDefault = ''; + } + return $this->centralColumns->updateOneColumn( + $this->db, + $params['orig_col_name'], + $params['col_name'], + $params['col_type'], + $params['col_attribute'], + $params['col_length'], + isset($params['col_isNull']) ? 1 : 0, + $params['collation'], + $params['col_extra'] ?? '', + $columnDefault + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function addNewColumn(array $params) + { + $columnDefault = $params['col_default']; + if ($columnDefault === 'NONE' && $params['col_default_sel'] !== 'USER_DEFINED') { + $columnDefault = ''; + } + return $this->centralColumns->updateOneColumn( + $this->db, + '', + $params['col_name'], + $params['col_type'], + $params['col_attribute'], + $params['col_length'], + isset($params['col_isNull']) ? 1 : 0, + $params['collation'], + $params['col_extra'] ?? '', + $columnDefault + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function addColumn(array $params) + { + return $this->centralColumns->syncUniqueColumns( + [$params['column-select']], + false, + $params['table-select'] + ); + } + + /** + * @param array $params Request parameters + * @return string HTML + */ + public function editPage(array $params): string + { + return $this->centralColumns->getHtmlForEditingPage( + $params['selected_fld'], + $params['db'] + ); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function updateMultipleColumn(array $params) + { + return $this->centralColumns->updateMultipleColumn($params); + } + + /** + * @param array $params Request parameters + * @return true|Message + */ + public function deleteSave(array $params) + { + $name = []; + parse_str($params['col_name'], $name); + return $this->centralColumns->deleteColumnsFromList( + $params['db'], + $name['selected_fld'], + false + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php new file mode 100644 index 0000000..ba424b6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/DataDictionaryController.php @@ -0,0 +1,156 @@ +relation = $relation; + $this->transformations = $transformations; + } + + /** + * @return string HTML + */ + public function index(): string + { + $cfgRelation = $this->relation->getRelationsParam(); + + $comment = $this->relation->getDbComment($this->db); + + $this->dbi->selectDb($this->db); + $tablesNames = $this->dbi->getTables($this->db); + + $tables = []; + foreach ($tablesNames as $tableName) { + $showComment = (string) $this->dbi->getTable( + $this->db, + $tableName + )->getStatusInfo('TABLE_COMMENT'); + + list(, $primaryKeys, , ) = Util::processIndexData( + $this->dbi->getTableIndexes($this->db, $tableName) + ); + + list($foreigners, $hasRelation) = $this->relation->getRelationsAndStatus( + ! empty($cfgRelation['relation']), + $this->db, + $tableName + ); + + $columnsComments = $this->relation->getComments($this->db, $tableName); + + $columns = $this->dbi->getColumns($this->db, $tableName); + $rows = []; + foreach ($columns as $row) { + $extractedColumnSpec = Util::extractColumnSpec($row['Type']); + + $relation = ''; + if ($hasRelation) { + $foreigner = $this->relation->searchColumnInForeigners( + $foreigners, + $row['Field'] + ); + if ($foreigner !== false && $foreigner !== []) { + $relation = $foreigner['foreign_table']; + $relation .= ' -> '; + $relation .= $foreigner['foreign_field']; + } + } + + $mime = ''; + if ($cfgRelation['mimework']) { + $mimeMap = $this->transformations->getMime( + $this->db, + $tableName, + true + ); + if (isset($mimeMap[$row['Field']])) { + $mime = str_replace( + '_', + '/', + $mimeMap[$row['Field']]['mimetype'] + ); + } + } + + $rows[$row['Field']] = [ + 'name' => $row['Field'], + 'has_primary_key' => isset($primaryKeys[$row['Field']]), + 'type' => $extractedColumnSpec['type'], + 'print_type' => $extractedColumnSpec['print_type'], + 'is_nullable' => $row['Null'] !== '' && $row['Null'] !== 'NO', + 'default' => $row['Default'] ?? null, + 'comment' => $columnsComments[$row['Field']] ?? '', + 'mime' => $mime, + 'relation' => $relation, + ]; + } + + $indexesTable = ''; + if (count(Index::getFromTable($tableName, $this->db)) > 0) { + $indexesTable = Index::getHtmlForIndexes( + $tableName, + $this->db, + true + ); + } + + $tables[$tableName] = [ + 'name' => $tableName, + 'comment' => $showComment, + 'has_relation' => $hasRelation, + 'has_mime' => $cfgRelation['mimework'], + 'columns' => $rows, + 'indexes_table' => $indexesTable, + ]; + } + + return $this->template->render('database/data_dictionary/index', [ + 'database' => $this->db, + 'comment' => $comment, + 'tables' => $tables, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php new file mode 100644 index 0000000..e2b0121 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/EventsController.php @@ -0,0 +1,43 @@ +dbi); + $events->main(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php new file mode 100644 index 0000000..b4e1375 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/MultiTableQueryController.php @@ -0,0 +1,61 @@ +dbi, $template, $this->db); + + return $queryInstance->getFormHtml(); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function displayResults(array $params): void + { + global $pmaThemeImage; + + MultiTableQuery::displayResults( + $params['sql_query'], + $params['db'], + $pmaThemeImage + ); + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function table(array $params): array + { + $constrains = $this->dbi->getForeignKeyConstrains( + $params['db'], + $params['tables'] + ); + + return ['foreignKeyConstrains' => $constrains]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php new file mode 100644 index 0000000..4106ccc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/RoutinesController.php @@ -0,0 +1,44 @@ +dbi); + $routines->main($params['type']); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php new file mode 100644 index 0000000..963f5bc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/SqlController.php @@ -0,0 +1,49 @@ +getHtml( + true, + false, + isset($params['delimiter']) + ? htmlspecialchars($params['delimiter']) + : ';' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php new file mode 100644 index 0000000..5407f19 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/StructureController.php @@ -0,0 +1,1088 @@ +relation = $relation; + $this->replication = $replication; + } + + /** + * Retrieves database information for further use + * + * @param string $subPart Page part name + * + * @return void + */ + private function getDatabaseInfo(string $subPart): void + { + list( + $tables, + $numTables, + $totalNumTables, + , + $isShowStats, + $dbIsSystemSchema, + , + , + $position + ) = Util::getDbInfo($this->db, $subPart); + + $this->tables = $tables; + $this->numTables = $numTables; + $this->position = $position; + $this->dbIsSystemSchema = $dbIsSystemSchema; + $this->totalNumTables = $totalNumTables; + $this->isShowStats = $isShowStats; + } + + /** + * Index action + * + * @param array $params Request parameters + * @return string HTML + */ + public function index(array $params): string + { + global $cfg; + + // Drops/deletes/etc. multiple tables if required + if ((! empty($params['submit_mult']) && isset($params['selected_tbl'])) + || isset($params['mult_btn']) + ) { + $this->multiSubmitAction(); + } + + // Gets the database structure + $this->getDatabaseInfo('_structure'); + + // Checks if there are any tables to be shown on current page. + // If there are no tables, the user is redirected to the last page + // having any. + if ($this->totalNumTables > 0 && $this->position > $this->totalNumTables) { + $uri = './db_structure.php' . Url::getCommonRaw([ + 'db' => $this->db, + 'pos' => max(0, $this->totalNumTables - $cfg['MaxTableList']), + 'reload' => 1, + ]); + Core::sendHeaderLocation($uri); + } + + include_once ROOT_PATH . 'libraries/replication.inc.php'; + + PageSettings::showGroup('DbStructure'); + + if ($this->numTables > 0) { + $urlParams = [ + 'pos' => $this->position, + 'db' => $this->db, + ]; + if (isset($params['sort'])) { + $urlParams['sort'] = $params['sort']; + } + if (isset($params['sort_order'])) { + $urlParams['sort_order'] = $params['sort_order']; + } + $listNavigator = Util::getListNavigator( + $this->totalNumTables, + $this->position, + $urlParams, + 'db_structure.php', + 'frame_content', + $cfg['MaxTableList'] + ); + + $tableList = $this->displayTableList(); + } + + $createTable = ''; + if (empty($this->dbIsSystemSchema)) { + $createTable = CreateTable::getHtml($this->db); + } + + return $this->template->render('database/structure/index', [ + 'database' => $this->db, + 'has_tables' => $this->numTables > 0, + 'list_navigator_html' => $listNavigator ?? '', + 'table_list_html' => $tableList ?? '', + 'is_system_schema' => ! empty($this->dbIsSystemSchema), + 'create_table_html' => $createTable, + ]); + } + + /** + * Add or remove favorite tables + * + * @param array $params Request parameters + * @return array|null JSON + */ + public function addRemoveFavoriteTablesAction(array $params): ?array + { + global $cfg; + + $favoriteInstance = RecentFavoriteTable::getInstance('favorite'); + if (isset($params['favoriteTables'])) { + $favoriteTables = json_decode($params['favoriteTables'], true); + } else { + $favoriteTables = []; + } + // Required to keep each user's preferences separate. + $user = sha1($cfg['Server']['user']); + + // Request for Synchronization of favorite tables. + if (isset($params['sync_favorite_tables'])) { + $cfgRelation = $this->relation->getRelationsParam(); + if ($cfgRelation['favoritework']) { + return $this->synchronizeFavoriteTables($favoriteInstance, $user, $favoriteTables); + } + return null; + } + $changes = true; + $titles = Util::buildActionTitles(); + $favoriteTable = $params['favorite_table']; + $alreadyFavorite = $this->checkFavoriteTable($favoriteTable); + + if (isset($params['remove_favorite'])) { + if ($alreadyFavorite) { + // If already in favorite list, remove it. + $favoriteInstance->remove($this->db, $favoriteTable); + $alreadyFavorite = false; // for favorite_anchor template + } + } elseif (isset($params['add_favorite'])) { + if (! $alreadyFavorite) { + $numTables = count($favoriteInstance->getTables()); + if ($numTables == $cfg['NumFavoriteTables']) { + $changes = false; + } else { + // Otherwise add to favorite list. + $favoriteInstance->add($this->db, $favoriteTable); + $alreadyFavorite = true; // for favorite_anchor template + } + } + } + + $favoriteTables[$user] = $favoriteInstance->getTables(); + + $json = []; + $json['changes'] = $changes; + if (! $changes) { + $json['message'] = $this->template->render('components/error_message', [ + 'msg' => __("Favorite List is full!"), + ]); + return $json; + } + // Check if current table is already in favorite list. + $favoriteParams = [ + 'db' => $this->db, + 'ajax_request' => true, + 'favorite_table' => $favoriteTable, + ($alreadyFavorite ? 'remove' : 'add') . '_favorite' => true, + ]; + + $json['user'] = $user; + $json['favoriteTables'] = json_encode($favoriteTables); + $json['list'] = $favoriteInstance->getHtmlList(); + $json['anchor'] = $this->template->render('database/structure/favorite_anchor', [ + 'table_name_hash' => md5($favoriteTable), + 'db_table_name_hash' => md5($this->db . "." . $favoriteTable), + 'fav_params' => $favoriteParams, + 'already_favorite' => $alreadyFavorite, + 'titles' => $titles, + ]); + + return $json; + } + + /** + * Handles request for real row count on database level view page. + * + * @param array $params Request parameters + * @return array JSON + */ + public function handleRealRowCountRequestAction(array $params): array + { + // If there is a request to update all table's row count. + if (! isset($params['real_row_count_all'])) { + // Get the real row count for the table. + $realRowCount = $this->dbi + ->getTable($this->db, (string) $params['table']) + ->getRealRowCountTable(); + // Format the number. + $realRowCount = Util::formatNumber($realRowCount, 0); + + return ['real_row_count' => $realRowCount]; + } + + // Array to store the results. + $realRowCountAll = []; + // Iterate over each table and fetch real row count. + foreach ($this->tables as $table) { + $rowCount = $this->dbi + ->getTable($this->db, $table['TABLE_NAME']) + ->getRealRowCountTable(); + $realRowCountAll[] = [ + 'table' => $table['TABLE_NAME'], + 'row_count' => $rowCount, + ]; + } + + return ['real_row_count_all' => json_encode($realRowCountAll)]; + } + + /** + * Handles actions related to multiple tables + * + * @return void + */ + public function multiSubmitAction(): void + { + $action = 'db_structure.php'; + $err_url = 'db_structure.php' . Url::getCommon( + ['db' => $this->db] + ); + + // see bug #2794840; in this case, code path is: + // db_structure.php -> libraries/mult_submits.inc.php -> sql.php + // -> db_structure.php and if we got an error on the multi submit, + // we must display it here and not call again mult_submits.inc.php + if (! isset($_POST['error']) || false === $_POST['error']) { + include ROOT_PATH . 'libraries/mult_submits.inc.php'; + } + if (empty($_POST['message'])) { + $_POST['message'] = Message::success(); + } + } + + /** + * Displays the list of tables + * + * @return string HTML + */ + protected function displayTableList(): string + { + $html = ''; + + // filtering + $html .= $this->template->render('filter', ['filter_value' => '']); + + $i = $sum_entries = 0; + $overhead_check = false; + $create_time_all = ''; + $update_time_all = ''; + $check_time_all = ''; + $num_columns = $GLOBALS['cfg']['PropertiesNumColumns'] > 1 + ? ceil($this->numTables / $GLOBALS['cfg']['PropertiesNumColumns']) + 1 + : 0; + $row_count = 0; + $sum_size = 0; + $overhead_size = 0; + + $hidden_fields = []; + $overall_approx_rows = false; + $structure_table_rows = []; + foreach ($this->tables as $keyname => $current_table) { + // Get valid statistics whatever is the table type + + $drop_query = ''; + $drop_message = ''; + $overhead = ''; + $input_class = ['checkall']; + + $table_is_view = false; + // Sets parameters for links + $tbl_url_query = Url::getCommon( + [ + 'db' => $this->db, + 'table' => $current_table['TABLE_NAME'], + ] + ); + // do not list the previous table's size info for a view + + list($current_table, $formatted_size, $unit, $formatted_overhead, + $overhead_unit, $overhead_size, $table_is_view, $sum_size) + = $this->getStuffForEngineTypeTable( + $current_table, + $sum_size, + $overhead_size + ); + + $curTable = $this->dbi + ->getTable($this->db, $current_table['TABLE_NAME']); + if (! $curTable->isMerge()) { + $sum_entries += $current_table['TABLE_ROWS']; + } + + $collationDefinition = '---'; + if (isset($current_table['Collation'])) { + $tableCollation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $current_table['Collation'] + ); + if ($tableCollation !== null) { + $collationDefinition = '' + . $tableCollation->getName() . ''; + } + } + + if ($this->isShowStats) { + $overhead = '-'; + if ($formatted_overhead != '') { + $overhead = '' + . '' . $formatted_overhead . ' ' + . '' . $overhead_unit . '' + . '' . "\n"; + $overhead_check = true; + $input_class[] = 'tbl-overhead'; + } + } + + if ($GLOBALS['cfg']['ShowDbStructureCharset']) { + $charset = ''; + if (isset($tableCollation)) { + $charset = $tableCollation->getCharset(); + } + } + + if ($GLOBALS['cfg']['ShowDbStructureCreation']) { + $create_time = isset($current_table['Create_time']) + ? $current_table['Create_time'] : ''; + if ($create_time + && (! $create_time_all + || $create_time < $create_time_all) + ) { + $create_time_all = $create_time; + } + } + + if ($GLOBALS['cfg']['ShowDbStructureLastUpdate']) { + $update_time = isset($current_table['Update_time']) + ? $current_table['Update_time'] : ''; + if ($update_time + && (! $update_time_all + || $update_time < $update_time_all) + ) { + $update_time_all = $update_time; + } + } + + if ($GLOBALS['cfg']['ShowDbStructureLastCheck']) { + $check_time = isset($current_table['Check_time']) + ? $current_table['Check_time'] : ''; + if ($check_time + && (! $check_time_all + || $check_time < $check_time_all) + ) { + $check_time_all = $check_time; + } + } + + $truename = $current_table['TABLE_NAME']; + + $i++; + + $row_count++; + if ($table_is_view) { + $hidden_fields[] = ''; + } + + /* + * Always activate links for Browse, Search and Empty, even if + * the icons are greyed, because + * 1. for views, we don't know the number of rows at this point + * 2. for tables, another source could have populated them since the + * page was generated + * + * I could have used the PHP ternary conditional operator but I find + * the code easier to read without this operator. + */ + $may_have_rows = $current_table['TABLE_ROWS'] > 0 || $table_is_view; + $titles = Util::buildActionTitles(); + + if (! $this->dbIsSystemSchema) { + $drop_query = sprintf( + 'DROP %s %s', + $table_is_view || $current_table['ENGINE'] == null ? 'VIEW' + : 'TABLE', + Util::backquote( + $current_table['TABLE_NAME'] + ) + ); + $drop_message = sprintf( + ($table_is_view || $current_table['ENGINE'] == null + ? __('View %s has been dropped.') + : __('Table %s has been dropped.')), + str_replace( + ' ', + ' ', + htmlspecialchars($current_table['TABLE_NAME']) + ) + ); + } + + if ($num_columns > 0 + && $this->numTables > $num_columns + && ($row_count % $num_columns) == 0 + ) { + $row_count = 1; + + $html .= $this->template->render('database/structure/table_header', [ + 'db' => $this->db, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'replication' => $GLOBALS['replication_info']['slave']['status'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'is_show_stats' => $GLOBALS['is_show_stats'], + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'structure_table_rows' => $structure_table_rows, + ]); + $structure_table_rows = []; + } + + list($approx_rows, $show_superscript) = $this->isRowCountApproximated( + $current_table, + $table_is_view + ); + + list($do, $ignored) = $this->getReplicationStatus($truename); + + $structure_table_rows[] = [ + 'table_name_hash' => md5($current_table['TABLE_NAME']), + 'db_table_name_hash' => md5($this->db . '.' . $current_table['TABLE_NAME']), + 'db' => $this->db, + 'curr' => $i, + 'input_class' => implode(' ', $input_class), + 'table_is_view' => $table_is_view, + 'current_table' => $current_table, + 'browse_table_title' => $may_have_rows ? $titles['Browse'] : $titles['NoBrowse'], + 'search_table_title' => $may_have_rows ? $titles['Search'] : $titles['NoSearch'], + 'browse_table_label_title' => htmlspecialchars($current_table['TABLE_COMMENT']), + 'browse_table_label_truename' => $truename, + 'empty_table_sql_query' => urlencode( + 'TRUNCATE ' . Util::backquote( + $current_table['TABLE_NAME'] + ) + ), + 'empty_table_message_to_show' => urlencode( + sprintf( + __('Table %s has been emptied.'), + htmlspecialchars( + $current_table['TABLE_NAME'] + ) + ) + ), + 'empty_table_title' => $may_have_rows ? $titles['Empty'] : $titles['NoEmpty'], + 'tracking_icon' => $this->getTrackingIcon($truename), + 'server_slave_status' => $GLOBALS['replication_info']['slave']['status'], + 'tbl_url_query' => $tbl_url_query, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'titles' => $titles, + 'drop_query' => $drop_query, + 'drop_message' => $drop_message, + 'collation' => $collationDefinition, + 'formatted_size' => $formatted_size, + 'unit' => $unit, + 'overhead' => $overhead, + 'create_time' => isset($create_time) && $create_time + ? Util::localisedDate(strtotime($create_time)) : '-', + 'update_time' => isset($update_time) && $update_time + ? Util::localisedDate(strtotime($update_time)) : '-', + 'check_time' => isset($check_time) && $check_time + ? Util::localisedDate(strtotime($check_time)) : '-', + 'charset' => isset($charset) + ? $charset : '', + 'is_show_stats' => $this->isShowStats, + 'ignored' => $ignored, + 'do' => $do, + 'approx_rows' => $approx_rows, + 'show_superscript' => $show_superscript, + 'already_favorite' => $this->checkFavoriteTable( + $current_table['TABLE_NAME'] + ), + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'limit_chars' => $GLOBALS['cfg']['LimitChars'], + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + ]; + + $overall_approx_rows = $overall_approx_rows || $approx_rows; + } + + $databaseCollation = []; + $databaseCharset = ''; + $collation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $this->dbi->getDbCollation($this->db) + ); + if ($collation !== null) { + $databaseCollation = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + $databaseCharset = $collation->getCharset(); + } + + // table form + $html .= $this->template->render('database/structure/table_header', [ + 'db' => $this->db, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'replication' => $GLOBALS['replication_info']['slave']['status'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'is_show_stats' => $GLOBALS['is_show_stats'], + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'structure_table_rows' => $structure_table_rows, + 'body_for_table_summary' => [ + 'num_tables' => $this->numTables, + 'server_slave_status' => $GLOBALS['replication_info']['slave']['status'], + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'sum_entries' => $sum_entries, + 'database_collation' => $databaseCollation, + 'is_show_stats' => $this->isShowStats, + 'database_charset' => $databaseCharset, + 'sum_size' => $sum_size, + 'overhead_size' => $overhead_size, + 'create_time_all' => $create_time_all ? Util::localisedDate(strtotime($create_time_all)) : '-', + 'update_time_all' => $update_time_all ? Util::localisedDate(strtotime($update_time_all)) : '-', + 'check_time_all' => $check_time_all ? Util::localisedDate(strtotime($check_time_all)) : '-', + 'approx_rows' => $overall_approx_rows, + 'num_favorite_tables' => $GLOBALS['cfg']['NumFavoriteTables'], + 'db' => $GLOBALS['db'], + 'properties_num_columns' => $GLOBALS['cfg']['PropertiesNumColumns'], + 'dbi' => $this->dbi, + 'show_charset' => $GLOBALS['cfg']['ShowDbStructureCharset'], + 'show_comment' => $GLOBALS['cfg']['ShowDbStructureComment'], + 'show_creation' => $GLOBALS['cfg']['ShowDbStructureCreation'], + 'show_last_update' => $GLOBALS['cfg']['ShowDbStructureLastUpdate'], + 'show_last_check' => $GLOBALS['cfg']['ShowDbStructureLastCheck'], + ], + 'check_all_tables' => [ + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'overhead_check' => $overhead_check, + 'db_is_system_schema' => $this->dbIsSystemSchema, + 'hidden_fields' => $hidden_fields, + 'disable_multi_table' => $GLOBALS['cfg']['DisableMultiTableMaintenance'], + 'central_columns_work' => $GLOBALS['cfgRelation']['centralcolumnswork'], + ], + ]); + + return $html; + } + + /** + * Returns the tracking icon if the table is tracked + * + * @param string $table table name + * + * @return string HTML for tracking icon + */ + protected function getTrackingIcon(string $table): string + { + $tracking_icon = ''; + if (Tracker::isActive()) { + $is_tracked = Tracker::isTracked($this->db, $table); + if ($is_tracked + || Tracker::getVersion($this->db, $table) > 0 + ) { + $tracking_icon = $this->template->render('database/structure/tracking_icon', [ + 'db' => $this->db, + 'table' => $table, + 'is_tracked' => $is_tracked, + ]); + } + } + return $tracking_icon; + } + + /** + * Returns whether the row count is approximated + * + * @param array $current_table array containing details about the table + * @param boolean $table_is_view whether the table is a view + * + * @return array + */ + protected function isRowCountApproximated( + array $current_table, + bool $table_is_view + ): array { + $approx_rows = false; + $show_superscript = ''; + + // there is a null value in the ENGINE + // - when the table needs to be repaired, or + // - when it's a view + // so ensure that we'll display "in use" below for a table + // that needs to be repaired + if (isset($current_table['TABLE_ROWS']) + && ($current_table['ENGINE'] != null || $table_is_view) + ) { + // InnoDB/TokuDB table: we did not get an accurate row count + $approx_rows = ! $table_is_view + && in_array($current_table['ENGINE'], ['InnoDB', 'TokuDB']) + && ! $current_table['COUNTED']; + + if ($table_is_view + && $current_table['TABLE_ROWS'] >= $GLOBALS['cfg']['MaxExactCountViews'] + ) { + $approx_rows = true; + $show_superscript = Util::showHint( + Sanitize::sanitizeMessage( + sprintf( + __( + 'This view has at least this number of ' + . 'rows. Please refer to %sdocumentation%s.' + ), + '[doc@cfg_MaxExactCountViews]', + '[/doc]' + ) + ) + ); + } + } + + return [ + $approx_rows, + $show_superscript, + ]; + } + + /** + * Returns the replication status of the table. + * + * @param string $table table name + * + * @return array + */ + protected function getReplicationStatus(string $table): array + { + $do = $ignored = false; + if ($GLOBALS['replication_info']['slave']['status']) { + $nbServSlaveDoDb = count( + $GLOBALS['replication_info']['slave']['Do_DB'] + ); + $nbServSlaveIgnoreDb = count( + $GLOBALS['replication_info']['slave']['Ignore_DB'] + ); + $searchDoDBInTruename = array_search( + $table, + $GLOBALS['replication_info']['slave']['Do_DB'] + ); + $searchDoDBInDB = array_search( + $this->db, + $GLOBALS['replication_info']['slave']['Do_DB'] + ); + + $do = (is_string($searchDoDBInTruename) && strlen($searchDoDBInTruename) > 0) + || (is_string($searchDoDBInDB) && strlen($searchDoDBInDB) > 0) + || ($nbServSlaveDoDb == 0 && $nbServSlaveIgnoreDb == 0) + || $this->hasTable( + $GLOBALS['replication_info']['slave']['Wild_Do_Table'], + $table + ); + + $searchDb = array_search( + $this->db, + $GLOBALS['replication_info']['slave']['Ignore_DB'] + ); + $searchTable = array_search( + $table, + $GLOBALS['replication_info']['slave']['Ignore_Table'] + ); + $ignored = (is_string($searchTable) && strlen($searchTable) > 0) + || (is_string($searchDb) && strlen($searchDb) > 0) + || $this->hasTable( + $GLOBALS['replication_info']['slave']['Wild_Ignore_Table'], + $table + ); + } + + return [ + $do, + $ignored, + ]; + } + + /** + * Synchronize favorite tables + * + * + * @param RecentFavoriteTable $favoriteInstance Instance of this class + * @param string $user The user hash + * @param array $favoriteTables Existing favorites + * + * @return array + */ + protected function synchronizeFavoriteTables( + RecentFavoriteTable $favoriteInstance, + string $user, + array $favoriteTables + ): array { + $favoriteInstanceTables = $favoriteInstance->getTables(); + + if (empty($favoriteInstanceTables) + && isset($favoriteTables[$user]) + ) { + foreach ($favoriteTables[$user] as $key => $value) { + $favoriteInstance->add($value['db'], $value['table']); + } + } + $favoriteTables[$user] = $favoriteInstance->getTables(); + + $json = [ + 'favoriteTables' => json_encode($favoriteTables), + 'list' => $favoriteInstance->getHtmlList(), + ]; + $serverId = $GLOBALS['server']; + // Set flag when localStorage and pmadb(if present) are in sync. + $_SESSION['tmpval']['favorites_synced'][$serverId] = true; + + return $json; + } + + /** + * Function to check if a table is already in favorite list. + * + * @param string $currentTable current table + * + * @return bool + */ + protected function checkFavoriteTable(string $currentTable): bool + { + // ensure $_SESSION['tmpval']['favoriteTables'] is initialized + RecentFavoriteTable::getInstance('favorite'); + foreach ($_SESSION['tmpval']['favoriteTables'][$GLOBALS['server']] as $value) { + if ($value['db'] == $this->db && $value['table'] == $currentTable) { + return true; + } + } + return false; + } + + /** + * Find table with truename + * + * @param array $db DB to look into + * @param string $truename Table name + * + * @return bool + */ + protected function hasTable(array $db, $truename) + { + foreach ($db as $db_table) { + if ($this->db == $this->replication->extractDbOrTable($db_table) + && preg_match( + '@^' . + preg_quote(mb_substr($this->replication->extractDbOrTable($db_table, 'table'), 0, -1), '@') . '@', + $truename + ) + ) { + return true; + } + } + return false; + } + + /** + * Get the value set for ENGINE table, + * + * @param array $current_table current table + * @param integer $sum_size total table size + * @param integer $overhead_size overhead size + * + * @return array + * @internal param bool $table_is_view whether table is view or not + */ + protected function getStuffForEngineTypeTable( + array $current_table, + $sum_size, + $overhead_size + ) { + $formatted_size = '-'; + $unit = ''; + $formatted_overhead = ''; + $overhead_unit = ''; + $table_is_view = false; + + switch ($current_table['ENGINE']) { + // MyISAM, ISAM or Heap table: Row count, data size and index size + // are accurate; data size is accurate for ARCHIVE + case 'MyISAM': + case 'ISAM': + case 'HEAP': + case 'MEMORY': + case 'ARCHIVE': + case 'Aria': + case 'Maria': + list($current_table, $formatted_size, $unit, $formatted_overhead, + $overhead_unit, $overhead_size, $sum_size) + = $this->getValuesForAriaTable( + $current_table, + $sum_size, + $overhead_size, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit + ); + break; + case 'InnoDB': + case 'PBMS': + case 'TokuDB': + // InnoDB table: Row count is not accurate but data and index sizes are. + // PBMS table in Drizzle: TABLE_ROWS is taken from table cache, + // so it may be unavailable + list($current_table, $formatted_size, $unit, $sum_size) + = $this->getValuesForInnodbTable( + $current_table, + $sum_size + ); + break; + // Mysql 5.0.x (and lower) uses MRG_MyISAM + // and MySQL 5.1.x (and higher) uses MRG_MYISAM + // Both are aliases for MERGE + case 'MRG_MyISAM': + case 'MRG_MYISAM': + case 'MERGE': + case 'BerkeleyDB': + // Merge or BerkleyDB table: Only row count is accurate. + if ($this->isShowStats) { + $formatted_size = ' - '; + $unit = ''; + } + break; + // for a view, the ENGINE is sometimes reported as null, + // or on some servers it's reported as "SYSTEM VIEW" + case null: + case 'SYSTEM VIEW': + // possibly a view, do nothing + break; + default: + // Unknown table type. + if ($this->isShowStats) { + $formatted_size = __('unknown'); + $unit = ''; + } + } // end switch + + if ($current_table['TABLE_TYPE'] == 'VIEW' + || $current_table['TABLE_TYPE'] == 'SYSTEM VIEW' + ) { + // countRecords() takes care of $cfg['MaxExactCountViews'] + $current_table['TABLE_ROWS'] = $this->dbi + ->getTable($this->db, $current_table['TABLE_NAME']) + ->countRecords(true); + $table_is_view = true; + } + + return [ + $current_table, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit, + $overhead_size, + $table_is_view, + $sum_size, + ]; + } + + /** + * Get values for ARIA/MARIA tables + * + * @param array $current_table current table + * @param integer $sum_size sum size + * @param integer $overhead_size overhead size + * @param integer $formatted_size formatted size + * @param string $unit unit + * @param integer $formatted_overhead overhead formatted + * @param string $overhead_unit overhead unit + * + * @return array + */ + protected function getValuesForAriaTable( + array $current_table, + $sum_size, + $overhead_size, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit + ) { + if ($this->dbIsSystemSchema) { + $current_table['Rows'] = $this->dbi + ->getTable($this->db, $current_table['Name']) + ->countRecords(); + } + + if ($this->isShowStats) { + $tblsize = $current_table['Data_length'] + + $current_table['Index_length']; + $sum_size += $tblsize; + list($formatted_size, $unit) = Util::formatByteDown( + $tblsize, + 3, + $tblsize > 0 ? 1 : 0 + ); + if (isset($current_table['Data_free']) + && $current_table['Data_free'] > 0 + ) { + list($formatted_overhead, $overhead_unit) + = Util::formatByteDown( + $current_table['Data_free'], + 3, + ($current_table['Data_free'] > 0 ? 1 : 0) + ); + $overhead_size += $current_table['Data_free']; + } + } + return [ + $current_table, + $formatted_size, + $unit, + $formatted_overhead, + $overhead_unit, + $overhead_size, + $sum_size, + ]; + } + + /** + * Get values for InnoDB table + * + * @param array $current_table current table + * @param integer $sum_size sum size + * + * @return array + */ + protected function getValuesForInnodbTable( + array $current_table, + $sum_size + ) { + $formatted_size = $unit = ''; + + if ((in_array($current_table['ENGINE'], ['InnoDB', 'TokuDB']) + && $current_table['TABLE_ROWS'] < $GLOBALS['cfg']['MaxExactCount']) + || ! isset($current_table['TABLE_ROWS']) + ) { + $current_table['COUNTED'] = true; + $current_table['TABLE_ROWS'] = $this->dbi + ->getTable($this->db, $current_table['TABLE_NAME']) + ->countRecords(true); + } else { + $current_table['COUNTED'] = false; + } + + if ($this->isShowStats) { + $tblsize = $current_table['Data_length'] + + $current_table['Index_length']; + $sum_size += $tblsize; + list($formatted_size, $unit) = Util::formatByteDown( + $tblsize, + 3, + ($tblsize > 0 ? 1 : 0) + ); + } + + return [ + $current_table, + $formatted_size, + $unit, + $sum_size, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php new file mode 100644 index 0000000..fd47457 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Database/TriggersController.php @@ -0,0 +1,43 @@ +dbi); + $triggers->main(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php b/srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php new file mode 100644 index 0000000..c0fe1ba --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/HomeController.php @@ -0,0 +1,517 @@ +config = $config; + $this->themeManager = $themeManager; + } + + + /** + * @return string HTML + */ + public function index(): string + { + global $cfg, $server, $collation_connection, $message; + + $languageManager = LanguageManager::getInstance(); + + if (! empty($message)) { + $displayMessage = Util::getMessage($message); + unset($message); + } + if (isset($_SESSION['partial_logout'])) { + $partialLogout = Message::success(__( + 'You were logged out from one server, to logout completely ' + . 'from phpMyAdmin, you need to logout from all servers.' + ))->getDisplay(); + unset($_SESSION['partial_logout']); + } + + $syncFavoriteTables = RecentFavoriteTable::getInstance('favorite') + ->getHtmlSyncFavoriteTables(); + + $hasServer = $server > 0 || count($cfg['Servers']) > 1; + if ($hasServer) { + $hasServerSelection = $cfg['ServerDefault'] == 0 + || (! $cfg['NavigationDisplayServers'] + && (count($cfg['Servers']) > 1 + || ($server == 0 && count($cfg['Servers']) === 1))); + if ($hasServerSelection) { + $serverSelection = Select::render(true, true); + } + + if ($server > 0) { + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + if (($cfg['Server']['auth_type'] != 'config') && $cfg['ShowChgPassword']) { + $changePassword = $this->template->render('list/item', [ + 'content' => Util::getImage('s_passwd') . ' ' . __( + 'Change password' + ), + 'id' => 'li_change_password', + 'class' => 'no_bullets', + 'url' => [ + 'href' => 'user_password.php' . Url::getCommon(), + 'target' => null, + 'id' => 'change_password_anchor', + 'class' => 'ajax', + ], + 'mysql_help_page' => null, + ]); + } + + $charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']); + $collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']); + $charsetsList = []; + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $collationsList = []; + /** @var Collation $collation */ + foreach ($collations[$charset->getName()] as $collation) { + $collationsList[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + 'is_selected' => $collation_connection === $collation->getName(), + ]; + } + $charsetsList[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $collationsList, + ]; + } + + $userPreferences = $this->template->render('list/item', [ + 'content' => Util::getImage('b_tblops') . ' ' . __( + 'More settings' + ), + 'id' => 'li_user_preferences', + 'class' => 'no_bullets', + 'url' => [ + 'href' => 'prefs_manage.php' . Url::getCommon(), + 'target' => null, + 'id' => null, + 'class' => null, + ], + 'mysql_help_page' => null, + ]); + } + } + + $languageSelector = ''; + if (empty($cfg['Lang']) && $languageManager->hasChoice()) { + $languageSelector = $languageManager->getSelectorDisplay($this->template); + } + + $themeSelection = ''; + if ($cfg['ThemeManager']) { + $themeSelection = $this->themeManager->getHtmlSelectBox(); + } + + $databaseServer = []; + if ($server > 0 && $cfg['ShowServerInfo']) { + $hostInfo = ''; + if (! empty($cfg['Server']['verbose'])) { + $hostInfo .= $cfg['Server']['verbose']; + if ($cfg['ShowServerInfo']) { + $hostInfo .= ' ('; + } + } + if ($cfg['ShowServerInfo'] || empty($cfg['Server']['verbose'])) { + $hostInfo .= $this->dbi->getHostInfo(); + } + if (! empty($cfg['Server']['verbose']) && $cfg['ShowServerInfo']) { + $hostInfo .= ')'; + } + + $serverCharset = Charsets::getServerCharset($this->dbi, $cfg['Server']['DisableIS']); + $databaseServer = [ + 'host' => $hostInfo, + 'type' => Util::getServerType(), + 'connection' => Util::getServerSSL(), + 'version' => $this->dbi->getVersionString() . ' - ' . $this->dbi->getVersionComment(), + 'protocol' => $this->dbi->getProtoInfo(), + 'user' => $this->dbi->fetchValue('SELECT USER();'), + 'charset' => $serverCharset->getDescription() . ' (' . $serverCharset->getName() . ')', + ]; + } + + $webServer = []; + if ($cfg['ShowServerInfo']) { + $webServer['software'] = $_SERVER['SERVER_SOFTWARE']; + + if ($server > 0) { + $clientVersion = $this->dbi->getClientInfo(); + if (preg_match('#\d+\.\d+\.\d+#', $clientVersion)) { + $clientVersion = 'libmysql - ' . $clientVersion; + } + + $webServer['database'] = $clientVersion; + $webServer['php_extensions'] = Util::listPHPExtensions(); + $webServer['php_version'] = PHP_VERSION; + } + } + if ($cfg['ShowPhpInfo']) { + $phpInfo = $this->template->render('list/item', [ + 'content' => __('Show PHP information'), + 'id' => 'li_phpinfo', + 'class' => null, + 'url' => [ + 'href' => 'phpinfo.php' . Url::getCommon(), + 'target' => '_blank', + 'id' => null, + 'class' => null, + ], + 'mysql_help_page' => null, + ]); + } + + $relation = new Relation($this->dbi); + if ($server > 0) { + $cfgRelation = $relation->getRelationsParam(); + if (! $cfgRelation['allworks'] + && $cfg['PmaNoRelation_DisableWarning'] == false + ) { + $messageText = __( + 'The phpMyAdmin configuration storage is not completely ' + . 'configured, some extended features have been deactivated. ' + . '%sFind out why%s. ' + ); + if ($cfg['ZeroConf'] == true) { + $messageText .= '
' . + __( + 'Or alternately go to \'Operations\' tab of any database ' + . 'to set it up there.' + ); + } + $messageInstance = Message::notice($messageText); + $messageInstance->addParamHtml(''); + $messageInstance->addParamHtml(''); + /* Show error if user has configured something, notice elsewhere */ + if (! empty($cfg['Servers'][$server]['pmadb'])) { + $messageInstance->isError(true); + } + $configStorageMessage = $messageInstance->getDisplay(); + } + } + + $this->checkRequirements(); + + return $this->template->render('home/index', [ + 'message' => $displayMessage ?? '', + 'partial_logout' => $partialLogout ?? '', + 'is_git_revision' => $this->config->isGitRevision(), + 'server' => $server, + 'sync_favorite_tables' => $syncFavoriteTables, + 'has_server' => $hasServer, + 'is_demo' => $cfg['DBG']['demo'], + 'has_server_selection' => $hasServerSelection ?? false, + 'server_selection' => $serverSelection ?? '', + 'change_password' => $changePassword ?? '', + 'charsets' => $charsetsList ?? [], + 'language_selector' => $languageSelector, + 'theme_selection' => $themeSelection, + 'user_preferences' => $userPreferences ?? '', + 'database_server' => $databaseServer, + 'web_server' => $webServer, + 'php_info' => $phpInfo ?? '', + 'is_version_checked' => $cfg['VersionCheck'], + 'phpmyadmin_version' => PMA_VERSION, + 'config_storage_message' => $configStorageMessage ?? '', + ]); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function setTheme(array $params): void + { + $this->themeManager->setActiveTheme($params['set_theme']); + $this->themeManager->setThemeCookie(); + + $userPreferences = new UserPreferences(); + $preferences = $userPreferences->load(); + $preferences['config_data']['ThemeDefault'] = $params['set_theme']; + $userPreferences->save($preferences['config_data']); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function setCollationConnection(array $params): void + { + $this->config->setUserValue( + null, + 'DefaultConnectionCollation', + $params['collation_connection'], + 'utf8mb4_unicode_ci' + ); + } + + /** + * @return array JSON + */ + public function reloadRecentTablesList(): array + { + return [ + 'list' => RecentFavoriteTable::getInstance('recent')->getHtmlList(), + ]; + } + + /** + * @return string HTML + */ + public function gitRevision(): string + { + return (new GitRevision( + $this->response, + $this->config, + $this->template + ))->display(); + } + + /** + * @return void + */ + private function checkRequirements(): void + { + global $cfg, $server, $lang; + + /** + * mbstring is used for handling multibytes inside parser, so it is good + * to tell user something might be broken without it, see bug #1063149. + */ + if (! extension_loaded('mbstring')) { + trigger_error( + __( + 'The mbstring PHP extension was not found and you seem to be using' + . ' a multibyte charset. Without the mbstring extension phpMyAdmin' + . ' is unable to split strings correctly and it may result in' + . ' unexpected results.' + ), + E_USER_WARNING + ); + } + + /** + * Missing functionality + */ + if (! extension_loaded('curl') && ! ini_get('allow_url_fopen')) { + trigger_error( + __( + 'The curl extension was not found and allow_url_fopen is ' + . 'disabled. Due to this some features such as error reporting ' + . 'or version check are disabled.' + ) + ); + } + + if ($cfg['LoginCookieValidityDisableWarning'] == false) { + /** + * Check whether session.gc_maxlifetime limits session validity. + */ + $gc_time = (int) ini_get('session.gc_maxlifetime'); + if ($gc_time < $cfg['LoginCookieValidity']) { + trigger_error( + __( + 'Your PHP parameter [a@https://secure.php.net/manual/en/session.' . + 'configuration.php#ini.session.gc-maxlifetime@_blank]session.' . + 'gc_maxlifetime[/a] is lower than cookie validity configured ' . + 'in phpMyAdmin, because of this, your login might expire sooner ' . + 'than configured in phpMyAdmin.' + ), + E_USER_WARNING + ); + } + } + + /** + * Check whether LoginCookieValidity is limited by LoginCookieStore. + */ + if ($cfg['LoginCookieStore'] != 0 + && $cfg['LoginCookieStore'] < $cfg['LoginCookieValidity'] + ) { + trigger_error( + __( + 'Login cookie store is lower than cookie validity configured in ' . + 'phpMyAdmin, because of this, your login will expire sooner than ' . + 'configured in phpMyAdmin.' + ), + E_USER_WARNING + ); + } + + /** + * Warning if using the default MySQL controluser account + */ + if ($server != 0 + && isset($cfg['Server']['controluser']) && $cfg['Server']['controluser'] == 'pma' + && isset($cfg['Server']['controlpass']) && $cfg['Server']['controlpass'] == 'pmapass' + ) { + trigger_error( + __( + 'Your server is running with default values for the ' . + 'controluser and password (controlpass) and is open to ' . + 'intrusion; you really should fix this security weakness' . + ' by changing the password for controluser \'pma\'.' + ), + E_USER_WARNING + ); + } + + /** + * Check if user does not have defined blowfish secret and it is being used. + */ + if (! empty($_SESSION['encryption_key'])) { + if (empty($cfg['blowfish_secret'])) { + trigger_error( + __( + 'The configuration file now needs a secret passphrase (blowfish_secret).' + ), + E_USER_WARNING + ); + } elseif (strlen($cfg['blowfish_secret']) < 32) { + trigger_error( + __( + 'The secret passphrase in configuration (blowfish_secret) is too short.' + ), + E_USER_WARNING + ); + } + } + + /** + * Check for existence of config directory which should not exist in + * production environment. + */ + if (@file_exists(ROOT_PATH . 'config')) { + trigger_error( + __( + 'Directory [code]config[/code], which is used by the setup script, ' . + 'still exists in your phpMyAdmin directory. It is strongly ' . + 'recommended to remove it once phpMyAdmin has been configured. ' . + 'Otherwise the security of your server may be compromised by ' . + 'unauthorized people downloading your configuration.' + ), + E_USER_WARNING + ); + } + + /** + * Warning about Suhosin only if its simulation mode is not enabled + */ + if ($cfg['SuhosinDisableWarning'] == false + && ini_get('suhosin.request.max_value_length') + && ini_get('suhosin.simulation') == '0' + ) { + trigger_error( + sprintf( + __( + 'Server running with Suhosin. Please refer ' . + 'to %sdocumentation%s for possible issues.' + ), + '[doc@faq1-38]', + '[/doc]' + ), + E_USER_WARNING + ); + } + + /* Missing template cache */ + if ($this->config->getTempDir('twig') === null) { + trigger_error( + sprintf( + __( + 'The $cfg[\'TempDir\'] (%s) is not accessible. ' . + 'phpMyAdmin is not able to cache templates and will ' . + 'be slow because of this.' + ), + $this->config->get('TempDir') + ), + E_USER_WARNING + ); + } + + /** + * Warning about incomplete translations. + * + * The data file is created while creating release by ./scripts/remove-incomplete-mo + */ + if (@file_exists(ROOT_PATH . 'libraries/language_stats.inc.php')) { + include ROOT_PATH . 'libraries/language_stats.inc.php'; + /* + * This message is intentionally not translated, because we're + * handling incomplete translations here and focus on english + * speaking users. + */ + if (isset($GLOBALS['language_stats'][$lang]) + && $GLOBALS['language_stats'][$lang] < $cfg['TranslationWarningThreshold'] + ) { + trigger_error( + 'You are using an incomplete translation, please help to make it ' + . 'better by [a@https://www.phpmyadmin.net/translate/' + . '@_blank]contributing[/a].', + E_USER_NOTICE + ); + } + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php new file mode 100644 index 0000000..859559c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/BinlogController.php @@ -0,0 +1,149 @@ +binaryLogs = $this->dbi->fetchResult( + 'SHOW MASTER LOGS', + 'Log_name', + null, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + } + + /** + * Index action + * + * @param array $params Request params + * + * @return string + */ + public function indexAction(array $params): string + { + global $cfg, $pmaThemeImage; + + include_once ROOT_PATH . 'libraries/server_common.inc.php'; + + $position = ! empty($params['pos']) ? (int) $params['pos'] : 0; + + $urlParams = []; + if (isset($params['log']) + && array_key_exists($params['log'], $this->binaryLogs) + ) { + $urlParams['log'] = $params['log']; + } + + $isFullQuery = false; + if (! empty($params['is_full_query'])) { + $isFullQuery = true; + $urlParams['is_full_query'] = 1; + } + + $sqlQuery = $this->getSqlQuery( + $params['log'] ?? '', + $position, + (int) $cfg['MaxRows'] + ); + $result = $this->dbi->query($sqlQuery); + + $numRows = 0; + if (isset($result) && $result) { + $numRows = $this->dbi->numRows($result); + } + + $previousParams = $urlParams; + $fullQueriesParams = $urlParams; + $nextParams = $urlParams; + if ($position > 0) { + $fullQueriesParams['pos'] = $position; + if ($position > $cfg['MaxRows']) { + $previousParams['pos'] = $position - $cfg['MaxRows']; + } + } + $fullQueriesParams['is_full_query'] = 1; + if ($isFullQuery) { + unset($fullQueriesParams['is_full_query']); + } + if ($numRows >= $cfg['MaxRows']) { + $nextParams['pos'] = $position + $cfg['MaxRows']; + } + + $values = []; + while ($value = $this->dbi->fetchAssoc($result)) { + $values[] = $value; + } + + return $this->template->render('server/binlog/index', [ + 'url_params' => $urlParams, + 'binary_logs' => $this->binaryLogs, + 'log' => $params['log'], + 'sql_message' => Util::getMessage(Message::success(), $sqlQuery), + 'values' => $values, + 'has_previous' => $position > 0, + 'has_next' => $numRows >= $cfg['MaxRows'], + 'previous_params' => $previousParams, + 'full_queries_params' => $fullQueriesParams, + 'next_params' => $nextParams, + 'has_icons' => Util::showIcons('TableNavigationLinksMode'), + 'is_full_query' => $isFullQuery, + 'image_path' => $pmaThemeImage, + ]); + } + + /** + * @param string $log Binary log file name + * @param int $position Position to display + * @param int $maxRows Maximum number of rows + * + * @return string + */ + private function getSqlQuery( + string $log, + int $position, + int $maxRows + ): string { + $sqlQuery = 'SHOW BINLOG EVENTS'; + if (! empty($log)) { + $sqlQuery .= ' IN \'' . $log . '\''; + } + $sqlQuery .= ' LIMIT ' . $position . ', ' . $maxRows; + + return $sqlQuery; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php new file mode 100644 index 0000000..2d806e8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/CollationsController.php @@ -0,0 +1,100 @@ +charsets = $charsets ?? Charsets::getCharsets( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + $this->collations = $collations ?? Charsets::getCollations( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + } + + /** + * Index action + * + * @return string HTML + */ + public function indexAction(): string + { + include_once ROOT_PATH . 'libraries/server_common.inc.php'; + + $charsets = []; + /** @var Charset $charset */ + foreach ($this->charsets as $charset) { + $charsetCollations = []; + /** @var Collation $collation */ + foreach ($this->collations[$charset->getName()] as $collation) { + $charsetCollations[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + 'is_default' => $collation->isDefault(), + ]; + } + + $charsets[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $charsetCollations, + ]; + } + + return $this->template->render('server/collations/index', [ + 'charsets' => $charsets, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php new file mode 100644 index 0000000..a601137 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/DatabasesController.php @@ -0,0 +1,424 @@ +setSortDetails($params['sort_by'], $params['sort_order']); + $this->hasStatistics = ! empty($params['statistics']); + $this->position = ! empty($params['pos']) ? (int) $params['pos'] : 0; + + /** + * Gets the databases list + */ + if ($server > 0) { + $this->databases = $this->dbi->getDatabasesFull( + null, + $this->hasStatistics, + DatabaseInterface::CONNECT_USER, + $this->sortBy, + $this->sortOrder, + $this->position, + true + ); + $this->databaseCount = count($dblist->databases); + } + + $urlParams = [ + 'statistics' => $this->hasStatistics, + 'pos' => $this->position, + 'sort_by' => $this->sortBy, + 'sort_order' => $this->sortOrder, + ]; + + $databases = $this->getDatabases($replication_types ?? []); + + $charsetsList = []; + if ($cfg['ShowCreateDb'] && $is_create_db_priv) { + $charsets = Charsets::getCharsets($this->dbi, $cfg['Server']['DisableIS']); + $collations = Charsets::getCollations($this->dbi, $cfg['Server']['DisableIS']); + $serverCollation = $this->dbi->getServerCollation(); + /** @var Charset $charset */ + foreach ($charsets as $charset) { + $collationsList = []; + /** @var Collation $collation */ + foreach ($collations[$charset->getName()] as $collation) { + $collationsList[] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + 'is_selected' => $serverCollation === $collation->getName(), + ]; + } + $charsetsList[] = [ + 'name' => $charset->getName(), + 'description' => $charset->getDescription(), + 'collations' => $collationsList, + ]; + } + } + + $headerStatistics = $this->getStatisticsColumns(); + + return $this->template->render('server/databases/index', [ + 'is_create_database_shown' => $cfg['ShowCreateDb'], + 'has_create_database_privileges' => $is_create_db_priv, + 'has_statistics' => $this->hasStatistics, + 'database_to_create' => $db_to_create, + 'databases' => $databases['databases'], + 'total_statistics' => $databases['total_statistics'], + 'header_statistics' => $headerStatistics, + 'charsets' => $charsetsList, + 'database_count' => $this->databaseCount, + 'pos' => $this->position, + 'url_params' => $urlParams, + 'max_db_list' => $cfg['MaxDbList'], + 'has_master_replication' => $replication_info['master']['status'], + 'has_slave_replication' => $replication_info['slave']['status'], + 'is_drop_allowed' => $this->dbi->isSuperuser() || $cfg['AllowUserDropDatabase'], + 'default_tab_database' => $cfg['DefaultTabDatabase'], + 'pma_theme_image' => $pmaThemeImage, + 'text_dir' => $text_dir, + ]); + } + + /** + * Handles creating a new database + * + * @param array $params Request parameters + * + * @return array JSON + */ + public function createDatabaseAction(array $params): array + { + global $cfg, $db; + + // lower_case_table_names=1 `DB` becomes `db` + if ($this->dbi->getLowerCaseNames() === '1') { + $params['new_db'] = mb_strtolower( + $params['new_db'] + ); + } + + /** + * Builds and executes the db creation sql query + */ + $sqlQuery = 'CREATE DATABASE ' . Util::backquote($params['new_db']); + if (! empty($params['db_collation'])) { + list($databaseCharset) = explode('_', $params['db_collation']); + $charsets = Charsets::getCharsets( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + $collations = Charsets::getCollations( + $this->dbi, + $cfg['Server']['DisableIS'] + ); + if (in_array($databaseCharset, array_keys($charsets)) + && in_array($params['db_collation'], array_keys($collations[$databaseCharset])) + ) { + $sqlQuery .= ' DEFAULT' + . Util::getCharsetQueryPart($params['db_collation']); + } + } + $sqlQuery .= ';'; + + $result = $this->dbi->tryQuery($sqlQuery); + + if (! $result) { + // avoid displaying the not-created db name in header or navi panel + $db = ''; + + $message = Message::rawError($this->dbi->getError()); + $json = ['message' => $message]; + + $this->response->setRequestStatus(false); + } else { + $db = $params['new_db']; + + $message = Message::success(__('Database %1$s has been created.')); + $message->addParam($params['new_db']); + + $json = [ + 'message' => $message, + 'sql_query' => Util::getMessage(null, $sqlQuery, 'success'), + 'url_query' => Util::getScriptNameForOption( + $cfg['DefaultTabDatabase'], + 'database' + ) . Url::getCommon(['db' => $params['new_db']]), + ]; + } + + return $json; + } + + /** + * Handles dropping multiple databases + * + * @param array $params Request parameters + * + * @return array JSON + */ + public function dropDatabasesAction(array $params): array + { + global $submit_mult, $mult_btn, $selected; + + if (! isset($params['selected_dbs'])) { + $message = Message::error(__('No databases selected.')); + } else { + $action = 'server_databases.php'; + $err_url = $action . Url::getCommon(); + + $submit_mult = 'drop_db'; + $mult_btn = __('Yes'); + + include ROOT_PATH . 'libraries/mult_submits.inc.php'; + + if (empty($message)) { // no error message + $numberOfDatabases = count($selected); + $message = Message::success( + _ngettext( + '%1$d database has been dropped successfully.', + '%1$d databases have been dropped successfully.', + $numberOfDatabases + ) + ); + $message->addParam($numberOfDatabases); + } + } + + $json = []; + if ($message instanceof Message) { + $json = ['message' => $message]; + $this->response->setRequestStatus($message->isSuccess()); + } + + return $json; + } + + /** + * Extracts parameters sort order and sort by + * + * @param string|null $sortBy sort by + * @param string|null $sortOrder sort order + * + * @return void + */ + private function setSortDetails(?string $sortBy, ?string $sortOrder): void + { + if (empty($sortBy)) { + $this->sortBy = 'SCHEMA_NAME'; + } else { + $sortByWhitelist = [ + 'SCHEMA_NAME', + 'DEFAULT_COLLATION_NAME', + 'SCHEMA_TABLES', + 'SCHEMA_TABLE_ROWS', + 'SCHEMA_DATA_LENGTH', + 'SCHEMA_INDEX_LENGTH', + 'SCHEMA_LENGTH', + 'SCHEMA_DATA_FREE', + ]; + $this->sortBy = 'SCHEMA_NAME'; + if (in_array($sortBy, $sortByWhitelist)) { + $this->sortBy = $sortBy; + } + } + + $this->sortOrder = 'asc'; + if (isset($sortOrder) + && mb_strtolower($sortOrder) === 'desc' + ) { + $this->sortOrder = 'desc'; + } + } + + /** + * Returns database list + * + * @param array $replicationTypes replication types + * + * @return array + */ + private function getDatabases(array $replicationTypes): array + { + global $cfg, $replication_info; + + $databases = []; + $totalStatistics = $this->getStatisticsColumns(); + foreach ($this->databases as $database) { + $replication = [ + 'master' => [ + 'status' => $replication_info['master']['status'], + ], + 'slave' => [ + 'status' => $replication_info['slave']['status'], + ], + ]; + foreach ($replicationTypes as $type) { + if ($replication_info[$type]['status']) { + $key = array_search( + $database["SCHEMA_NAME"], + $replication_info[$type]['Ignore_DB'] + ); + if (strlen((string) $key) > 0) { + $replication[$type]['is_replicated'] = false; + } else { + $key = array_search( + $database["SCHEMA_NAME"], + $replication_info[$type]['Do_DB'] + ); + + if (strlen((string) $key) > 0 + || count($replication_info[$type]['Do_DB']) === 0 + ) { + // if ($key != null) did not work for index "0" + $replication[$type]['is_replicated'] = true; + } + } + } + } + + $statistics = $this->getStatisticsColumns(); + if ($this->hasStatistics) { + foreach (array_keys($statistics) as $key) { + $statistics[$key]['raw'] = $database[$key] ?? null; + $totalStatistics[$key]['raw'] += (int) $database[$key] ?? 0; + } + } + + $databases[$database['SCHEMA_NAME']] = [ + 'name' => $database['SCHEMA_NAME'], + 'collation' => [], + 'statistics' => $statistics, + 'replication' => $replication, + 'is_system_schema' => $this->dbi->isSystemSchema( + $database['SCHEMA_NAME'], + true + ), + ]; + $collation = Charsets::findCollationByName( + $this->dbi, + $cfg['Server']['DisableIS'], + $database['DEFAULT_COLLATION_NAME'] + ); + if ($collation !== null) { + $databases[$database['SCHEMA_NAME']]['collation'] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + } + + return [ + 'databases' => $databases, + 'total_statistics' => $totalStatistics, + ]; + } + + /** + * Prepares the statistics columns + * + * @return array + */ + private function getStatisticsColumns(): array + { + return [ + 'SCHEMA_TABLES' => [ + 'title' => __('Tables'), + 'format' => 'number', + 'raw' => 0, + ], + 'SCHEMA_TABLE_ROWS' => [ + 'title' => __('Rows'), + 'format' => 'number', + 'raw' => 0, + ], + 'SCHEMA_DATA_LENGTH' => [ + 'title' => __('Data'), + 'format' => 'byte', + 'raw' => 0, + ], + 'SCHEMA_INDEX_LENGTH' => [ + 'title' => __('Indexes'), + 'format' => 'byte', + 'raw' => 0, + ], + 'SCHEMA_LENGTH' => [ + 'title' => __('Total'), + 'format' => 'byte', + 'raw' => 0, + ], + 'SCHEMA_DATA_FREE' => [ + 'title' => __('Overhead'), + 'format' => 'byte', + 'raw' => 0, + ], + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php new file mode 100644 index 0000000..1170551 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/EnginesController.php @@ -0,0 +1,69 @@ +template->render('server/engines/index', [ + 'engines' => StorageEngine::getStorageEngines(), + ]); + } + + /** + * Displays details about a given Storage Engine + * + * @param array $params Request params + * + * @return string + */ + public function show(array $params): string + { + require ROOT_PATH . 'libraries/server_common.inc.php'; + + $page = $params['page'] ?? ''; + + $engine = []; + if (StorageEngine::isValid($params['engine'])) { + $storageEngine = StorageEngine::getEngine($params['engine']); + $engine = [ + 'engine' => $params['engine'], + 'title' => $storageEngine->getTitle(), + 'help_page' => $storageEngine->getMysqlHelpPage(), + 'comment' => $storageEngine->getComment(), + 'info_pages' => $storageEngine->getInfoPages(), + 'support' => $storageEngine->getSupportInformationMessage(), + 'variables' => $storageEngine->getHtmlVariables(), + 'page' => ! empty($page) ? $storageEngine->getPage($page) : '', + ]; + } + + return $this->template->render('server/engines/show', [ + 'engine' => $engine, + 'page' => $page, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php new file mode 100644 index 0000000..dce48a6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/PluginsController.php @@ -0,0 +1,77 @@ +plugins = $plugins; + } + + /** + * Index action + * + * @return string + */ + public function index(): string + { + include ROOT_PATH . 'libraries/server_common.inc.php'; + + $header = $this->response->getHeader(); + $scripts = $header->getScripts(); + $scripts->addFile('vendor/jquery/jquery.tablesorter.js'); + $scripts->addFile('server/plugins.js'); + + $plugins = []; + $serverPlugins = $this->plugins->getAll(); + foreach ($serverPlugins as $plugin) { + $plugins[$plugin->getType()][] = $plugin->toArray(); + } + ksort($plugins); + + $cleanTypes = []; + foreach (array_keys($plugins) as $type) { + $cleanTypes[$type] = preg_replace( + '/[^a-z]/', + '', + mb_strtolower($type) + ); + } + return $this->template->render('server/plugins/index', [ + 'plugins' => $plugins, + 'clean_types' => $cleanTypes, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php new file mode 100644 index 0000000..dd48411 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/ReplicationController.php @@ -0,0 +1,72 @@ +getHtmlForErrorMessage(); + + if ($replication_info['master']['status']) { + $masterReplicationHtml = $replicationGui->getHtmlForMasterReplication(); + } + + if (isset($params['mr_configure'])) { + $masterConfigurationHtml = $replicationGui->getHtmlForMasterConfiguration(); + } else { + if (! isset($params['repl_clear_scr'])) { + $slaveConfigurationHtml = $replicationGui->getHtmlForSlaveConfiguration( + $replication_info['slave']['status'], + $server_slave_replication + ); + } + if (isset($params['sl_configure'])) { + $changeMasterHtml = $replicationGui->getHtmlForReplicationChangeMaster('slave_changemaster'); + } + } + + return $this->template->render('server/replication/index', [ + 'url_params' => $url_params, + 'is_super_user' => $this->dbi->isSuperuser(), + 'error_messages' => $errorMessages, + 'is_master' => $replication_info['master']['status'], + 'master_configure' => $params['mr_configure'], + 'slave_configure' => $params['sl_configure'], + 'clear_screen' => $params['repl_clear_scr'], + 'master_replication_html' => $masterReplicationHtml ?? '', + 'master_configuration_html' => $masterConfigurationHtml ?? '', + 'slave_configuration_html' => $slaveConfigurationHtml ?? '', + 'change_master_html' => $changeMasterHtml ?? '', + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php new file mode 100644 index 0000000..fd9ead6 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/SqlController.php @@ -0,0 +1,34 @@ +getHtml(); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php new file mode 100644 index 0000000..8d4b51d --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AbstractController.php @@ -0,0 +1,42 @@ +data = $data; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php new file mode 100644 index 0000000..5b93b0e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/AdvisorController.php @@ -0,0 +1,60 @@ +advisor = $advisor; + } + + /** + * @return string + */ + public function index(): string + { + $data = ''; + if ($this->data->dataLoaded) { + $data = json_encode($this->advisor->run()); + } + + return $this->template->render('server/status/advisor/index', [ + 'data' => $data, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php new file mode 100644 index 0000000..1d2cadc --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/MonitorController.php @@ -0,0 +1,146 @@ +monitor = $monitor; + } + + /** + * @return string HTML + */ + public function index(): string + { + $form = [ + 'server_time' => microtime(true) * 1000, + 'server_os' => SysInfo::getOs(), + 'is_superuser' => $this->dbi->isSuperuser(), + 'server_db_isLocal' => $this->data->db_isLocal, + ]; + + $javascriptVariableNames = []; + foreach ($this->data->status as $name => $value) { + if (is_numeric($value)) { + $javascriptVariableNames[] = $name; + } + } + + return $this->template->render('server/status/monitor/index', [ + 'image_path' => $GLOBALS['pmaThemeImage'], + 'javascript_variable_names' => $javascriptVariableNames, + 'form' => $form, + ]); + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function chartingData(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForChartingData( + $params['requiredData'] ?? '' + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function logDataTypeSlow(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForLogDataTypeSlow( + (int) $params['time_start'], + (int) $params['time_end'] + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function logDataTypeGeneral(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForLogDataTypeGeneral( + (int) $params['time_start'], + (int) $params['time_end'], + (bool) $params['limitTypes'], + (bool) $params['removeVariables'] + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function loggingVars(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForLoggingVars( + $params['varName'], + $params['varValue'] + ); + + return $json; + } + + /** + * @param array $params Request parameters + * @return array JSON + */ + public function queryAnalyzer(array $params): array + { + $json = []; + $json['message'] = $this->monitor->getJsonForQueryAnalyzer( + $params['database'] ?? '', + $params['query'] ?? '' + ); + + return $json; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php new file mode 100644 index 0000000..9817e1b --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/ProcessesController.php @@ -0,0 +1,240 @@ + true, + 'full' => $params['full'] ?? '', + 'column_name' => $params['column_name'] ?? '', + 'order_by_field' => $params['order_by_field'] ?? '', + 'sort_order' => $params['sort_order'] ?? '', + ]; + + $serverProcessList = $this->getList($params); + + return $this->template->render('server/status/processes/index', [ + 'url_params' => $urlParams, + 'is_checked' => $isChecked, + 'server_process_list' => $serverProcessList, + ]); + } + + /** + * Only sends the process list table + * + * @param array $params Request parameters + * @return string + */ + public function refresh(array $params): string + { + return $this->getList($params); + } + + /** + * @param array $params Request parameters + * @return array + */ + public function kill(array $params): array + { + $kill = (int) $params['kill']; + $query = $this->dbi->getKillQuery($kill); + + if ($this->dbi->tryQuery($query)) { + $message = Message::success( + __('Thread %s was successfully killed.') + ); + $this->response->setRequestStatus(true); + } else { + $message = Message::error( + __( + 'phpMyAdmin was unable to kill thread %s.' + . ' It probably has already been closed.' + ) + ); + $this->response->setRequestStatus(false); + } + $message->addParam($kill); + + $json = []; + $json['message'] = $message; + + return $json; + } + + /** + * @param array $params Request parameters + * @return string + */ + private function getList(array $params): string + { + $urlParams = []; + + $showFullSql = ! empty($params['full']); + if ($showFullSql) { + $urlParams['full'] = ''; + } else { + $urlParams['full'] = 1; + } + + // This array contains display name and real column name of each + // sortable column in the table + $sortableColumns = [ + [ + 'column_name' => __('ID'), + 'order_by_field' => 'Id', + ], + [ + 'column_name' => __('User'), + 'order_by_field' => 'User', + ], + [ + 'column_name' => __('Host'), + 'order_by_field' => 'Host', + ], + [ + 'column_name' => __('Database'), + 'order_by_field' => 'db', + ], + [ + 'column_name' => __('Command'), + 'order_by_field' => 'Command', + ], + [ + 'column_name' => __('Time'), + 'order_by_field' => 'Time', + ], + [ + 'column_name' => __('Status'), + 'order_by_field' => 'State', + ], + [ + 'column_name' => __('Progress'), + 'order_by_field' => 'Progress', + ], + [ + 'column_name' => __('SQL query'), + 'order_by_field' => 'Info', + ], + ]; + $sortableColCount = count($sortableColumns); + + $sqlQuery = $showFullSql + ? 'SHOW FULL PROCESSLIST' + : 'SHOW PROCESSLIST'; + if ((! empty($params['order_by_field']) + && ! empty($params['sort_order'])) + || ! empty($params['showExecuting']) + ) { + $urlParams['order_by_field'] = $params['order_by_field']; + $urlParams['sort_order'] = $params['sort_order']; + $urlParams['showExecuting'] = $params['showExecuting']; + $sqlQuery = 'SELECT * FROM `INFORMATION_SCHEMA`.`PROCESSLIST` '; + } + if (! empty($params['showExecuting'])) { + $sqlQuery .= ' WHERE state != "" '; + } + if (! empty($params['order_by_field']) && ! empty($params['sort_order'])) { + $sqlQuery .= ' ORDER BY ' + . Util::backquote($params['order_by_field']) + . ' ' . $params['sort_order']; + } + + $result = $this->dbi->query($sqlQuery); + + $columns = []; + foreach ($sortableColumns as $columnKey => $column) { + $is_sorted = ! empty($params['order_by_field']) + && ! empty($params['sort_order']) + && ($params['order_by_field'] == $column['order_by_field']); + + $column['sort_order'] = 'ASC'; + if ($is_sorted && $params['sort_order'] === 'ASC') { + $column['sort_order'] = 'DESC'; + } + if (isset($params['showExecuting'])) { + $column['showExecuting'] = 'on'; + } + + $columns[$columnKey] = [ + 'name' => $column['column_name'], + 'params' => $column, + 'is_sorted' => $is_sorted, + 'sort_order' => $column['sort_order'], + 'has_full_query' => false, + 'is_full' => false, + ]; + + if (0 === --$sortableColCount) { + $columns[$columnKey]['has_full_query'] = true; + if ($showFullSql) { + $columns[$columnKey]['is_full'] = true; + } + } + } + + $rows = []; + while ($process = $this->dbi->fetchAssoc($result)) { + // Array keys need to modify due to the way it has used + // to display column values + if ((! empty($params['order_by_field']) && ! empty($params['sort_order'])) + || ! empty($params['showExecuting']) + ) { + foreach (array_keys($process) as $key) { + $newKey = ucfirst(mb_strtolower($key)); + if ($newKey !== $key) { + $process[$newKey] = $process[$key]; + unset($process[$key]); + } + } + } + + $rows[] = [ + 'id' => $process['Id'], + 'user' => $process['User'], + 'host' => $process['Host'], + 'db' => ! isset($process['db']) || strlen($process['db']) === 0 ? '' : $process['db'], + 'command' => $process['Command'], + 'time' => $process['Time'], + 'state' => ! empty($process['State']) ? $process['State'] : '---', + 'progress' => ! empty($process['Progress']) ? $process['Progress'] : '---', + 'info' => ! empty($process['Info']) ? Util::formatSql( + $process['Info'], + ! $showFullSql + ) : '---', + ]; + } + + return $this->template->render('server/status/processes/list', [ + 'columns' => $columns, + 'rows' => $rows, + 'refresh_params' => $urlParams, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php new file mode 100644 index 0000000..76430d4 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/QueriesController.php @@ -0,0 +1,75 @@ +data->dataLoaded) { + $hourFactor = 3600 / $this->data->status['Uptime']; + $usedQueries = $this->data->used_queries; + $totalQueries = array_sum($usedQueries); + + $stats = [ + 'total' => $totalQueries, + 'per_hour' => $totalQueries * $hourFactor, + 'per_minute' => $totalQueries * 60 / $this->data->status['Uptime'], + 'per_second' => $totalQueries / $this->data->status['Uptime'], + ]; + + // reverse sort by value to show most used statements first + arsort($usedQueries); + + $chart = []; + $querySum = array_sum($usedQueries); + $otherSum = 0; + $queries = []; + foreach ($usedQueries as $key => $value) { + // For the percentage column, use Questions - Connections, because + // the number of connections is not an item of the Query types + // but is included in Questions. Then the total of the percentages is 100. + $name = str_replace(['Com_', '_'], ['', ' '], $key); + // Group together values that make out less than 2% into "Other", but only + // if we have more than 6 fractions already + if ($value < $querySum * 0.02 && count($chart) > 6) { + $otherSum += $value; + } else { + $chart[$name] = $value; + } + + $queries[$key] = [ + 'name' => $name, + 'value' => $value, + 'per_hour' => $value * $hourFactor, + 'percentage' => $value * 100 / $totalQueries, + ]; + } + + if ($otherSum > 0) { + $chart[__('Other')] = $otherSum; + } + } + + return $this->template->render('server/status/queries/index', [ + 'is_data_loaded' => $this->data->dataLoaded, + 'stats' => $stats ?? null, + 'queries' => $queries ?? [], + 'chart' => $chart ?? [], + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php new file mode 100644 index 0000000..53d78a3 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/StatusController.php @@ -0,0 +1,260 @@ +data->dataLoaded) { + $networkTraffic = implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'] + $this->data->status['Bytes_sent'], + 3, + 1 + ) + ); + $uptime = Util::timespanFormat($this->data->status['Uptime']); + $startTime = Util::localisedDate($this->getStartTime()); + + $traffic = $this->getTrafficInfo(); + + $connections = $this->getConnectionsInfo(); + + // display replication information + if ($replication_info['master']['status'] + || $replication_info['slave']['status'] + ) { + $replication = $this->getReplicationInfo($replicationGui); + } + } + + return $this->template->render('server/status/status/index', [ + 'is_data_loaded' => $this->data->dataLoaded, + 'network_traffic' => $networkTraffic ?? null, + 'uptime' => $uptime ?? null, + 'start_time' => $startTime ?? null, + 'traffic' => $traffic, + 'connections' => $connections, + 'is_master' => $replication_info['master']['status'], + 'is_slave' => $replication_info['slave']['status'], + 'replication' => $replication, + ]); + } + + /** + * @return int + */ + private function getStartTime(): int + { + return (int) $this->dbi->fetchValue( + 'SELECT UNIX_TIMESTAMP() - ' . $this->data->status['Uptime'] + ); + } + + /** + * @return array + */ + private function getTrafficInfo(): array + { + $hourFactor = 3600 / $this->data->status['Uptime']; + + return [ + [ + 'name' => __('Received'), + 'number' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'], + 3, + 1 + ) + ), + 'per_hour' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'] * $hourFactor, + 3, + 1 + ) + ), + ], + [ + 'name' => __('Sent'), + 'number' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_sent'], + 3, + 1 + ) + ), + 'per_hour' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_sent'] * $hourFactor, + 3, + 1 + ) + ), + ], + [ + 'name' => __('Total'), + 'number' => implode( + ' ', + Util::formatByteDown( + $this->data->status['Bytes_received'] + $this->data->status['Bytes_sent'], + 3, + 1 + ) + ), + 'per_hour' => implode( + ' ', + Util::formatByteDown( + ($this->data->status['Bytes_received'] + $this->data->status['Bytes_sent']) * $hourFactor, + 3, + 1 + ) + ), + ], + ]; + } + + /** + * @return array + */ + private function getConnectionsInfo(): array + { + $hourFactor = 3600 / $this->data->status['Uptime']; + + $failedAttemptsPercentage = '---'; + $abortedPercentage = '---'; + if ($this->data->status['Connections'] > 0) { + $failedAttemptsPercentage = Util::formatNumber( + $this->data->status['Aborted_connects'] * 100 / $this->data->status['Connections'], + 0, + 2, + true + ) . '%'; + + $abortedPercentage = Util::formatNumber( + $this->data->status['Aborted_clients'] * 100 / $this->data->status['Connections'], + 0, + 2, + true + ) . '%'; + } + + return [ + [ + 'name' => __('Max. concurrent connections'), + 'number' => Util::formatNumber( + $this->data->status['Max_used_connections'], + 0 + ), + 'per_hour' => '---', + 'percentage' => '---', + ], + [ + 'name' => __('Failed attempts'), + 'number' => Util::formatNumber( + $this->data->status['Aborted_connects'], + 4, + 1, + true + ), + 'per_hour' => Util::formatNumber( + $this->data->status['Aborted_connects'] * $hourFactor, + 4, + 2, + true + ), + 'percentage' => $failedAttemptsPercentage, + ], + [ + 'name' => __('Aborted'), + 'number' => Util::formatNumber( + $this->data->status['Aborted_clients'], + 4, + 1, + true + ), + 'per_hour' => Util::formatNumber( + $this->data->status['Aborted_clients'] * $hourFactor, + 4, + 2, + true + ), + 'percentage' => $abortedPercentage, + ], + [ + 'name' => __('Total'), + 'number' => Util::formatNumber( + $this->data->status['Connections'], + 4, + 0 + ), + 'per_hour' => Util::formatNumber( + $this->data->status['Connections'] * $hourFactor, + 4, + 2 + ), + 'percentage' => Util::formatNumber(100, 0, 2) . '%', + ], + ]; + } + + /** + * @param ReplicationGui $replicationGui ReplicationGui instance + * + * @return string + */ + private function getReplicationInfo(ReplicationGui $replicationGui): string + { + global $replication_info, $replication_types; + + $output = ''; + foreach ($replication_types as $type) { + if (isset($replication_info[$type]['status']) + && $replication_info[$type]['status'] + ) { + $output .= $replicationGui->getHtmlForReplicationStatusTable($type); + } + } + + return $output; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php new file mode 100644 index 0000000..a17f15f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/Status/VariablesController.php @@ -0,0 +1,639 @@ +flush($params['flush']); + } + + if ($this->data->dataLoaded) { + $categories = []; + foreach ($this->data->sections as $sectionId => $sectionName) { + if (isset($this->data->sectionUsed[$sectionId])) { + $categories[$sectionId] = [ + 'id' => $sectionId, + 'name' => $sectionName, + 'is_selected' => false, + ]; + if (! empty($params['filterCategory']) + && $params['filterCategory'] === $sectionId + ) { + $categories[$sectionId]['is_selected'] = true; + } + } + } + + $links = []; + foreach ($this->data->links as $sectionName => $sectionLinks) { + $links[$sectionName] = [ + 'name' => 'status_' . $sectionName, + 'links' => $sectionLinks, + ]; + } + + $descriptions = $this->getDescriptions(); + $alerts = $this->getAlerts(); + + $variables = []; + foreach ($this->data->status as $name => $value) { + $variables[$name] = [ + 'name' => $name, + 'value' => $value, + 'is_numeric' => is_numeric($value), + 'class' => $this->data->allocationMap[$name] ?? null, + 'doc' => '', + 'has_alert' => false, + 'is_alert' => false, + 'description' => $descriptions[$name] ?? '', + 'description_doc' => [], + ]; + + // Fields containing % are calculated, + // they can not be described in MySQL documentation + if (mb_strpos($name, '%') === false) { + $variables[$name]['doc'] = Util::linkToVarDocumentation( + $name, + $this->dbi->isMariaDB() + ); + } + + if (isset($alerts[$name])) { + $variables[$name]['has_alert'] = true; + if ($value > $alerts[$name]) { + $variables[$name]['is_alert'] = true; + } + } + + if (isset($this->data->links[$name])) { + foreach ($this->data->links[$name] as $linkName => $linkUrl) { + $variables[$name]['description_doc'][] = [ + 'name' => $linkName, + 'url' => $linkUrl, + ]; + } + } + } + } + + return $this->template->render('server/status/variables/index', [ + 'is_data_loaded' => $this->data->dataLoaded, + 'filter_text' => ! empty($params['filterText']) ? $params['filterText'] : '', + 'is_only_alerts' => ! empty($params['filterAlert']), + 'is_not_formatted' => ! empty($params['dontFormat']), + 'categories' => $categories ?? [], + 'links' => $links ?? [], + 'variables' => $variables ?? [], + ]); + } + + /** + * Flush status variables if requested + * + * @param string $flush Variable name + * @return void + */ + private function flush(string $flush): void + { + $flushCommands = [ + 'STATUS', + 'TABLES', + 'QUERY CACHE', + ]; + + if (in_array($flush, $flushCommands)) { + $this->dbi->query('FLUSH ' . $flush . ';'); + } + } + + /** + * @return array + */ + private function getAlerts(): array + { + // name => max value before alert + return [ + // lower is better + // variable => max value + 'Aborted_clients' => 0, + 'Aborted_connects' => 0, + + 'Binlog_cache_disk_use' => 0, + + 'Created_tmp_disk_tables' => 0, + + 'Handler_read_rnd' => 0, + 'Handler_read_rnd_next' => 0, + + 'Innodb_buffer_pool_pages_dirty' => 0, + 'Innodb_buffer_pool_reads' => 0, + 'Innodb_buffer_pool_wait_free' => 0, + 'Innodb_log_waits' => 0, + 'Innodb_row_lock_time_avg' => 10, // ms + 'Innodb_row_lock_time_max' => 50, // ms + 'Innodb_row_lock_waits' => 0, + + 'Slow_queries' => 0, + 'Delayed_errors' => 0, + 'Select_full_join' => 0, + 'Select_range_check' => 0, + 'Sort_merge_passes' => 0, + 'Opened_tables' => 0, + 'Table_locks_waited' => 0, + 'Qcache_lowmem_prunes' => 0, + + 'Qcache_free_blocks' => + isset($this->data->status['Qcache_total_blocks']) + ? $this->data->status['Qcache_total_blocks'] / 5 + : 0, + 'Slow_launch_threads' => 0, + + // depends on Key_read_requests + // normally lower then 1:0.01 + 'Key_reads' => isset($this->data->status['Key_read_requests']) + ? (0.01 * $this->data->status['Key_read_requests']) : 0, + // depends on Key_write_requests + // normally nearly 1:1 + 'Key_writes' => isset($this->data->status['Key_write_requests']) + ? (0.9 * $this->data->status['Key_write_requests']) : 0, + + 'Key_buffer_fraction' => 0.5, + + // alert if more than 95% of thread cache is in use + 'Threads_cached' => isset($this->data->variables['thread_cache_size']) + ? 0.95 * $this->data->variables['thread_cache_size'] : 0, + + // higher is better + // variable => min value + //'Handler read key' => '> ', + ]; + } + + /** + * Returns a list of variable descriptions + * + * @return array + */ + private function getDescriptions(): array + { + /** + * Messages are built using the message name + */ + return [ + 'Aborted_clients' => __( + 'The number of connections that were aborted because the client died' + . ' without closing the connection properly.' + ), + 'Aborted_connects' => __( + 'The number of failed attempts to connect to the MySQL server.' + ), + 'Binlog_cache_disk_use' => __( + 'The number of transactions that used the temporary binary log cache' + . ' but that exceeded the value of binlog_cache_size and used a' + . ' temporary file to store statements from the transaction.' + ), + 'Binlog_cache_use' => __( + 'The number of transactions that used the temporary binary log cache.' + ), + 'Connections' => __( + 'The number of connection attempts (successful or not)' + . ' to the MySQL server.' + ), + 'Created_tmp_disk_tables' => __( + 'The number of temporary tables on disk created automatically by' + . ' the server while executing statements. If' + . ' Created_tmp_disk_tables is big, you may want to increase the' + . ' tmp_table_size value to cause temporary tables to be' + . ' memory-based instead of disk-based.' + ), + 'Created_tmp_files' => __( + 'How many temporary files mysqld has created.' + ), + 'Created_tmp_tables' => __( + 'The number of in-memory temporary tables created automatically' + . ' by the server while executing statements.' + ), + 'Delayed_errors' => __( + 'The number of rows written with INSERT DELAYED for which some' + . ' error occurred (probably duplicate key).' + ), + 'Delayed_insert_threads' => __( + 'The number of INSERT DELAYED handler threads in use. Every' + . ' different table on which one uses INSERT DELAYED gets' + . ' its own thread.' + ), + 'Delayed_writes' => __( + 'The number of INSERT DELAYED rows written.' + ), + 'Flush_commands' => __( + 'The number of executed FLUSH statements.' + ), + 'Handler_commit' => __( + 'The number of internal COMMIT statements.' + ), + 'Handler_delete' => __( + 'The number of times a row was deleted from a table.' + ), + 'Handler_discover' => __( + 'The MySQL server can ask the NDB Cluster storage engine if it' + . ' knows about a table with a given name. This is called discovery.' + . ' Handler_discover indicates the number of time tables have been' + . ' discovered.' + ), + 'Handler_read_first' => __( + 'The number of times the first entry was read from an index. If this' + . ' is high, it suggests that the server is doing a lot of full' + . ' index scans; for example, SELECT col1 FROM foo, assuming that' + . ' col1 is indexed.' + ), + 'Handler_read_key' => __( + 'The number of requests to read a row based on a key. If this is' + . ' high, it is a good indication that your queries and tables' + . ' are properly indexed.' + ), + 'Handler_read_next' => __( + 'The number of requests to read the next row in key order. This is' + . ' incremented if you are querying an index column with a range' + . ' constraint or if you are doing an index scan.' + ), + 'Handler_read_prev' => __( + 'The number of requests to read the previous row in key order.' + . ' This read method is mainly used to optimize ORDER BY … DESC.' + ), + 'Handler_read_rnd' => __( + 'The number of requests to read a row based on a fixed position.' + . ' This is high if you are doing a lot of queries that require' + . ' sorting of the result. You probably have a lot of queries that' + . ' require MySQL to scan whole tables or you have joins that' + . ' don\'t use keys properly.' + ), + 'Handler_read_rnd_next' => __( + 'The number of requests to read the next row in the data file.' + . ' This is high if you are doing a lot of table scans. Generally' + . ' this suggests that your tables are not properly indexed or that' + . ' your queries are not written to take advantage of the indexes' + . ' you have.' + ), + 'Handler_rollback' => __( + 'The number of internal ROLLBACK statements.' + ), + 'Handler_update' => __( + 'The number of requests to update a row in a table.' + ), + 'Handler_write' => __( + 'The number of requests to insert a row in a table.' + ), + 'Innodb_buffer_pool_pages_data' => __( + 'The number of pages containing data (dirty or clean).' + ), + 'Innodb_buffer_pool_pages_dirty' => __( + 'The number of pages currently dirty.' + ), + 'Innodb_buffer_pool_pages_flushed' => __( + 'The number of buffer pool pages that have been requested' + . ' to be flushed.' + ), + 'Innodb_buffer_pool_pages_free' => __( + 'The number of free pages.' + ), + 'Innodb_buffer_pool_pages_latched' => __( + 'The number of latched pages in InnoDB buffer pool. These are pages' + . ' currently being read or written or that can\'t be flushed or' + . ' removed for some other reason.' + ), + 'Innodb_buffer_pool_pages_misc' => __( + 'The number of pages busy because they have been allocated for' + . ' administrative overhead such as row locks or the adaptive' + . ' hash index. This value can also be calculated as' + . ' Innodb_buffer_pool_pages_total - Innodb_buffer_pool_pages_free' + . ' - Innodb_buffer_pool_pages_data.' + ), + 'Innodb_buffer_pool_pages_total' => __( + 'Total size of buffer pool, in pages.' + ), + 'Innodb_buffer_pool_read_ahead_rnd' => __( + 'The number of "random" read-aheads InnoDB initiated. This happens' + . ' when a query is to scan a large portion of a table but in' + . ' random order.' + ), + 'Innodb_buffer_pool_read_ahead_seq' => __( + 'The number of sequential read-aheads InnoDB initiated. This' + . ' happens when InnoDB does a sequential full table scan.' + ), + 'Innodb_buffer_pool_read_requests' => __( + 'The number of logical read requests InnoDB has done.' + ), + 'Innodb_buffer_pool_reads' => __( + 'The number of logical reads that InnoDB could not satisfy' + . ' from buffer pool and had to do a single-page read.' + ), + 'Innodb_buffer_pool_wait_free' => __( + 'Normally, writes to the InnoDB buffer pool happen in the' + . ' background. However, if it\'s necessary to read or create a page' + . ' and no clean pages are available, it\'s necessary to wait for' + . ' pages to be flushed first. This counter counts instances of' + . ' these waits. If the buffer pool size was set properly, this' + . ' value should be small.' + ), + 'Innodb_buffer_pool_write_requests' => __( + 'The number writes done to the InnoDB buffer pool.' + ), + 'Innodb_data_fsyncs' => __( + 'The number of fsync() operations so far.' + ), + 'Innodb_data_pending_fsyncs' => __( + 'The current number of pending fsync() operations.' + ), + 'Innodb_data_pending_reads' => __( + 'The current number of pending reads.' + ), + 'Innodb_data_pending_writes' => __( + 'The current number of pending writes.' + ), + 'Innodb_data_read' => __( + 'The amount of data read so far, in bytes.' + ), + 'Innodb_data_reads' => __( + 'The total number of data reads.' + ), + 'Innodb_data_writes' => __( + 'The total number of data writes.' + ), + 'Innodb_data_written' => __( + 'The amount of data written so far, in bytes.' + ), + 'Innodb_dblwr_pages_written' => __( + 'The number of pages that have been written for' + . ' doublewrite operations.' + ), + 'Innodb_dblwr_writes' => __( + 'The number of doublewrite operations that have been performed.' + ), + 'Innodb_log_waits' => __( + 'The number of waits we had because log buffer was too small and' + . ' we had to wait for it to be flushed before continuing.' + ), + 'Innodb_log_write_requests' => __( + 'The number of log write requests.' + ), + 'Innodb_log_writes' => __( + 'The number of physical writes to the log file.' + ), + 'Innodb_os_log_fsyncs' => __( + 'The number of fsync() writes done to the log file.' + ), + 'Innodb_os_log_pending_fsyncs' => __( + 'The number of pending log file fsyncs.' + ), + 'Innodb_os_log_pending_writes' => __( + 'Pending log file writes.' + ), + 'Innodb_os_log_written' => __( + 'The number of bytes written to the log file.' + ), + 'Innodb_pages_created' => __( + 'The number of pages created.' + ), + 'Innodb_page_size' => __( + 'The compiled-in InnoDB page size (default 16KB). Many values are' + . ' counted in pages; the page size allows them to be easily' + . ' converted to bytes.' + ), + 'Innodb_pages_read' => __( + 'The number of pages read.' + ), + 'Innodb_pages_written' => __( + 'The number of pages written.' + ), + 'Innodb_row_lock_current_waits' => __( + 'The number of row locks currently being waited for.' + ), + 'Innodb_row_lock_time_avg' => __( + 'The average time to acquire a row lock, in milliseconds.' + ), + 'Innodb_row_lock_time' => __( + 'The total time spent in acquiring row locks, in milliseconds.' + ), + 'Innodb_row_lock_time_max' => __( + 'The maximum time to acquire a row lock, in milliseconds.' + ), + 'Innodb_row_lock_waits' => __( + 'The number of times a row lock had to be waited for.' + ), + 'Innodb_rows_deleted' => __( + 'The number of rows deleted from InnoDB tables.' + ), + 'Innodb_rows_inserted' => __( + 'The number of rows inserted in InnoDB tables.' + ), + 'Innodb_rows_read' => __( + 'The number of rows read from InnoDB tables.' + ), + 'Innodb_rows_updated' => __( + 'The number of rows updated in InnoDB tables.' + ), + 'Key_blocks_not_flushed' => __( + 'The number of key blocks in the key cache that have changed but' + . ' haven\'t yet been flushed to disk. It used to be known as' + . ' Not_flushed_key_blocks.' + ), + 'Key_blocks_unused' => __( + 'The number of unused blocks in the key cache. You can use this' + . ' value to determine how much of the key cache is in use.' + ), + 'Key_blocks_used' => __( + 'The number of used blocks in the key cache. This value is a' + . ' high-water mark that indicates the maximum number of blocks' + . ' that have ever been in use at one time.' + ), + 'Key_buffer_fraction_%' => __( + 'Percentage of used key cache (calculated value)' + ), + 'Key_read_requests' => __( + 'The number of requests to read a key block from the cache.' + ), + 'Key_reads' => __( + 'The number of physical reads of a key block from disk. If Key_reads' + . ' is big, then your key_buffer_size value is probably too small.' + . ' The cache miss rate can be calculated as' + . ' Key_reads/Key_read_requests.' + ), + 'Key_read_ratio_%' => __( + 'Key cache miss calculated as rate of physical reads compared' + . ' to read requests (calculated value)' + ), + 'Key_write_requests' => __( + 'The number of requests to write a key block to the cache.' + ), + 'Key_writes' => __( + 'The number of physical writes of a key block to disk.' + ), + 'Key_write_ratio_%' => __( + 'Percentage of physical writes compared' + . ' to write requests (calculated value)' + ), + 'Last_query_cost' => __( + 'The total cost of the last compiled query as computed by the query' + . ' optimizer. Useful for comparing the cost of different query' + . ' plans for the same query. The default value of 0 means that' + . ' no query has been compiled yet.' + ), + 'Max_used_connections' => __( + 'The maximum number of connections that have been in use' + . ' simultaneously since the server started.' + ), + 'Not_flushed_delayed_rows' => __( + 'The number of rows waiting to be written in INSERT DELAYED queues.' + ), + 'Opened_tables' => __( + 'The number of tables that have been opened. If opened tables is' + . ' big, your table cache value is probably too small.' + ), + 'Open_files' => __( + 'The number of files that are open.' + ), + 'Open_streams' => __( + 'The number of streams that are open (used mainly for logging).' + ), + 'Open_tables' => __( + 'The number of tables that are open.' + ), + 'Qcache_free_blocks' => __( + 'The number of free memory blocks in query cache. High numbers can' + . ' indicate fragmentation issues, which may be solved by issuing' + . ' a FLUSH QUERY CACHE statement.' + ), + 'Qcache_free_memory' => __( + 'The amount of free memory for query cache.' + ), + 'Qcache_hits' => __( + 'The number of cache hits.' + ), + 'Qcache_inserts' => __( + 'The number of queries added to the cache.' + ), + 'Qcache_lowmem_prunes' => __( + 'The number of queries that have been removed from the cache to' + . ' free up memory for caching new queries. This information can' + . ' help you tune the query cache size. The query cache uses a' + . ' least recently used (LRU) strategy to decide which queries' + . ' to remove from the cache.' + ), + 'Qcache_not_cached' => __( + 'The number of non-cached queries (not cachable, or not cached' + . ' due to the query_cache_type setting).' + ), + 'Qcache_queries_in_cache' => __( + 'The number of queries registered in the cache.' + ), + 'Qcache_total_blocks' => __( + 'The total number of blocks in the query cache.' + ), + 'Rpl_status' => __( + 'The status of failsafe replication (not yet implemented).' + ), + 'Select_full_join' => __( + 'The number of joins that do not use indexes. If this value is' + . ' not 0, you should carefully check the indexes of your tables.' + ), + 'Select_full_range_join' => __( + 'The number of joins that used a range search on a reference table.' + ), + 'Select_range_check' => __( + 'The number of joins without keys that check for key usage after' + . ' each row. (If this is not 0, you should carefully check the' + . ' indexes of your tables.)' + ), + 'Select_range' => __( + 'The number of joins that used ranges on the first table. (It\'s' + . ' normally not critical even if this is big.)' + ), + 'Select_scan' => __( + 'The number of joins that did a full scan of the first table.' + ), + 'Slave_open_temp_tables' => __( + 'The number of temporary tables currently' + . ' open by the slave SQL thread.' + ), + 'Slave_retried_transactions' => __( + 'Total (since startup) number of times the replication slave SQL' + . ' thread has retried transactions.' + ), + 'Slave_running' => __( + 'This is ON if this server is a slave that is connected to a master.' + ), + 'Slow_launch_threads' => __( + 'The number of threads that have taken more than slow_launch_time' + . ' seconds to create.' + ), + 'Slow_queries' => __( + 'The number of queries that have taken more than long_query_time' + . ' seconds.' + ), + 'Sort_merge_passes' => __( + 'The number of merge passes the sort algorithm has had to do.' + . ' If this value is large, you should consider increasing the' + . ' value of the sort_buffer_size system variable.' + ), + 'Sort_range' => __( + 'The number of sorts that were done with ranges.' + ), + 'Sort_rows' => __( + 'The number of sorted rows.' + ), + 'Sort_scan' => __( + 'The number of sorts that were done by scanning the table.' + ), + 'Table_locks_immediate' => __( + 'The number of times that a table lock was acquired immediately.' + ), + 'Table_locks_waited' => __( + 'The number of times that a table lock could not be acquired' + . ' immediately and a wait was needed. If this is high, and you have' + . ' performance problems, you should first optimize your queries,' + . ' and then either split your table or tables or use replication.' + ), + 'Threads_cached' => __( + 'The number of threads in the thread cache. The cache hit rate can' + . ' be calculated as Threads_created/Connections. If this value is' + . ' red you should raise your thread_cache_size.' + ), + 'Threads_connected' => __( + 'The number of currently open connections.' + ), + 'Threads_created' => __( + 'The number of threads created to handle connections. If' + . ' Threads_created is big, you may want to increase the' + . ' thread_cache_size value. (Normally this doesn\'t give a notable' + . ' performance improvement if you have a good thread' + . ' implementation.)' + ), + 'Threads_cache_hitrate_%' => __( + 'Thread cache hit rate (calculated value)' + ), + 'Threads_running' => __( + 'The number of threads that are not sleeping.' + ), + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php new file mode 100644 index 0000000..ad10ec8 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Server/VariablesController.php @@ -0,0 +1,238 @@ +response->getHeader(); + $scripts = $header->getScripts(); + $scripts->addFile('server/variables.js'); + + $variables = []; + $serverVarsResult = $this->dbi->tryQuery('SHOW SESSION VARIABLES;'); + if ($serverVarsResult !== false) { + $serverVarsSession = []; + while ($arr = $this->dbi->fetchRow($serverVarsResult)) { + $serverVarsSession[$arr[0]] = $arr[1]; + } + $this->dbi->freeResult($serverVarsResult); + + $serverVars = $this->dbi->fetchResult('SHOW GLOBAL VARIABLES;', 0, 1); + + // list of static (i.e. non-editable) system variables + $staticVariables = KBSearch::getStaticVariables(); + + foreach ($serverVars as $name => $value) { + $hasSessionValue = isset($serverVarsSession[$name]) + && $serverVarsSession[$name] !== $value; + $docLink = Util::linkToVarDocumentation( + $name, + $this->dbi->isMariaDB(), + str_replace('_', ' ', $name) + ); + + list($formattedValue, $isEscaped) = $this->formatVariable($name, $value); + if ($hasSessionValue) { + list($sessionFormattedValue, ) = $this->formatVariable( + $name, + $serverVarsSession[$name] + ); + } + + $variables[] = [ + 'name' => $name, + 'is_editable' => ! in_array(strtolower($name), $staticVariables), + 'doc_link' => $docLink, + 'value' => $formattedValue, + 'is_escaped' => $isEscaped, + 'has_session_value' => $hasSessionValue, + 'session_value' => $sessionFormattedValue ?? null, + ]; + } + } + + return $this->template->render('server/variables/index', [ + 'variables' => $variables, + 'filter_value' => $filterValue, + 'is_superuser' => $this->dbi->isSuperuser(), + 'is_mariadb' => $this->dbi->isMariaDB(), + ]); + } + + /** + * Handle the AJAX request for a single variable value + * + * @param array $params Request parameters + * + * @return array + */ + public function getValue(array $params): array + { + // Send with correct charset + header('Content-Type: text/html; charset=UTF-8'); + // Do not use double quotes inside the query to avoid a problem + // when server is running in ANSI_QUOTES sql_mode + $varValue = $this->dbi->fetchSingleRow( + 'SHOW GLOBAL VARIABLES WHERE Variable_name=\'' + . $this->dbi->escapeString($params['varName']) . '\';', + 'NUM' + ); + + $json = []; + try { + $type = KBSearch::getVariableType($params['varName']); + if ($type === 'byte') { + $json['message'] = implode( + ' ', + Util::formatByteDown($varValue[1], 3, 3) + ); + } else { + throw new KBException("Not a type=byte"); + } + } catch (KBException $e) { + $json['message'] = $varValue[1]; + } + + return $json; + } + + /** + * Handle the AJAX request for setting value for a single variable + * + * @param array $params Request parameters + * + * @return array + */ + public function setValue(array $params): array + { + $value = $params['varValue']; + $matches = []; + try { + $type = KBSearch::getVariableType($params['varName']); + if ($type === 'byte' && preg_match( + '/^\s*(\d+(\.\d+)?)\s*(mb|kb|mib|kib|gb|gib)\s*$/i', + $value, + $matches + )) { + $exp = [ + 'kb' => 1, + 'kib' => 1, + 'mb' => 2, + 'mib' => 2, + 'gb' => 3, + 'gib' => 3, + ]; + $value = floatval($matches[1]) * pow( + 1024, + $exp[mb_strtolower($matches[3])] + ); + } else { + throw new KBException("Not a type=byte or regex not matching"); + } + } catch (KBException $e) { + $value = $this->dbi->escapeString($value); + } + + if (! is_numeric($value)) { + $value = "'" . $value . "'"; + } + + $json = []; + if (! preg_match("/[^a-zA-Z0-9_]+/", $params['varName']) + && $this->dbi->query( + 'SET GLOBAL ' . $params['varName'] . ' = ' . $value + ) + ) { + // Some values are rounded down etc. + $varValue = $this->dbi->fetchSingleRow( + 'SHOW GLOBAL VARIABLES WHERE Variable_name="' + . $this->dbi->escapeString($params['varName']) + . '";', + 'NUM' + ); + list($formattedValue, $isHtmlFormatted) = $this->formatVariable( + $params['varName'], + $varValue[1] + ); + + if ($isHtmlFormatted === false) { + $json['variable'] = htmlspecialchars($formattedValue); + } else { + $json['variable'] = $formattedValue; + } + } else { + $this->response->setRequestStatus(false); + $json['error'] = __('Setting variable failed'); + } + + return $json; + } + + /** + * Format Variable + * + * @param string $name variable name + * @param integer $value variable value + * + * @return array formatted string and bool if string is HTML formatted + */ + private function formatVariable($name, $value) + { + $isHtmlFormatted = false; + $formattedValue = $value; + + if (is_numeric($value)) { + try { + $type = KBSearch::getVariableType($name); + if ($type === 'byte') { + $isHtmlFormatted = true; + $formattedValue = '' + . htmlspecialchars( + implode(' ', Util::formatByteDown($value, 3, 3)) + ) + . ''; + } else { + throw new KBException("Not a type=byte or regex not matching"); + } + } catch (KBException $e) { + $formattedValue = Util::formatNumber($value, 0); + } + } + + return [ + $formattedValue, + $isHtmlFormatted, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php new file mode 100644 index 0000000..8dae377 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/AbstractController.php @@ -0,0 +1,70 @@ +config = $config; + $this->template = $template; + } + + /** + * @return array + */ + protected function getPages(): array + { + $ignored = [ + 'Config', + 'Servers', + ]; + $pages = []; + foreach (SetupFormList::getAll() as $formset) { + if (in_array($formset, $ignored)) { + continue; + } + /** @var BaseForm $formClass */ + $formClass = SetupFormList::get($formset); + + $pages[$formset] = [ + 'name' => $formClass::getName(), + 'formset' => $formset, + ]; + } + + return $pages; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php new file mode 100644 index 0000000..f6e37a2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ConfigController.php @@ -0,0 +1,55 @@ +getPages(); + + $formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']); + + $formTop = $formDisplayTemplate->displayFormTop('config.php'); + $fieldsetTop = $formDisplayTemplate->displayFieldsetTop( + 'config.inc.php', + '', + null, + ['class' => 'simple'] + ); + $formBottom = $formDisplayTemplate->displayFieldsetBottom(false); + $fieldsetBottom = $formDisplayTemplate->displayFormBottom(); + + $config = ConfigGenerator::getConfigFile($this->config); + + return $this->template->render('setup/config/index', [ + 'formset' => $params['formset'] ?? '', + 'pages' => $pages, + 'form_top_html' => $formTop, + 'fieldset_top_html' => $fieldsetTop, + 'form_bottom_html' => $formBottom, + 'fieldset_bottom_html' => $fieldsetBottom, + 'eol' => Core::ifSetOr($params['eol'], 'unix'), + 'config' => $config, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php new file mode 100644 index 0000000..c2caf01 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/FormController.php @@ -0,0 +1,50 @@ +getPages(); + + $formset = Core::isValid($params['formset'], 'scalar') ? $params['formset'] : null; + + /** @var BaseForm $formClass */ + $formClass = SetupFormList::get($formset); + if ($formClass === null) { + Core::fatalError(__('Incorrect form specified!')); + } + + ob_start(); + FormProcessing::process(new $formClass($this->config)); + $page = ob_get_clean(); + + return $this->template->render('setup/form/index', [ + 'formset' => $params['formset'] ?? '', + 'pages' => $pages, + 'name' => $formClass::getName(), + 'page' => $page, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php new file mode 100644 index 0000000..37e3ea2 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/HomeController.php @@ -0,0 +1,228 @@ +getPages(); + + // Handle done action info + $actionDone = Core::isValid($params['action_done'], 'scalar') ? $params['action_done'] : null; + $actionDone = preg_replace('/[^a-z_]/', '', $actionDone); + + // message handling + Index::messagesBegin(); + + // Check phpMyAdmin version + if (isset($params['version_check'])) { + Index::versionCheck(); + } + + // Perform various security, compatibility and consistency checks + $configChecker = new ServerConfigChecks($this->config); + $configChecker->performConfigChecks(); + + $text = __( + 'You are not using a secure connection; all data (including potentially ' + . 'sensitive information, like passwords) is transferred unencrypted!' + ); + $text .= ' '; + $text .= __( + 'If your server is also configured to accept HTTPS requests ' + . 'follow this link to use a secure connection.' + ); + $text .= ''; + Index::messagesSet('notice', 'no_https', __('Insecure connection'), $text); + + // Check for done action info and set notice message if present + switch ($actionDone) { + case 'config_saved': + /* Use uniqid to display this message every time configuration is saved */ + Index::messagesSet( + 'notice', + uniqid('config_saved'), + __('Configuration saved.'), + Sanitize::sanitizeMessage( + __( + 'Configuration saved to file config/config.inc.php in phpMyAdmin ' + . 'top level directory, copy it to top level one and delete ' + . 'directory config to use it.' + ) + ) + ); + break; + case 'config_not_saved': + /* Use uniqid to display this message every time configuration is saved */ + Index::messagesSet( + 'notice', + uniqid('config_not_saved'), + __('Configuration not saved!'), + Sanitize::sanitizeMessage( + __( + 'Please create web server writable folder [em]config[/em] in ' + . 'phpMyAdmin top level directory as described in ' + . '[doc@setup_script]documentation[/doc]. Otherwise you will be ' + . 'only able to download or display it.' + ) + ) + ); + break; + default: + break; + } + + Index::messagesEnd(); + $messages = Index::messagesShowHtml(); + + $formDisplay = new FormDisplay($this->config); + + $defaultLanguageOptions = [ + 'doc' => $formDisplay->getDocLink('DefaultLang'), + 'values' => [], + 'values_escaped' => true, + ]; + + // prepare unfiltered language list + $sortedLanguages = LanguageManager::getInstance()->sortedLanguages(); + $languages = []; + foreach ($sortedLanguages as $language) { + $languages[] = [ + 'code' => $language->getCode(), + 'name' => $language->getName(), + 'is_active' => $language->isActive(), + ]; + $defaultLanguageOptions['values'][$language->getCode()] = $language->getName(); + } + + $serverDefaultOptions = [ + 'doc' => $formDisplay->getDocLink('ServerDefault'), + 'values' => [], + 'values_disabled' => [], + ]; + + $servers = []; + if ($this->config->getServerCount() > 0) { + $serverDefaultOptions['values']['0'] = __('let the user choose'); + $serverDefaultOptions['values']['-'] = '------------------------------'; + if ($this->config->getServerCount() === 1) { + $serverDefaultOptions['values_disabled'][] = '0'; + } + $serverDefaultOptions['values_disabled'][] = '-'; + + foreach ($this->config->getServers() as $id => $server) { + $servers[$id] = [ + 'id' => $id, + 'name' => $this->config->getServerName($id), + 'auth_type' => $this->config->getValue("Servers/$id/auth_type"), + 'dsn' => $this->config->getServerDSN($id), + 'params' => [ + 'token' => $_SESSION[' PMA_token '], + 'edit' => [ + 'page' => 'servers', + 'mode' => 'edit', + 'id' => $id, + ], + 'remove' => [ + 'page' => 'servers', + 'mode' => 'remove', + 'id' => $id, + ], + ], + ]; + $serverDefaultOptions['values'][(string) $id] = $this->config->getServerName($id) . " [$id]"; + } + } else { + $serverDefaultOptions['values']['1'] = __('- none -'); + $serverDefaultOptions['values_escaped'] = true; + } + + $formDisplayTemplate = new FormDisplayTemplate($GLOBALS['PMA_Config']); + $serversFormTopHtml = $formDisplayTemplate->displayFormTop( + 'index.php', + 'get', + [ + 'page' => 'servers', + 'mode' => 'add', + ] + ); + $configFormTopHtml = $formDisplayTemplate->displayFormTop('config.php'); + $formBottomHtml = $formDisplayTemplate->displayFormBottom(); + + $defaultLanguageInput = $formDisplayTemplate->displayInput( + 'DefaultLang', + __('Default language'), + 'select', + $this->config->getValue('DefaultLang'), + '', + true, + $defaultLanguageOptions + ); + $serverDefaultInput = $formDisplayTemplate->displayInput( + 'ServerDefault', + __('Default server'), + 'select', + $this->config->getValue('ServerDefault'), + '', + true, + $serverDefaultOptions + ); + + $eolOptions = [ + 'values' => [ + 'unix' => 'UNIX / Linux (\n)', + 'win' => 'Windows (\r\n)', + ], + 'values_escaped' => true, + ]; + $eol = Core::ifSetOr($_SESSION['eol'], (PMA_IS_WINDOWS ? 'win' : 'unix')); + $eolInput = $formDisplayTemplate->displayInput( + 'eol', + __('End of line'), + 'select', + $eol, + '', + true, + $eolOptions + ); + + return $this->template->render('setup/home/index', [ + 'formset' => $params['formset'] ?? '', + 'languages' => $languages, + 'messages' => $messages, + 'servers_form_top_html' => $serversFormTopHtml, + 'config_form_top_html' => $configFormTopHtml, + 'form_bottom_html' => $formBottomHtml, + 'server_count' => $this->config->getServerCount(), + 'servers' => $servers, + 'default_language_input' => $defaultLanguageInput, + 'server_default_input' => $serverDefaultInput, + 'eol_input' => $eolInput, + 'pages' => $pages, + ]); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php new file mode 100644 index 0000000..eedb94e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Setup/ServersController.php @@ -0,0 +1,66 @@ +getPages(); + + $id = Core::isValid($params['id'], 'numeric') ? (int) $params['id'] : null; + $hasServer = ! empty($id) && $this->config->get("Servers/$id") !== null; + + if (! $hasServer && ($params['mode'] !== 'revert' && $params['mode'] !== 'edit')) { + $id = 0; + } + + ob_start(); + FormProcessing::process(new ServersForm($this->config, $id)); + $page = ob_get_clean(); + + return $this->template->render('setup/servers/index', [ + 'formset' => $params['formset'] ?? '', + 'pages' => $pages, + 'has_server' => $hasServer, + 'mode' => $params['mode'], + 'server_id' => $id, + 'server_dsn' => $this->config->getServerDSN($id), + 'page' => $page, + ]); + } + + /** + * @param array $params Request parameters + * @return void + */ + public function destroy(array $params): void + { + $id = Core::isValid($params['id'], 'numeric') ? (int) $params['id'] : null; + + $hasServer = ! empty($id) && $this->config->get("Servers/$id") !== null; + + if ($hasServer) { + $this->config->removeServer($id); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php new file mode 100644 index 0000000..35f01ac --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/AbstractController.php @@ -0,0 +1,54 @@ +db = $db; + $this->table = $table; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php new file mode 100644 index 0000000..b2c4176 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/ChartController.php @@ -0,0 +1,261 @@ +sql_query = $sql_query; + $this->url_query = $url_query; + $this->cfg = $cfg; + } + + /** + * Execute the query and return the result + * + * @return void + */ + public function indexAction() + { + $response = Response::getInstance(); + if ($response->isAjax() + && isset($_REQUEST['pos']) + && isset($_REQUEST['session_max_rows']) + ) { + $this->ajaxAction(); + return; + } + + // Throw error if no sql query is set + if (! isset($this->sql_query) || $this->sql_query == '') { + $this->response->setRequestStatus(false); + $this->response->addHTML( + Message::error(__('No SQL query was set to fetch data.')) + ); + return; + } + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'chart.js', + 'table/chart.js', + 'vendor/jqplot/jquery.jqplot.js', + 'vendor/jqplot/plugins/jqplot.barRenderer.js', + 'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js', + 'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js', + 'vendor/jqplot/plugins/jqplot.categoryAxisRenderer.js', + 'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js', + 'vendor/jqplot/plugins/jqplot.pointLabels.js', + 'vendor/jqplot/plugins/jqplot.pieRenderer.js', + 'vendor/jqplot/plugins/jqplot.enhancedPieLegendRenderer.js', + 'vendor/jqplot/plugins/jqplot.highlighter.js', + ] + ); + + /** + * Extract values for common work + * @todo Extract common files + */ + $db = &$this->db; + $table = &$this->table; + $url_params = []; + + /** + * Runs common work + */ + if (strlen($this->table) > 0) { + $url_params['goto'] = Util::getScriptNameForOption( + $this->cfg['DefaultTabTable'], + 'table' + ); + $url_params['back'] = 'tbl_sql.php'; + include ROOT_PATH . 'libraries/tbl_common.inc.php'; + $this->dbi->selectDb($GLOBALS['db']); + } elseif (strlen($this->db) > 0) { + $url_params['goto'] = Util::getScriptNameForOption( + $this->cfg['DefaultTabDatabase'], + 'database' + ); + $url_params['back'] = 'sql.php'; + include ROOT_PATH . 'libraries/db_common.inc.php'; + } else { + $url_params['goto'] = Util::getScriptNameForOption( + $this->cfg['DefaultTabServer'], + 'server' + ); + $url_params['back'] = 'sql.php'; + include ROOT_PATH . 'libraries/server_common.inc.php'; + } + + $data = []; + + $result = $this->dbi->tryQuery($this->sql_query); + $fields_meta = $this->dbi->getFieldsMeta($result); + while ($row = $this->dbi->fetchAssoc($result)) { + $data[] = $row; + } + + $keys = array_keys($data[0]); + + $numeric_types = [ + 'int', + 'real', + ]; + $numeric_column_count = 0; + foreach ($keys as $idx => $key) { + if (in_array($fields_meta[$idx]->type, $numeric_types)) { + $numeric_column_count++; + } + } + + if ($numeric_column_count == 0) { + $this->response->setRequestStatus(false); + $this->response->addJSON( + 'message', + __('No numeric columns present in the table to plot.') + ); + return; + } + + $url_params['db'] = $this->db; + $url_params['reload'] = 1; + + /** + * Displays the page + */ + $this->response->addHTML( + $this->template->render('table/chart/tbl_chart', [ + 'url_query' => $this->url_query, + 'url_params' => $url_params, + 'keys' => $keys, + 'fields_meta' => $fields_meta, + 'numeric_types' => $numeric_types, + 'numeric_column_count' => $numeric_column_count, + 'sql_query' => $this->sql_query, + ]) + ); + } + + /** + * Handle ajax request + * + * @return void + */ + public function ajaxAction() + { + /** + * Extract values for common work + * @todo Extract common files + */ + $db = &$this->db; + $table = &$this->table; + + if (strlen($this->table) > 0 && strlen($this->db) > 0) { + include ROOT_PATH . 'libraries/tbl_common.inc.php'; + } + + $parser = new Parser($this->sql_query); + /** + * @var SelectStatement $statement + */ + $statement = $parser->statements[0]; + if (empty($statement->limit)) { + $statement->limit = new Limit( + $_REQUEST['session_max_rows'], + $_REQUEST['pos'] + ); + } else { + $start = $statement->limit->offset + $_REQUEST['pos']; + $rows = min( + $_REQUEST['session_max_rows'], + $statement->limit->rowCount - $_REQUEST['pos'] + ); + $statement->limit = new Limit($rows, $start); + } + $sql_with_limit = $statement->build(); + + $data = []; + $result = $this->dbi->tryQuery($sql_with_limit); + while ($row = $this->dbi->fetchAssoc($result)) { + $data[] = $row; + } + + if (empty($data)) { + $this->response->setRequestStatus(false); + $this->response->addJSON('message', __('No data to display')); + return; + } + $sanitized_data = []; + + foreach ($data as $data_row_number => $data_row) { + $tmp_row = []; + foreach ($data_row as $data_column => $data_value) { + $escaped_value = $data_value === null ? null : htmlspecialchars($data_value); + $tmp_row[htmlspecialchars($data_column)] = $escaped_value; + } + $sanitized_data[] = $tmp_row; + } + $this->response->setRequestStatus(true); + $this->response->addJSON('message', null); + $this->response->addJSON('chartData', json_encode($sanitized_data)); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php new file mode 100644 index 0000000..18e844f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/GisVisualizationController.php @@ -0,0 +1,227 @@ +sql_query = $sql_query; + $this->url_params = $url_params; + $this->url_params['goto'] = $goto; + $this->url_params['back'] = $back; + $this->visualizationSettings = $visualizationSettings; + } + + /** + * Save to file + * + * @return void + */ + public function saveToFileAction() + { + $this->response->disable(); + $file_name = $this->visualizationSettings['spatialColumn']; + $save_format = $_GET['fileFormat']; + $this->visualization->toFile($file_name, $save_format); + } + + /** + * Index + * + * @return void + */ + public function indexAction() + { + // Throw error if no sql query is set + if (! isset($this->sql_query) || $this->sql_query == '') { + $this->response->setRequestStatus(false); + $this->response->addHTML( + Message::error(__('No SQL query was set to fetch data.')) + ); + return; + } + + // Execute the query and return the result + $result = $this->dbi->tryQuery($this->sql_query); + // Get the meta data of results + $meta = $this->dbi->getFieldsMeta($result); + + // Find the candidate fields for label column and spatial column + $labelCandidates = []; + $spatialCandidates = []; + foreach ($meta as $column_meta) { + if ($column_meta->type == 'geometry') { + $spatialCandidates[] = $column_meta->name; + } else { + $labelCandidates[] = $column_meta->name; + } + } + + // Get settings if any posted + if (Core::isValid($_POST['visualizationSettings'], 'array')) { + $this->visualizationSettings = $_POST['visualizationSettings']; + } + + // Check mysql version + $this->visualizationSettings['mysqlVersion'] = $this->dbi->getVersion(); + + if (! isset($this->visualizationSettings['labelColumn']) + && isset($labelCandidates[0]) + ) { + $this->visualizationSettings['labelColumn'] = ''; + } + + // If spatial column is not set, use first geometric column as spatial column + if (! isset($this->visualizationSettings['spatialColumn'])) { + $this->visualizationSettings['spatialColumn'] = $spatialCandidates[0]; + } + + // Convert geometric columns from bytes to text. + $pos = isset($_GET['pos']) ? $_GET['pos'] + : $_SESSION['tmpval']['pos']; + if (isset($_GET['session_max_rows'])) { + $rows = $_GET['session_max_rows']; + } else { + if ($_SESSION['tmpval']['max_rows'] != 'all') { + $rows = $_SESSION['tmpval']['max_rows']; + } else { + $rows = $GLOBALS['cfg']['MaxRows']; + } + } + $this->visualization = GisVisualization::get( + $this->sql_query, + $this->visualizationSettings, + $rows, + $pos + ); + + if (isset($_GET['saveToFile'])) { + $this->saveToFileAction(); + return; + } + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'vendor/openlayers/OpenLayers.js', + 'vendor/jquery/jquery.svg.js', + 'table/gis_visualization.js', + ] + ); + + // If all the rows contain SRID, use OpenStreetMaps on the initial loading. + if (! isset($_POST['displayVisualization'])) { + if ($this->visualization->hasSrid()) { + $this->visualizationSettings['choice'] = 'useBaseLayer'; + } else { + unset($this->visualizationSettings['choice']); + } + } + + $this->visualization->setUserSpecifiedSettings($this->visualizationSettings); + if ($this->visualizationSettings != null) { + foreach ($this->visualization->getSettings() as $setting => $val) { + if (! isset($this->visualizationSettings[$setting])) { + $this->visualizationSettings[$setting] = $val; + } + } + } + + /** + * Displays the page + */ + $this->url_params['sql_query'] = $this->sql_query; + $downloadUrl = 'tbl_gis_visualization.php' . Url::getCommon( + array_merge( + $this->url_params, + [ + 'saveToFile' => true, + 'session_max_rows' => $rows, + 'pos' => $pos, + ] + ) + ); + $html = $this->template->render('table/gis_visualization/gis_visualization', [ + 'url_params' => $this->url_params, + 'download_url' => $downloadUrl, + 'label_candidates' => $labelCandidates, + 'spatial_candidates' => $spatialCandidates, + 'visualization_settings' => $this->visualizationSettings, + 'sql_query' => $this->sql_query, + 'visualization' => $this->visualization->toImage('svg'), + 'draw_ol' => $this->visualization->asOl(), + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + ]); + + $this->response->addHTML($html); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php new file mode 100644 index 0000000..cdbfbb9 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/IndexesController.php @@ -0,0 +1,179 @@ +index = $index; + } + + /** + * Index + * + * @return void + */ + public function indexAction() + { + if (isset($_POST['do_save_data'])) { + $this->doSaveDataAction(); + return; + } // end builds the new index + + $this->displayFormAction(); + } + + /** + * Display the form to edit/create an index + * + * @return void + */ + public function displayFormAction() + { + $this->dbi->selectDb($GLOBALS['db']); + $add_fields = 0; + if (isset($_POST['index']) && is_array($_POST['index'])) { + // coming already from form + if (isset($_POST['index']['columns']['names'])) { + $add_fields = count($_POST['index']['columns']['names']) + - $this->index->getColumnCount(); + } + if (isset($_POST['add_fields'])) { + $add_fields += $_POST['added_fields']; + } + } elseif (isset($_POST['create_index'])) { + $add_fields = $_POST['added_fields']; + } // end preparing form values + + // Get fields and stores their name/type + if (isset($_POST['create_edit_table'])) { + $fields = json_decode($_POST['columns'], true); + $index_params = [ + 'Non_unique' => $_POST['index']['Index_choice'] == 'UNIQUE' + ? '0' : '1', + ]; + $this->index->set($index_params); + $add_fields = count($fields); + } else { + $fields = $this->dbi->getTable($this->db, $this->table) + ->getNameAndTypeOfTheColumns(); + } + + $form_params = [ + 'db' => $this->db, + 'table' => $this->table, + ]; + + if (isset($_POST['create_index'])) { + $form_params['create_index'] = 1; + } elseif (isset($_POST['old_index'])) { + $form_params['old_index'] = $_POST['old_index']; + } elseif (isset($_POST['index'])) { + $form_params['old_index'] = $_POST['index']; + } + + $this->response->getHeader()->getScripts()->addFile('indexes.js'); + + $this->response->addHTML( + $this->template->render('table/index_form', [ + 'fields' => $fields, + 'index' => $this->index, + 'form_params' => $form_params, + 'add_fields' => $add_fields, + 'create_edit_table' => isset($_POST['create_edit_table']), + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + ]) + ); + } + + /** + * Process the data from the edit/create index form, + * run the query to build the new index + * and moves back to "tbl_sql.php" + * + * @return void + */ + public function doSaveDataAction() + { + $error = false; + + $sql_query = $this->dbi->getTable($this->db, $this->table) + ->getSqlQueryForIndexCreateOrEdit($this->index, $error); + + // If there is a request for SQL previewing. + if (isset($_POST['preview_sql'])) { + $this->response->addJSON( + 'sql_data', + $this->template->render('preview_sql', ['query_data' => $sql_query]) + ); + } elseif (! $error) { + $this->dbi->query($sql_query); + $response = Response::getInstance(); + if ($response->isAjax()) { + $message = Message::success( + __('Table %1$s has been altered successfully.') + ); + $message->addParam($this->table); + $this->response->addJSON( + 'message', + Util::getMessage($message, $sql_query, 'success') + ); + $this->response->addJSON( + 'index_table', + Index::getHtmlForIndexes( + $this->table, + $this->db + ) + ); + } else { + include ROOT_PATH . 'tbl_structure.php'; + } + } else { + $this->response->setRequestStatus(false); + $this->response->addJSON('message', $error); + } + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php new file mode 100644 index 0000000..558842c --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/RelationController.php @@ -0,0 +1,398 @@ +options_array = $options_array; + $this->cfgRelation = $cfgRelation; + $this->tbl_storage_engine = $tbl_storage_engine; + $this->existrel = $existrel; + $this->existrel_foreign = $existrel_foreign; + $this->upd_query = $upd_query; + $this->relation = $relation; + } + + /** + * Index + * + * @return void + */ + public function indexAction() + { + // Send table of column names to populate corresponding dropdowns depending + // on the current selection + if (isset($_POST['getDropdownValues']) + && $_POST['getDropdownValues'] === 'true' + ) { + // if both db and table are selected + if (isset($_POST['foreignTable'])) { + $this->getDropdownValueForTableAction(); + } else { // if only the db is selected + $this->getDropdownValueForDbAction(); + } + return; + } + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'table/relation.js', + 'indexes.js', + ] + ); + + // Set the database + $this->dbi->selectDb($this->db); + + // updates for Internal relations + if (isset($_POST['destination_db']) && $this->cfgRelation['relwork']) { + $this->updateForInternalRelationAction(); + } + + // updates for foreign keys + $this->updateForForeignKeysAction(); + + // Updates for display field + if ($this->cfgRelation['displaywork'] && isset($_POST['display_field'])) { + $this->updateForDisplayField(); + } + + // If we did an update, refresh our data + if (isset($_POST['destination_db']) && $this->cfgRelation['relwork']) { + $this->existrel = $this->relation->getForeigners( + $this->db, + $this->table, + '', + 'internal' + ); + } + if (isset($_POST['destination_foreign_db']) + && Util::isForeignKeySupported($this->tbl_storage_engine) + ) { + $this->existrel_foreign = $this->relation->getForeigners( + $this->db, + $this->table, + '', + 'foreign' + ); + } + + /** + * Dialog + */ + // Now find out the columns of our $table + // need to use DatabaseInterface::QUERY_STORE with $this->dbi->numRows() + // in mysqli + $columns = $this->dbi->getColumns($this->db, $this->table); + + $column_array = []; + $column_hash_array = []; + $column_array[''] = ''; + foreach ($columns as $column) { + if (strtoupper($this->tbl_storage_engine) == 'INNODB' + || ! empty($column['Key']) + ) { + $column_array[$column['Field']] = $column['Field']; + $column_hash_array[$column['Field']] = md5($column['Field']); + } + } + if ($GLOBALS['cfg']['NaturalOrder']) { + uksort($column_array, 'strnatcasecmp'); + } + + // common form + $engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine(); + $foreignKeySupported = Util::isForeignKeySupported($this->tbl_storage_engine); + $this->response->addHTML( + $this->template->render('table/relation/common_form', [ + 'is_foreign_key_supported' => Util::isForeignKeySupported($engine), + 'db' => $this->db, + 'table' => $this->table, + 'cfg_relation' => $this->cfgRelation, + 'tbl_storage_engine' => $this->tbl_storage_engine, + 'existrel' => isset($this->existrel) ? $this->existrel : [], + 'existrel_foreign' => is_array($this->existrel_foreign) && array_key_exists('foreign_keys_data', $this->existrel_foreign) + ? $this->existrel_foreign['foreign_keys_data'] : [], + 'options_array' => $this->options_array, + 'column_array' => $column_array, + 'column_hash_array' => $column_hash_array, + 'save_row' => array_values($columns), + 'url_params' => $GLOBALS['url_params'], + 'databases' => $GLOBALS['dblist']->databases, + 'dbi' => $this->dbi, + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + 'foreignKeySupported' => $foreignKeySupported, + 'displayIndexesHtml' => $foreignKeySupported ? Index::getHtmlForDisplayIndexes() : null, + ]) + ); + } + + /** + * Update for display field + * + * @return void + */ + public function updateForDisplayField() + { + if ($this->upd_query->updateDisplayField( + $_POST['display_field'], + $this->cfgRelation + ) + ) { + $this->response->addHTML( + Util::getMessage( + __('Display column was successfully updated.'), + '', + 'success' + ) + ); + } + } + + /** + * Update for FK + * + * @return void + */ + public function updateForForeignKeysAction() + { + $multi_edit_columns_name = isset($_POST['foreign_key_fields_name']) + ? $_POST['foreign_key_fields_name'] + : null; + $preview_sql_data = ''; + $seen_error = false; + + // (for now, one index name only; we keep the definitions if the + // foreign db is not the same) + if (isset($_POST['destination_foreign_db']) + && isset($_POST['destination_foreign_table']) + && isset($_POST['destination_foreign_column'])) { + list($html, $preview_sql_data, $display_query, $seen_error) + = $this->upd_query->updateForeignKeys( + $_POST['destination_foreign_db'], + $multi_edit_columns_name, + $_POST['destination_foreign_table'], + $_POST['destination_foreign_column'], + $this->options_array, + $this->table, + is_array($this->existrel_foreign) && array_key_exists('foreign_keys_data', $this->existrel_foreign) + ? $this->existrel_foreign['foreign_keys_data'] : [] + ); + $this->response->addHTML($html); + } + + // If there is a request for SQL previewing. + if (isset($_POST['preview_sql'])) { + Core::previewSQL($preview_sql_data); + } + + if (! empty($display_query) && ! $seen_error) { + $GLOBALS['display_query'] = $display_query; + $this->response->addHTML( + Util::getMessage( + __('Your SQL query has been executed successfully.'), + null, + 'success' + ) + ); + } + } + + /** + * Update for internal relation + * + * @return void + */ + public function updateForInternalRelationAction() + { + $multi_edit_columns_name = isset($_POST['fields_name']) + ? $_POST['fields_name'] + : null; + + if ($this->upd_query->updateInternalRelations( + $multi_edit_columns_name, + $_POST['destination_db'], + $_POST['destination_table'], + $_POST['destination_column'], + $this->cfgRelation, + isset($this->existrel) ? $this->existrel : null + ) + ) { + $this->response->addHTML( + Util::getMessage( + __('Internal relationships were successfully updated.'), + '', + 'success' + ) + ); + } + } + + /** + * Send table columns for foreign table dropdown + * + * @return void + * + */ + public function getDropdownValueForTableAction() + { + $foreignTable = $_POST['foreignTable']; + $table_obj = $this->dbi->getTable($_POST['foreignDb'], $foreignTable); + // Since views do not have keys defined on them provide the full list of + // columns + if ($table_obj->isView()) { + $columnList = $table_obj->getColumns(false, false); + } else { + $columnList = $table_obj->getIndexedColumns(false, false); + } + $columns = []; + foreach ($columnList as $column) { + $columns[] = htmlspecialchars($column); + } + if ($GLOBALS['cfg']['NaturalOrder']) { + usort($columns, 'strnatcasecmp'); + } + $this->response->addJSON('columns', $columns); + + // @todo should be: $server->db($db)->table($table)->primary() + $primary = Index::getPrimary($foreignTable, $_POST['foreignDb']); + if (false === $primary) { + return; + } + + $this->response->addJSON('primary', array_keys($primary->getColumns())); + } + + /** + * Send database selection values for dropdown + * + * @return void + * + */ + public function getDropdownValueForDbAction() + { + $tables = []; + $foreign = isset($_POST['foreign']) && $_POST['foreign'] === 'true'; + + if ($foreign) { + $query = 'SHOW TABLE STATUS FROM ' + . Util::backquote($_POST['foreignDb']); + $tables_rs = $this->dbi->query( + $query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + + while ($row = $this->dbi->fetchArray($tables_rs)) { + if (isset($row['Engine']) + && mb_strtoupper($row['Engine']) == $this->tbl_storage_engine + ) { + $tables[] = htmlspecialchars($row['Name']); + } + } + } else { + $query = 'SHOW TABLES FROM ' + . Util::backquote($_POST['foreignDb']); + $tables_rs = $this->dbi->query( + $query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + while ($row = $this->dbi->fetchArray($tables_rs)) { + $tables[] = htmlspecialchars($row[0]); + } + } + if ($GLOBALS['cfg']['NaturalOrder']) { + usort($tables, 'strnatcasecmp'); + } + $this->response->addJSON('tables', $tables); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php new file mode 100644 index 0000000..3f2ceae --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SearchController.php @@ -0,0 +1,1244 @@ +url_query = $url_query; + $this->_searchType = $searchType; + $this->_columnNames = []; + $this->_columnNullFlags = []; + $this->_columnTypes = []; + $this->_columnCollations = []; + $this->_geomColumnFlag = false; + $this->_foreigners = []; + $this->relation = $relation; + // Loads table's information + $this->_loadTableInfo(); + $this->_connectionCharSet = $this->dbi->fetchValue( + "SELECT @@character_set_connection" + ); + } + + /** + * Gets all the columns of a table along with their types, collations + * and whether null or not. + * + * @return void + */ + private function _loadTableInfo() + { + // Gets the list and number of columns + $columns = $this->dbi->getColumns( + $this->db, + $this->table, + null, + true + ); + // Get details about the geometry functions + $geom_types = Util::getGISDatatypes(); + + foreach ($columns as $row) { + // set column name + $this->_columnNames[] = $row['Field']; + + $type = $row['Type']; + // check whether table contains geometric columns + if (in_array($type, $geom_types)) { + $this->_geomColumnFlag = true; + } + // reformat mysql query output + if (strncasecmp($type, 'set', 3) == 0 + || strncasecmp($type, 'enum', 4) == 0 + ) { + $type = str_replace(',', ', ', $type); + } else { + // strip the "BINARY" attribute, except if we find "BINARY(" because + // this would be a BINARY or VARBINARY column type + if (! preg_match('@BINARY[\(]@i', $type)) { + $type = str_ireplace("BINARY", '', $type); + } + $type = str_ireplace("ZEROFILL", '', $type); + $type = str_ireplace("UNSIGNED", '', $type); + $type = mb_strtolower($type); + } + if (empty($type)) { + $type = ' '; + } + $this->_columnTypes[] = $type; + $this->_columnNullFlags[] = $row['Null']; + $this->_columnCollations[] + = ! empty($row['Collation']) && $row['Collation'] != 'NULL' + ? $row['Collation'] + : ''; + } // end for + + // Retrieve foreign keys + $this->_foreigners = $this->relation->getForeigners($this->db, $this->table); + } + + /** + * Index action + * + * @return void + */ + public function indexAction() + { + global $goto; + switch ($this->_searchType) { + case 'replace': + if (isset($_POST['find'])) { + $this->findAction(); + + return; + } + $this->response + ->getHeader() + ->getScripts() + ->addFile('table/find_replace.js'); + + if (isset($_POST['replace'])) { + $this->replaceAction(); + } + + // Displays the find and replace form + $this->displaySelectionFormAction(); + break; + + case 'normal': + $this->response->getHeader() + ->getScripts() + ->addFiles( + [ + 'makegrid.js', + 'sql.js', + 'table/select.js', + 'table/change.js', + 'vendor/jquery/jquery.uitablefilter.js', + 'gis_data_editor.js', + ] + ); + + if (isset($_POST['range_search'])) { + $this->rangeSearchAction(); + + return; + } + + /** + * No selection criteria received -> display the selection form + */ + if (! isset($_POST['columnsToDisplay']) + && ! isset($_POST['displayAllColumns']) + ) { + $this->displaySelectionFormAction(); + } else { + $this->doSelectionAction(); + } + break; + + case 'zoom': + $this->response->getHeader() + ->getScripts() + ->addFiles( + [ + 'makegrid.js', + 'sql.js', + 'vendor/jqplot/jquery.jqplot.js', + 'vendor/jqplot/plugins/jqplot.canvasTextRenderer.js', + 'vendor/jqplot/plugins/jqplot.canvasAxisLabelRenderer.js', + 'vendor/jqplot/plugins/jqplot.dateAxisRenderer.js', + 'vendor/jqplot/plugins/jqplot.highlighter.js', + 'vendor/jqplot/plugins/jqplot.cursor.js', + 'table/zoom_plot_jqplot.js', + 'table/change.js', + ] + ); + + /** + * Handle AJAX request for data row on point select + * + * @var boolean Object containing parameters for the POST request + */ + if (isset($_POST['get_data_row']) + && $_POST['get_data_row'] == true + ) { + $this->getDataRowAction(); + + return; + } + /** + * Handle AJAX request for changing field information + * (value,collation,operators,field values) in input form + * + * @var boolean Object containing parameters for the POST request + */ + if (isset($_POST['change_tbl_info']) + && $_POST['change_tbl_info'] == true + ) { + $this->changeTableInfoAction(); + + return; + } + + //Set default datalabel if not selected + if (! isset($_POST['zoom_submit']) || $_POST['dataLabel'] == '') { + $dataLabel = $this->relation->getDisplayField($this->db, $this->table); + } else { + $dataLabel = $_POST['dataLabel']; + } + + // Displays the zoom search form + $this->displaySelectionFormAction($dataLabel); + + /* + * Handle the input criteria and generate the query result + * Form for displaying query results + */ + if (isset($_POST['zoom_submit']) + && $_POST['criteriaColumnNames'][0] != 'pma_null' + && $_POST['criteriaColumnNames'][1] != 'pma_null' + && $_POST['criteriaColumnNames'][0] != $_POST['criteriaColumnNames'][1] + ) { + if (! isset($goto)) { + $goto = Util::getScriptNameForOption( + $GLOBALS['cfg']['DefaultTabTable'], + 'table' + ); + } + $this->zoomSubmitAction($dataLabel, $goto); + } + break; + } + } + + /** + * Zoom submit action + * + * @param string $dataLabel Data label + * @param string $goto Goto + * + * @return void + */ + public function zoomSubmitAction($dataLabel, $goto) + { + //Query generation part + $sql_query = $this->_buildSqlQuery(); + $sql_query .= ' LIMIT ' . $_POST['maxPlotLimit']; + + //Query execution part + $result = $this->dbi->query( + $sql_query . ";", + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $fields_meta = $this->dbi->getFieldsMeta($result); + $data = []; + while ($row = $this->dbi->fetchAssoc($result)) { + //Need a row with indexes as 0,1,2 for the getUniqueCondition + // hence using a temporary array + $tmpRow = []; + foreach ($row as $val) { + $tmpRow[] = $val; + } + //Get unique condition on each row (will be needed for row update) + $uniqueCondition = Util::getUniqueCondition( + $result, // handle + count($this->_columnNames), // fields_cnt + $fields_meta, // fields_meta + $tmpRow, // row + true, // force_unique + false, // restrict_to_table + null // analyzed_sql_results + ); + //Append it to row array as where_clause + $row['where_clause'] = $uniqueCondition[0]; + + $tmpData = [ + $_POST['criteriaColumnNames'][0] => + $row[$_POST['criteriaColumnNames'][0]], + $_POST['criteriaColumnNames'][1] => + $row[$_POST['criteriaColumnNames'][1]], + 'where_clause' => $uniqueCondition[0], + ]; + $tmpData[$dataLabel] = $dataLabel ? $row[$dataLabel] : ''; + $data[] = $tmpData; + } + unset($tmpData); + + //Displays form for point data and scatter plot + $titles = [ + 'Browse' => Util::getIcon( + 'b_browse', + __('Browse foreign values') + ), + ]; + $column_names_hashes = []; + + foreach ($this->_columnNames as $columnName) { + $column_names_hashes[$columnName] = md5($columnName); + } + + $this->response->addHTML( + $this->template->render('table/search/zoom_result_form', [ + 'db' => $this->db, + 'table' => $this->table, + 'column_names' => $this->_columnNames, + 'column_names_hashes' => $column_names_hashes, + 'foreigners' => $this->_foreigners, + 'column_null_flags' => $this->_columnNullFlags, + 'column_types' => $this->_columnTypes, + 'titles' => $titles, + 'goto' => $goto, + 'data' => $data, + 'data_json' => json_encode($data), + 'zoom_submit' => isset($_POST['zoom_submit']), + 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'], + ]) + ); + } + + /** + * Change table info action + * + * @return void + */ + public function changeTableInfoAction() + { + $field = $_POST['field']; + if ($field == 'pma_null') { + $this->response->addJSON('field_type', ''); + $this->response->addJSON('field_collation', ''); + $this->response->addJSON('field_operators', ''); + $this->response->addJSON('field_value', ''); + return; + } + $key = array_search($field, $this->_columnNames); + $search_index + = (isset($_POST['it']) && is_numeric($_POST['it']) + ? intval($_POST['it']) : 0); + + $properties = $this->getColumnProperties($search_index, $key); + $this->response->addJSON( + 'field_type', + htmlspecialchars($properties['type']) + ); + $this->response->addJSON('field_collation', $properties['collation']); + $this->response->addJSON('field_operators', $properties['func']); + $this->response->addJSON('field_value', $properties['value']); + } + + /** + * Get data row action + * + * @return void + */ + public function getDataRowAction() + { + $extra_data = []; + $row_info_query = 'SELECT * FROM `' . $_POST['db'] . '`.`' + . $_POST['table'] . '` WHERE ' . $_POST['where_clause']; + $result = $this->dbi->query( + $row_info_query . ";", + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $fields_meta = $this->dbi->getFieldsMeta($result); + while ($row = $this->dbi->fetchAssoc($result)) { + // for bit fields we need to convert them to printable form + $i = 0; + foreach ($row as $col => $val) { + if ($fields_meta[$i]->type == 'bit') { + $row[$col] = Util::printableBitValue( + (int) $val, + (int) $fields_meta[$i]->length + ); + } + $i++; + } + $extra_data['row_info'] = $row; + } + $this->response->addJSON($extra_data); + } + + /** + * Do selection action + * + * @return void + */ + public function doSelectionAction() + { + /** + * Selection criteria have been submitted -> do the work + */ + $sql_query = $this->_buildSqlQuery(); + + /** + * Add this to ensure following procedures included running correctly. + */ + $sql = new Sql(); + $sql->executeQueryAndSendQueryResponse( + null, // analyzed_sql_results + false, // is_gotofile + $this->db, // db + $this->table, // table + null, // find_real_end + null, // sql_query_for_bookmark + null, // extra_data + null, // message_to_show + null, // message + null, // sql_data + $GLOBALS['goto'], // goto + $GLOBALS['pmaThemeImage'], // pmaThemeImage + null, // disp_query + null, // disp_message + null, // query_type + $sql_query, // sql_query + null, // selectedTables + null // complete_query + ); + } + + /** + * Display selection form action + * + * @param string $dataLabel Data label + * + * @return void + */ + public function displaySelectionFormAction($dataLabel = null) + { + global $goto; + $this->url_query .= '&goto=tbl_select.php&back=tbl_select.php'; + if (! isset($goto)) { + $goto = Util::getScriptNameForOption( + $GLOBALS['cfg']['DefaultTabTable'], + 'table' + ); + } + // Displays the table search form + $this->response->addHTML( + $this->template->render('secondary_tabs', [ + 'url_params' => [ + 'db' => $this->db, + 'table' => $this->table, + ], + 'sub_tabs' => $this->_getSubTabs(), + ]) + ); + + $column_names = $this->_columnNames; + $column_types = $this->_columnTypes; + $types = []; + if ($this->_searchType == 'replace') { + $num_cols = count($column_names); + for ($i = 0; $i < $num_cols; $i++) { + $types[$column_names[$i]] = preg_replace('@\\(.*@s', '', $column_types[$i]); + } + } + + $criteria_column_names = isset($_POST['criteriaColumnNames']) ? $_POST['criteriaColumnNames'] : null; + $keys = []; + for ($i = 0; $i < 4; $i++) { + if (isset($criteria_column_names[$i])) { + if ($criteria_column_names[$i] != 'pma_null') { + $keys[$criteria_column_names[$i]] = array_search($criteria_column_names[$i], $column_names); + } + } + } + + $this->response->addHTML( + $this->template->render('table/search/selection_form', [ + 'search_type' => $this->_searchType, + 'db' => $this->db, + 'table' => $this->table, + 'goto' => $goto, + 'self' => $this, + 'geom_column_flag' => $this->_geomColumnFlag, + 'column_names' => $column_names, + 'column_types' => $column_types, + 'types' => $types, + 'column_collations' => $this->_columnCollations, + 'data_label' => $dataLabel, + 'keys' => $keys, + 'criteria_column_names' => $criteria_column_names, + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + 'criteria_column_types' => isset($_POST['criteriaColumnTypes']) ? $_POST['criteriaColumnTypes'] : null, + 'sql_types' => $this->dbi->types, + 'max_rows' => intval($GLOBALS['cfg']['MaxRows']), + 'max_plot_limit' => ! empty($_POST['maxPlotLimit']) + ? intval($_POST['maxPlotLimit']) + : intval($GLOBALS['cfg']['maxRowPlotLimit']), + ]) + ); + } + + /** + * Range search action + * + * @return void + */ + public function rangeSearchAction() + { + $min_max = $this->getColumnMinMax($_POST['column']); + $this->response->addJSON('column_data', $min_max); + } + + /** + * Find action + * + * @return void + */ + public function findAction() + { + $useRegex = array_key_exists('useRegex', $_POST) + && $_POST['useRegex'] == 'on'; + + $preview = $this->getReplacePreview( + $_POST['columnIndex'], + $_POST['find'], + $_POST['replaceWith'], + $useRegex, + $this->_connectionCharSet + ); + $this->response->addJSON('preview', $preview); + } + + /** + * Replace action + * + * @return void + */ + public function replaceAction() + { + $this->replace( + $_POST['columnIndex'], + $_POST['findString'], + $_POST['replaceWith'], + $_POST['useRegex'], + $this->_connectionCharSet + ); + $this->response->addHTML( + Util::getMessage( + __('Your SQL query has been executed successfully.'), + null, + 'success' + ) + ); + } + + /** + * Returns HTML for previewing strings found and their replacements + * + * @param int $columnIndex index of the column + * @param string $find string to find in the column + * @param string $replaceWith string to replace with + * @param boolean $useRegex to use Regex replace or not + * @param string $charSet character set of the connection + * + * @return string HTML for previewing strings found and their replacements + */ + public function getReplacePreview( + $columnIndex, + $find, + $replaceWith, + $useRegex, + $charSet + ) { + $column = $this->_columnNames[$columnIndex]; + if ($useRegex) { + $result = $this->_getRegexReplaceRows( + $columnIndex, + $find, + $replaceWith, + $charSet + ); + } else { + $sql_query = "SELECT " + . Util::backquote($column) . "," + . " REPLACE(" + . Util::backquote($column) . ", '" . $find . "', '" + . $replaceWith + . "')," + . " COUNT(*)" + . " FROM " . Util::backquote($this->db) + . "." . Util::backquote($this->table) + . " WHERE " . Util::backquote($column) + . " LIKE '%" . $find . "%' COLLATE " . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison + // is case sensitive + $sql_query .= " GROUP BY " . Util::backquote($column) + . " ORDER BY " . Util::backquote($column) . " ASC"; + + $result = $this->dbi->fetchResult($sql_query, 0); + } + + return $this->template->render('table/search/replace_preview', [ + 'db' => $this->db, + 'table' => $this->table, + 'column_index' => $columnIndex, + 'find' => $find, + 'replace_with' => $replaceWith, + 'use_regex' => $useRegex, + 'result' => $result, + ]); + } + + /** + * Finds and returns Regex pattern and their replacements + * + * @param int $columnIndex index of the column + * @param string $find string to find in the column + * @param string $replaceWith string to replace with + * @param string $charSet character set of the connection + * + * @return array|bool Array containing original values, replaced values and count + */ + private function _getRegexReplaceRows( + $columnIndex, + $find, + $replaceWith, + $charSet + ) { + $column = $this->_columnNames[$columnIndex]; + $sql_query = "SELECT " + . Util::backquote($column) . "," + . " 1," // to add an extra column that will have replaced value + . " COUNT(*)" + . " FROM " . Util::backquote($this->db) + . "." . Util::backquote($this->table) + . " WHERE " . Util::backquote($column) + . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE " + . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison is case sensitive + $sql_query .= " GROUP BY " . Util::backquote($column) + . " ORDER BY " . Util::backquote($column) . " ASC"; + + $result = $this->dbi->fetchResult($sql_query, 0); + + if (is_array($result)) { + /* Iterate over possible delimiters to get one */ + $delimiters = [ + '/', + '@', + '#', + '~', + '!', + '$', + '%', + '^', + '&', + '_', + ]; + $found = false; + for ($i = 0, $l = count($delimiters); $i < $l; $i++) { + if (strpos($find, $delimiters[$i]) === false) { + $found = true; + break; + } + } + if (! $found) { + return false; + } + $find = $delimiters[$i] . $find . $delimiters[$i]; + foreach ($result as $index => $row) { + $result[$index][1] = preg_replace( + $find, + $replaceWith, + $row[0] + ); + } + } + return $result; + } + + /** + * Replaces a given string in a column with a give replacement + * + * @param int $columnIndex index of the column + * @param string $find string to find in the column + * @param string $replaceWith string to replace with + * @param boolean $useRegex to use Regex replace or not + * @param string $charSet character set of the connection + * + * @return void + */ + public function replace( + $columnIndex, + $find, + $replaceWith, + $useRegex, + $charSet + ) { + $column = $this->_columnNames[$columnIndex]; + if ($useRegex) { + $toReplace = $this->_getRegexReplaceRows( + $columnIndex, + $find, + $replaceWith, + $charSet + ); + $sql_query = "UPDATE " . Util::backquote($this->table) + . " SET " . Util::backquote($column) . " = CASE"; + if (is_array($toReplace)) { + foreach ($toReplace as $row) { + $sql_query .= "\n WHEN " . Util::backquote($column) + . " = '" . $this->dbi->escapeString($row[0]) + . "' THEN '" . $this->dbi->escapeString($row[1]) . "'"; + } + } + $sql_query .= " END" + . " WHERE " . Util::backquote($column) + . " RLIKE '" . $this->dbi->escapeString($find) . "' COLLATE " + . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison + // is case sensitive + } else { + $sql_query = "UPDATE " . Util::backquote($this->table) + . " SET " . Util::backquote($column) . " =" + . " REPLACE(" + . Util::backquote($column) . ", '" . $find . "', '" + . $replaceWith + . "')" + . " WHERE " . Util::backquote($column) + . " LIKE '%" . $find . "%' COLLATE " . $charSet . "_bin"; // here we + // change the collation of the 2nd operand to a case sensitive + // binary collation to make sure that the comparison + // is case sensitive + } + $this->dbi->query( + $sql_query, + DatabaseInterface::CONNECT_USER, + DatabaseInterface::QUERY_STORE + ); + $GLOBALS['sql_query'] = $sql_query; + } + + /** + * Finds minimum and maximum value of a given column. + * + * @param string $column Column name + * + * @return array + */ + public function getColumnMinMax($column) + { + $sql_query = 'SELECT MIN(' . Util::backquote($column) . ') AS `min`, ' + . 'MAX(' . Util::backquote($column) . ') AS `max` ' + . 'FROM ' . Util::backquote($this->db) . '.' + . Util::backquote($this->table); + + return $this->dbi->fetchSingleRow($sql_query); + } + + /** + * Returns an array with necessary configurations to create + * sub-tabs in the table_select page. + * + * @return array Array containing configuration (icon, text, link, id, args) + * of sub-tabs + */ + private function _getSubTabs() + { + $subtabs = []; + $subtabs['search']['icon'] = 'b_search'; + $subtabs['search']['text'] = __('Table search'); + $subtabs['search']['link'] = 'tbl_select.php'; + $subtabs['search']['id'] = 'tbl_search_id'; + $subtabs['search']['args']['pos'] = 0; + + $subtabs['zoom']['icon'] = 'b_select'; + $subtabs['zoom']['link'] = 'tbl_zoom_select.php'; + $subtabs['zoom']['text'] = __('Zoom search'); + $subtabs['zoom']['id'] = 'zoom_search_id'; + + $subtabs['replace']['icon'] = 'b_find_replace'; + $subtabs['replace']['link'] = 'tbl_find_replace.php'; + $subtabs['replace']['text'] = __('Find and replace'); + $subtabs['replace']['id'] = 'find_replace_id'; + + return $subtabs; + } + + /** + * Builds the sql search query from the post parameters + * + * @return string the generated SQL query + */ + private function _buildSqlQuery() + { + $sql_query = 'SELECT '; + + // If only distinct values are needed + $is_distinct = isset($_POST['distinct']) ? 'true' : 'false'; + if ($is_distinct == 'true') { + $sql_query .= 'DISTINCT '; + } + + // if all column names were selected to display, we do a 'SELECT *' + // (more efficient and this helps prevent a problem in IE + // if one of the rows is edited and we come back to the Select results) + if (isset($_POST['zoom_submit']) || ! empty($_POST['displayAllColumns'])) { + $sql_query .= '* '; + } else { + $sql_query .= implode( + ', ', + Util::backquote($_POST['columnsToDisplay']) + ); + } // end if + + $sql_query .= ' FROM ' + . Util::backquote($_POST['table']); + $whereClause = $this->_generateWhereClause(); + $sql_query .= $whereClause; + + // if the search results are to be ordered + if (isset($_POST['orderByColumn']) && $_POST['orderByColumn'] != '--nil--') { + $sql_query .= ' ORDER BY ' + . Util::backquote($_POST['orderByColumn']) + . ' ' . $_POST['order']; + } // end if + return $sql_query; + } + + /** + * Provides a column's type, collation, operators list, and criteria value + * to display in table search form + * + * @param integer $search_index Row number in table search form + * @param integer $column_index Column index in ColumnNames array + * + * @return array Array containing column's properties + */ + public function getColumnProperties($search_index, $column_index) + { + $selected_operator = (isset($_POST['criteriaColumnOperators'][$search_index]) + ? $_POST['criteriaColumnOperators'][$search_index] : ''); + $entered_value = (isset($_POST['criteriaValues']) + ? $_POST['criteriaValues'] : ''); + $titles = [ + 'Browse' => Util::getIcon( + 'b_browse', + __('Browse foreign values') + ), + ]; + //Gets column's type and collation + $type = $this->_columnTypes[$column_index]; + $collation = $this->_columnCollations[$column_index]; + //Gets column's comparison operators depending on column type + $typeOperators = $this->dbi->types->getTypeOperatorsHtml( + preg_replace('@\(.*@s', '', $this->_columnTypes[$column_index]), + $this->_columnNullFlags[$column_index], + $selected_operator + ); + $func = $this->template->render('table/search/column_comparison_operators', [ + 'search_index' => $search_index, + 'type_operators' => $typeOperators, + ]); + //Gets link to browse foreign data(if any) and criteria inputbox + $foreignData = $this->relation->getForeignData( + $this->_foreigners, + $this->_columnNames[$column_index], + false, + '', + '' + ); + $value = $this->template->render('table/search/input_box', [ + 'str' => '', + 'column_type' => (string) $type, + 'column_id' => 'fieldID_', + 'in_zoom_search_edit' => false, + 'foreigners' => $this->_foreigners, + 'column_name' => $this->_columnNames[$column_index], + 'column_name_hash' => md5($this->_columnNames[$column_index]), + 'foreign_data' => $foreignData, + 'table' => $this->table, + 'column_index' => $search_index, + 'foreign_max_limit' => $GLOBALS['cfg']['ForeignKeyMaxLimit'], + 'criteria_values' => $entered_value, + 'db' => $this->db, + 'titles' => $titles, + 'in_fbs' => true, + ]); + return [ + 'type' => $type, + 'collation' => $collation, + 'func' => $func, + 'value' => $value, + ]; + } + + /** + * Generates the where clause for the SQL search query to be executed + * + * @return string the generated where clause + */ + private function _generateWhereClause() + { + if (isset($_POST['customWhereClause']) + && trim($_POST['customWhereClause']) != '' + ) { + return ' WHERE ' . $_POST['customWhereClause']; + } + + // If there are no search criteria set or no unary criteria operators, + // return + if (! isset($_POST['criteriaValues']) + && ! isset($_POST['criteriaColumnOperators']) + && ! isset($_POST['geom_func']) + ) { + return ''; + } + + // else continue to form the where clause from column criteria values + $fullWhereClause = []; + foreach ($_POST['criteriaColumnOperators'] as $column_index => $operator) { + $unaryFlag = $this->dbi->types->isUnaryOperator($operator); + $tmp_geom_func = isset($_POST['geom_func'][$column_index]) + ? $_POST['geom_func'][$column_index] : null; + + $whereClause = $this->_getWhereClause( + $_POST['criteriaValues'][$column_index], + $_POST['criteriaColumnNames'][$column_index], + $_POST['criteriaColumnTypes'][$column_index], + $operator, + $unaryFlag, + $tmp_geom_func + ); + + if ($whereClause) { + $fullWhereClause[] = $whereClause; + } + } // end foreach + + if (! empty($fullWhereClause)) { + return ' WHERE ' . implode(' AND ', $fullWhereClause); + } + return ''; + } + + /** + * Return the where clause in case column's type is ENUM. + * + * @param mixed $criteriaValues Search criteria input + * @param string $func_type Search function/operator + * + * @return string part of where clause. + */ + private function _getEnumWhereClause($criteriaValues, $func_type) + { + if (! is_array($criteriaValues)) { + $criteriaValues = explode(',', $criteriaValues); + } + $enum_selected_count = count($criteriaValues); + if ($func_type == '=' && $enum_selected_count > 1) { + $func_type = 'IN'; + $parens_open = '('; + $parens_close = ')'; + } elseif ($func_type == '!=' && $enum_selected_count > 1) { + $func_type = 'NOT IN'; + $parens_open = '('; + $parens_close = ')'; + } else { + $parens_open = ''; + $parens_close = ''; + } + $enum_where = '\'' + . $this->dbi->escapeString($criteriaValues[0]) . '\''; + for ($e = 1; $e < $enum_selected_count; $e++) { + $enum_where .= ', \'' + . $this->dbi->escapeString($criteriaValues[$e]) . '\''; + } + + return ' ' . $func_type . ' ' . $parens_open + . $enum_where . $parens_close; + } + + /** + * Return the where clause for a geometrical column. + * + * @param mixed $criteriaValues Search criteria input + * @param string $names Name of the column on which search is submitted + * @param string $func_type Search function/operator + * @param string $types Type of the field + * @param bool $geom_func Whether geometry functions should be applied + * + * @return string part of where clause. + */ + private function _getGeomWhereClause( + $criteriaValues, + $names, + $func_type, + $types, + $geom_func = null + ) { + $geom_unary_functions = [ + 'IsEmpty' => 1, + 'IsSimple' => 1, + 'IsRing' => 1, + 'IsClosed' => 1, + ]; + $where = ''; + + // Get details about the geometry functions + $geom_funcs = Util::getGISFunctions($types, true, false); + + // If the function takes multiple parameters + if (strpos($func_type, "IS NULL") !== false || strpos($func_type, "IS NOT NULL") !== false) { + return Util::backquote($names) . " " . $func_type; + } elseif ($geom_funcs[$geom_func]['params'] > 1) { + // create gis data from the criteria input + $gis_data = Util::createGISData($criteriaValues, $this->dbi->getVersion()); + return $geom_func . '(' . Util::backquote($names) + . ', ' . $gis_data . ')'; + } + + // New output type is the output type of the function being applied + $type = $geom_funcs[$geom_func]['type']; + $geom_function_applied = $geom_func + . '(' . Util::backquote($names) . ')'; + + // If the where clause is something like 'IsEmpty(`spatial_col_name`)' + if (isset($geom_unary_functions[$geom_func]) + && trim($criteriaValues) == '' + ) { + $where = $geom_function_applied; + } elseif (in_array($type, Util::getGISDatatypes()) + && ! empty($criteriaValues) + ) { + // create gis data from the criteria input + $gis_data = Util::createGISData($criteriaValues, $this->dbi->getVersion()); + $where = $geom_function_applied . " " . $func_type . " " . $gis_data; + } elseif (strlen($criteriaValues) > 0) { + $where = $geom_function_applied . " " + . $func_type . " '" . $criteriaValues . "'"; + } + return $where; + } + + /** + * Return the where clause for query generation based on the inputs provided. + * + * @param mixed $criteriaValues Search criteria input + * @param string $names Name of the column on which search is submitted + * @param string $types Type of the field + * @param string $func_type Search function/operator + * @param bool $unaryFlag Whether operator unary or not + * @param bool $geom_func Whether geometry functions should be applied + * + * @return string generated where clause. + */ + private function _getWhereClause( + $criteriaValues, + $names, + $types, + $func_type, + $unaryFlag, + $geom_func = null + ) { + // If geometry function is set + if (! empty($geom_func)) { + return $this->_getGeomWhereClause( + $criteriaValues, + $names, + $func_type, + $types, + $geom_func + ); + } + + $backquoted_name = Util::backquote($names); + $where = ''; + if ($unaryFlag) { + $where = $backquoted_name . ' ' . $func_type; + } elseif (strncasecmp($types, 'enum', 4) == 0 && (! empty($criteriaValues) || $criteriaValues[0] === '0')) { + $where = $backquoted_name; + $where .= $this->_getEnumWhereClause($criteriaValues, $func_type); + } elseif ($criteriaValues != '') { + // For these types we quote the value. Even if it's another type + // (like INT), for a LIKE we always quote the value. MySQL converts + // strings to numbers and numbers to strings as necessary + // during the comparison + if (preg_match('@char|binary|blob|text|set|date|time|year@i', $types) + || mb_strpos(' ' . $func_type, 'LIKE') + ) { + $quot = '\''; + } else { + $quot = ''; + } + + // LIKE %...% + if ($func_type == 'LIKE %...%') { + $func_type = 'LIKE'; + $criteriaValues = '%' . $criteriaValues . '%'; + } + if ($func_type == 'REGEXP ^...$') { + $func_type = 'REGEXP'; + $criteriaValues = '^' . $criteriaValues . '$'; + } + + if ('IN (...)' != $func_type + && 'NOT IN (...)' != $func_type + && 'BETWEEN' != $func_type + && 'NOT BETWEEN' != $func_type + ) { + return $backquoted_name . ' ' . $func_type . ' ' . $quot + . $this->dbi->escapeString($criteriaValues) . $quot; + } + $func_type = str_replace(' (...)', '', $func_type); + + //Don't explode if this is already an array + //(Case for (NOT) IN/BETWEEN.) + if (is_array($criteriaValues)) { + $values = $criteriaValues; + } else { + $values = explode(',', $criteriaValues); + } + // quote values one by one + $emptyKey = false; + foreach ($values as $key => &$value) { + if ('' === $value) { + $emptyKey = $key; + $value = 'NULL'; + continue; + } + $value = $quot . $this->dbi->escapeString(trim($value)) + . $quot; + } + + if ('BETWEEN' == $func_type || 'NOT BETWEEN' == $func_type) { + $where = $backquoted_name . ' ' . $func_type . ' ' + . (isset($values[0]) ? $values[0] : '') + . ' AND ' . (isset($values[1]) ? $values[1] : ''); + } else { //[NOT] IN + if (false !== $emptyKey) { + unset($values[$emptyKey]); + } + $wheres = []; + if (! empty($values)) { + $wheres[] = $backquoted_name . ' ' . $func_type + . ' (' . implode(',', $values) . ')'; + } + if (false !== $emptyKey) { + $wheres[] = $backquoted_name . ' IS NULL'; + } + $where = implode(' OR ', $wheres); + if (1 < count($wheres)) { + $where = '(' . $where . ')'; + } + } + } // end if + + return $where; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php new file mode 100644 index 0000000..a3975ac --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/SqlController.php @@ -0,0 +1,53 @@ +getHtml( + $params['sql_query'] ?? true, + false, + isset($params['delimiter']) + ? htmlspecialchars($params['delimiter']) + : ';' + ); + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php b/srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php new file mode 100644 index 0000000..7f59013 --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/Table/StructureController.php @@ -0,0 +1,1648 @@ +_db_is_system_schema = $db_is_system_schema; + $this->_url_query = Url::getCommonRaw(['db' => $db, 'table' => $table]); + $this->_tbl_is_view = $tbl_is_view; + $this->_tbl_storage_engine = $tbl_storage_engine; + $this->_table_info_num_rows = $table_info_num_rows; + $this->_tbl_collation = $tbl_collation; + $this->_showtable = $showtable; + $this->table_obj = $this->dbi->getTable($this->db, $this->table); + + $this->createAddField = $createAddField; + $this->relation = $relation; + $this->transformations = $transformations; + } + + /** + * Index action + * + * @param ContainerBuilder $containerBuilder ContainerBuilder instance + * + * @return void + */ + public function indexAction(ContainerBuilder $containerBuilder): void + { + global $sql_query; + + PageSettings::showGroup('TableStructure'); + + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + $this->response->getHeader()->getScripts()->addFiles( + [ + 'table/structure.js', + 'indexes.js', + ] + ); + + /** + * Handle column moving + */ + if (isset($_POST['move_columns']) + && is_array($_POST['move_columns']) + && $this->response->isAjax() + ) { + $this->moveColumns(); + return; + } + + /** + * handle MySQL reserved words columns check + */ + if (isset($_POST['reserved_word_check'])) { + if ($GLOBALS['cfg']['ReservedWordDisableWarning'] === false) { + $columns_names = $_POST['field_name']; + $reserved_keywords_names = []; + foreach ($columns_names as $column) { + if (Context::isKeyword(trim($column), true)) { + $reserved_keywords_names[] = trim($column); + } + } + if (Context::isKeyword(trim($this->table), true)) { + $reserved_keywords_names[] = trim($this->table); + } + if (count($reserved_keywords_names) === 0) { + $this->response->setRequestStatus(false); + } + $this->response->addJSON( + 'message', + sprintf( + _ngettext( + 'The name \'%s\' is a MySQL reserved keyword.', + 'The names \'%s\' are MySQL reserved keywords.', + count($reserved_keywords_names) + ), + implode(',', $reserved_keywords_names) + ) + ); + } else { + $this->response->setRequestStatus(false); + } + return; + } + /** + * A click on Change has been made for one column + */ + if (isset($_GET['change_column'])) { + $this->displayHtmlForColumnChange(null, 'tbl_structure.php', $containerBuilder); + return; + } + + /** + * Adding or editing partitioning of the table + */ + if (isset($_POST['edit_partitioning']) + && ! isset($_POST['save_partitioning']) + ) { + $this->displayHtmlForPartitionChange(); + return; + } + + /** + * handle multiple field commands if required + * + * submit_mult_*_x comes from IE if is used + */ + $submit_mult = $this->getMultipleFieldCommandType(); + + if (! empty($submit_mult)) { + if (isset($_POST['selected_fld'])) { + if ($submit_mult == 'browse') { + // browsing the table displaying only selected columns + $this->displayTableBrowseForSelectedColumns( + $GLOBALS['goto'], + $GLOBALS['pmaThemeImage'] + ); + } else { + // handle multiple field commands + // handle confirmation of deleting multiple columns + $action = 'tbl_structure.php'; + $GLOBALS['selected'] = $_POST['selected_fld']; + list( + $what_ret, $query_type_ret, $is_unset_submit_mult, + $mult_btn_ret, $centralColsError + ) + = $this->getDataForSubmitMult( + $submit_mult, + $_POST['selected_fld'], + $action, + $containerBuilder + ); + //update the existing variables + // todo: refactor mult_submits.inc.php such as + // below globals are not needed anymore + if (isset($what_ret)) { + $GLOBALS['what'] = $what_ret; + global $what; + } + if (isset($query_type_ret)) { + $GLOBALS['query_type'] = $query_type_ret; + global $query_type; + } + if ($is_unset_submit_mult) { + unset($submit_mult); + } + if (isset($mult_btn_ret)) { + $GLOBALS['mult_btn'] = $mult_btn_ret; + global $mult_btn; + } + include ROOT_PATH . 'libraries/mult_submits.inc.php'; + /** + * if $submit_mult == 'change', execution will have stopped + * at this point + */ + if (empty($message)) { + $message = Message::success(); + } + $this->response->addHTML( + Util::getMessage($message, $sql_query) + ); + } + } else { + $this->response->setRequestStatus(false); + $message = Message::error(__('No column selected.')); + $this->response->addJSON('message', $message); + } + } + + /** + * Modifications have been submitted -> updates the table + */ + if (isset($_POST['do_save_data'])) { + $regenerate = $this->updateColumns(); + if (! $regenerate) { + // continue to show the table's structure + unset($_POST['selected']); + } + } + + /** + * Modifications to the partitioning have been submitted -> updates the table + */ + if (isset($_POST['save_partitioning'])) { + $this->updatePartitioning(); + } + + /** + * Adding indexes + */ + if (isset($_POST['add_key']) + || isset($_POST['partition_maintenance']) + ) { + //todo: set some variables for sql.php include, to be eliminated + //after refactoring sql.php + $db = $this->db; + $table = $this->table; + $sql_query = $GLOBALS['sql_query']; + $cfg = $GLOBALS['cfg']; + $pmaThemeImage = $GLOBALS['pmaThemeImage']; + include ROOT_PATH . 'sql.php'; + $GLOBALS['reload'] = true; + } + + /** + * Gets the relation settings + */ + $cfgRelation = $this->relation->getRelationsParam(); + + /** + * Runs common work + */ + // set db, table references, for require_once that follows + // got to be eliminated in long run + $db = &$this->db; + $table = &$this->table; + $url_params = []; + include_once ROOT_PATH . 'libraries/tbl_common.inc.php'; + $this->_db_is_system_schema = $db_is_system_schema; + $this->_url_query = Url::getCommonRaw([ + 'db' => $db, + 'table' => $table, + 'goto' => 'tbl_structure.php', + 'back' => 'tbl_structure.php', + ]); + /* The url_params array is initialized in above include */ + $url_params['goto'] = 'tbl_structure.php'; + $url_params['back'] = 'tbl_structure.php'; + + // 2. Gets table keys and retains them + // @todo should be: $server->db($db)->table($table)->primary() + $primary = Index::getPrimary($this->table, $this->db); + $columns_with_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex( + Index::UNIQUE | Index::INDEX | Index::SPATIAL + | Index::FULLTEXT + ); + $columns_with_unique_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex(Index::UNIQUE); + + // 3. Get fields + $fields = (array) $this->dbi->getColumns( + $this->db, + $this->table, + null, + true + ); + + //display table structure + $this->response->addHTML( + $this->displayStructure( + $cfgRelation, + $columns_with_unique_index, + $url_params, + $primary, + $fields, + $columns_with_index + ) + ); + } + + /** + * Moves columns in the table's structure based on $_REQUEST + * + * @return void + */ + protected function moveColumns() + { + $this->dbi->selectDb($this->db); + + /* + * load the definitions for all columns + */ + $columns = $this->dbi->getColumnsFull($this->db, $this->table); + $column_names = array_keys($columns); + $changes = []; + + // @see https://mariadb.com/kb/en/library/changes-improvements-in-mariadb-102/#information-schema + $usesLiteralNull = $this->dbi->isMariaDB() && $this->dbi->getVersion() >= 100200; + $defaultNullValue = $usesLiteralNull ? 'NULL' : null; + // move columns from first to last + for ($i = 0, $l = count($_POST['move_columns']); $i < $l; $i++) { + $column = $_POST['move_columns'][$i]; + // is this column already correctly placed? + if ($column_names[$i] == $column) { + continue; + } + + // it is not, let's move it to index $i + $data = $columns[$column]; + $extracted_columnspec = Util::extractColumnSpec($data['Type']); + if (isset($data['Extra']) + && $data['Extra'] == 'on update CURRENT_TIMESTAMP' + ) { + $extracted_columnspec['attribute'] = $data['Extra']; + unset($data['Extra']); + } + $current_timestamp = ($data['Type'] == 'timestamp' + || $data['Type'] == 'datetime') + && ($data['Default'] == 'CURRENT_TIMESTAMP' + || $data['Default'] == 'current_timestamp()'); + + // @see https://mariadb.com/kb/en/library/information-schema-columns-table/#examples + if ($data['Null'] === 'YES' && in_array($data['Default'], [$defaultNullValue, null])) { + $default_type = 'NULL'; + } elseif ($current_timestamp) { + $default_type = 'CURRENT_TIMESTAMP'; + } elseif ($data['Default'] === null) { + $default_type = 'NONE'; + } else { + $default_type = 'USER_DEFINED'; + } + + $virtual = [ + 'VIRTUAL', + 'PERSISTENT', + 'VIRTUAL GENERATED', + 'STORED GENERATED', + ]; + $data['Virtuality'] = ''; + $data['Expression'] = ''; + if (isset($data['Extra']) && in_array($data['Extra'], $virtual)) { + $data['Virtuality'] = str_replace(' GENERATED', '', $data['Extra']); + $expressions = $this->table_obj->getColumnGenerationExpression($column); + $data['Expression'] = $expressions[$column]; + } + + $changes[] = 'CHANGE ' . Table::generateAlter( + $column, + $column, + mb_strtoupper($extracted_columnspec['type']), + $extracted_columnspec['spec_in_brackets'], + $extracted_columnspec['attribute'], + isset($data['Collation']) ? $data['Collation'] : '', + $data['Null'] === 'YES' ? 'YES' : 'NO', + $default_type, + $current_timestamp ? '' : $data['Default'], + isset($data['Extra']) && $data['Extra'] !== '' ? $data['Extra'] + : false, + isset($data['COLUMN_COMMENT']) && $data['COLUMN_COMMENT'] !== '' + ? $data['COLUMN_COMMENT'] : false, + $data['Virtuality'], + $data['Expression'], + $i === 0 ? '-first' : $column_names[$i - 1] + ); + // update current column_names array, first delete old position + for ($j = 0, $ll = count($column_names); $j < $ll; $j++) { + if ($column_names[$j] == $column) { + unset($column_names[$j]); + } + } + // insert moved column + array_splice($column_names, $i, 0, $column); + } + if (empty($changes) && ! isset($_REQUEST['preview_sql'])) { // should never happen + $this->response->setRequestStatus(false); + return; + } + // query for moving the columns + $sql_query = sprintf( + 'ALTER TABLE %s %s', + Util::backquote($this->table), + implode(', ', $changes) + ); + + if (isset($_REQUEST['preview_sql'])) { // preview sql + $this->response->addJSON( + 'sql_data', + $this->template->render('preview_sql', [ + 'query_data' => $sql_query, + ]) + ); + } else { // move column + $this->dbi->tryQuery($sql_query); + $tmp_error = $this->dbi->getError(); + if ($tmp_error) { + $this->response->setRequestStatus(false); + $this->response->addJSON('message', Message::error($tmp_error)); + } else { + $message = Message::success( + __('The columns have been moved successfully.') + ); + $this->response->addJSON('message', $message); + $this->response->addJSON('columns', $column_names); + } + } + } + + /** + * Displays HTML for changing one or more columns + * + * @param array $selected the selected columns + * @param string $action target script to call + * @param ContainerBuilder $containerBuilder Container builder instance (Used in tbl_columns_definition_form.inc.php) + * + * @return void + */ + protected function displayHtmlForColumnChange($selected, $action, ContainerBuilder $containerBuilder) + { + // $selected comes from mult_submits.inc.php + if (empty($selected)) { + $selected[] = $_REQUEST['field']; + $selected_cnt = 1; + } else { // from a multiple submit + $selected_cnt = count($selected); + } + + /** + * @todo optimize in case of multiple fields to modify + */ + $fields_meta = []; + for ($i = 0; $i < $selected_cnt; $i++) { + $value = $this->dbi->getColumns( + $this->db, + $this->table, + $this->dbi->escapeString($selected[$i]), + true + ); + if (count($value) === 0) { + $message = Message::error( + __('Failed to get description of column %s!') + ); + $message->addParam($selected[$i]); + $this->response->addHTML($message); + } else { + $fields_meta[] = $value; + } + } + $num_fields = count($fields_meta); + // set these globals because tbl_columns_definition_form.inc.php + // verifies them + // @todo: refactor tbl_columns_definition_form.inc.php so that it uses + // protected function params + $GLOBALS['action'] = $action; + $GLOBALS['num_fields'] = $num_fields; + + /** + * Form for changing properties. + */ + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + include ROOT_PATH . 'libraries/tbl_columns_definition_form.inc.php'; + } + + /** + * Displays HTML for partition change + * + * @return void + */ + protected function displayHtmlForPartitionChange() + { + $partitionDetails = null; + if (! isset($_POST['partition_by'])) { + $partitionDetails = $this->_extractPartitionDetails(); + } + + $partitionDetails = TablePartitionDefinition::getDetails($partitionDetails); + $this->response->addHTML( + $this->template->render('table/structure/partition_definition_form', [ + 'db' => $this->db, + 'table' => $this->table, + 'partition_details' => $partitionDetails, + ]) + ); + } + + /** + * Extracts partition details from CREATE TABLE statement + * + * @return array[]|null array of partition details + */ + private function _extractPartitionDetails() + { + $createTable = (new Table($this->table, $this->db))->showCreate(); + if (! $createTable) { + return null; + } + + $parser = new Parser($createTable); + /** + * @var CreateStatement $stmt + */ + $stmt = $parser->statements[0]; + + $partitionDetails = []; + + $partitionDetails['partition_by'] = ''; + $partitionDetails['partition_expr'] = ''; + $partitionDetails['partition_count'] = ''; + + if (! empty($stmt->partitionBy)) { + $openPos = strpos($stmt->partitionBy, "("); + $closePos = strrpos($stmt->partitionBy, ")"); + + $partitionDetails['partition_by'] + = trim(substr($stmt->partitionBy, 0, $openPos)); + $partitionDetails['partition_expr'] + = trim(substr($stmt->partitionBy, $openPos + 1, $closePos - ($openPos + 1))); + if (isset($stmt->partitionsNum)) { + $count = $stmt->partitionsNum; + } else { + $count = count($stmt->partitions); + } + $partitionDetails['partition_count'] = $count; + } + + $partitionDetails['subpartition_by'] = ''; + $partitionDetails['subpartition_expr'] = ''; + $partitionDetails['subpartition_count'] = ''; + + if (! empty($stmt->subpartitionBy)) { + $openPos = strpos($stmt->subpartitionBy, "("); + $closePos = strrpos($stmt->subpartitionBy, ")"); + + $partitionDetails['subpartition_by'] + = trim(substr($stmt->subpartitionBy, 0, $openPos)); + $partitionDetails['subpartition_expr'] + = trim(substr($stmt->subpartitionBy, $openPos + 1, $closePos - ($openPos + 1))); + if (isset($stmt->subpartitionsNum)) { + $count = $stmt->subpartitionsNum; + } else { + $count = count($stmt->partitions[0]->subpartitions); + } + $partitionDetails['subpartition_count'] = $count; + } + + // Only LIST and RANGE type parameters allow subpartitioning + $partitionDetails['can_have_subpartitions'] + = $partitionDetails['partition_count'] > 1 + && ($partitionDetails['partition_by'] == 'RANGE' + || $partitionDetails['partition_by'] == 'RANGE COLUMNS' + || $partitionDetails['partition_by'] == 'LIST' + || $partitionDetails['partition_by'] == 'LIST COLUMNS'); + + // Values are specified only for LIST and RANGE type partitions + $partitionDetails['value_enabled'] = isset($partitionDetails['partition_by']) + && ($partitionDetails['partition_by'] == 'RANGE' + || $partitionDetails['partition_by'] == 'RANGE COLUMNS' + || $partitionDetails['partition_by'] == 'LIST' + || $partitionDetails['partition_by'] == 'LIST COLUMNS'); + + $partitionDetails['partitions'] = []; + + for ($i = 0, $iMax = (int) $partitionDetails['partition_count']; $i < $iMax; $i++) { + if (! isset($stmt->partitions[$i])) { + $partitionDetails['partitions'][$i] = [ + 'name' => 'p' . $i, + 'value_type' => '', + 'value' => '', + 'engine' => '', + 'comment' => '', + 'data_directory' => '', + 'index_directory' => '', + 'max_rows' => '', + 'min_rows' => '', + 'tablespace' => '', + 'node_group' => '', + ]; + } else { + $p = $stmt->partitions[$i]; + $type = $p->type; + $expr = trim((string) $p->expr, '()'); + if ($expr == 'MAXVALUE') { + $type .= ' MAXVALUE'; + $expr = ''; + } + $partitionDetails['partitions'][$i] = [ + 'name' => $p->name, + 'value_type' => $type, + 'value' => $expr, + 'engine' => $p->options->has('ENGINE', true), + 'comment' => trim((string) $p->options->has('COMMENT', true), "'"), + 'data_directory' => trim((string) $p->options->has('DATA DIRECTORY', true), "'"), + 'index_directory' => trim((string) $p->options->has('INDEX_DIRECTORY', true), "'"), + 'max_rows' => $p->options->has('MAX_ROWS', true), + 'min_rows' => $p->options->has('MIN_ROWS', true), + 'tablespace' => $p->options->has('TABLESPACE', true), + 'node_group' => $p->options->has('NODEGROUP', true), + ]; + } + + $partition =& $partitionDetails['partitions'][$i]; + $partition['prefix'] = 'partitions[' . $i . ']'; + + if ($partitionDetails['subpartition_count'] > 1) { + $partition['subpartition_count'] = $partitionDetails['subpartition_count']; + $partition['subpartitions'] = []; + + for ($j = 0, $jMax = (int) $partitionDetails['subpartition_count']; $j < $jMax; $j++) { + if (! isset($stmt->partitions[$i]->subpartitions[$j])) { + $partition['subpartitions'][$j] = [ + 'name' => $partition['name'] . '_s' . $j, + 'engine' => '', + 'comment' => '', + 'data_directory' => '', + 'index_directory' => '', + 'max_rows' => '', + 'min_rows' => '', + 'tablespace' => '', + 'node_group' => '', + ]; + } else { + $sp = $stmt->partitions[$i]->subpartitions[$j]; + $partition['subpartitions'][$j] = [ + 'name' => $sp->name, + 'engine' => $sp->options->has('ENGINE', true), + 'comment' => trim($sp->options->has('COMMENT', true), "'"), + 'data_directory' => trim($sp->options->has('DATA DIRECTORY', true), "'"), + 'index_directory' => trim($sp->options->has('INDEX_DIRECTORY', true), "'"), + 'max_rows' => $sp->options->has('MAX_ROWS', true), + 'min_rows' => $sp->options->has('MIN_ROWS', true), + 'tablespace' => $sp->options->has('TABLESPACE', true), + 'node_group' => $sp->options->has('NODEGROUP', true), + ]; + } + + $subpartition =& $partition['subpartitions'][$j]; + $subpartition['prefix'] = 'partitions[' . $i . ']' + . '[subpartitions][' . $j . ']'; + } + } + } + + return $partitionDetails; + } + + /** + * Update the table's partitioning based on $_REQUEST + * + * @return void + */ + protected function updatePartitioning() + { + $sql_query = "ALTER TABLE " . Util::backquote($this->table) . " " + . $this->createAddField->getPartitionsDefinition(); + + // Execute alter query + $result = $this->dbi->tryQuery($sql_query); + + if ($result !== false) { + $message = Message::success( + __('Table %1$s has been altered successfully.') + ); + $message->addParam($this->table); + $this->response->addHTML( + Util::getMessage($message, $sql_query, 'success') + ); + } else { + $this->response->setRequestStatus(false); + $this->response->addJSON( + 'message', + Message::rawError( + __('Query error') . ':
' . $this->dbi->getError() + ) + ); + } + } + + /** + * Function to get the type of command for multiple field handling + * + * @return string|null + */ + protected function getMultipleFieldCommandType() + { + $types = [ + 'change', + 'drop', + 'primary', + 'index', + 'unique', + 'spatial', + 'fulltext', + 'browse', + ]; + + foreach ($types as $type) { + if (isset($_POST['submit_mult_' . $type . '_x'])) { + return $type; + } + } + + if (isset($_POST['submit_mult'])) { + return $_POST['submit_mult']; + } elseif (isset($_POST['mult_btn']) + && $_POST['mult_btn'] == __('Yes') + ) { + if (isset($_POST['selected'])) { + $_POST['selected_fld'] = $_POST['selected']; + } + return 'row_delete'; + } + + return null; + } + + /** + * Function to display table browse for selected columns + * + * @param string $goto goto page url + * @param string $pmaThemeImage URI of the pma theme image + * + * @return void + */ + protected function displayTableBrowseForSelectedColumns($goto, $pmaThemeImage) + { + $GLOBALS['active_page'] = 'sql.php'; + $fields = []; + foreach ($_POST['selected_fld'] as $sval) { + $fields[] = Util::backquote($sval); + } + $sql_query = sprintf( + 'SELECT %s FROM %s.%s', + implode(', ', $fields), + Util::backquote($this->db), + Util::backquote($this->table) + ); + + // Parse and analyze the query + $db = &$this->db; + list( + $analyzed_sql_results, + $db, + ) = ParseAnalyze::sqlQuery($sql_query, $db); + // @todo: possibly refactor + extract($analyzed_sql_results); + + $sql = new Sql(); + $this->response->addHTML( + $sql->executeQueryAndGetQueryResponse( + isset($analyzed_sql_results) ? $analyzed_sql_results : '', + false, // is_gotofile + $this->db, // db + $this->table, // table + null, // find_real_end + null, // sql_query_for_bookmark + null, // extra_data + null, // message_to_show + null, // message + null, // sql_data + $goto, // goto + $pmaThemeImage, // pmaThemeImage + null, // disp_query + null, // disp_message + null, // query_type + $sql_query, // sql_query + null, // selectedTables + null // complete_query + ) + ); + } + + /** + * Update the table's structure based on $_REQUEST + * + * @return boolean true if error occurred + * + */ + protected function updateColumns() + { + $err_url = 'tbl_structure.php' . Url::getCommon( + [ + 'db' => $this->db, + 'table' => $this->table, + ] + ); + $regenerate = false; + $field_cnt = count($_POST['field_name']); + $changes = []; + $adjust_privileges = []; + $columns_with_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex( + Index::PRIMARY | Index::UNIQUE + ); + for ($i = 0; $i < $field_cnt; $i++) { + if (! $this->columnNeedsAlterTable($i)) { + continue; + } + + $changes[] = 'CHANGE ' . Table::generateAlter( + Util::getValueByKey($_POST, "field_orig.${i}", ''), + $_POST['field_name'][$i], + $_POST['field_type'][$i], + $_POST['field_length'][$i], + $_POST['field_attribute'][$i], + Util::getValueByKey($_POST, "field_collation.${i}", ''), + Util::getValueByKey($_POST, "field_null.${i}", 'NO'), + $_POST['field_default_type'][$i], + $_POST['field_default_value'][$i], + Util::getValueByKey($_POST, "field_extra.${i}", false), + Util::getValueByKey($_POST, "field_comments.${i}", ''), + Util::getValueByKey($_POST, "field_virtuality.${i}", ''), + Util::getValueByKey($_POST, "field_expression.${i}", ''), + Util::getValueByKey($_POST, "field_move_to.${i}", ''), + $columns_with_index + ); + + // find the remembered sort expression + $sorted_col = $this->table_obj->getUiProp( + Table::PROP_SORTED_COLUMN + ); + // if the old column name is part of the remembered sort expression + if (mb_strpos( + (string) $sorted_col, + Util::backquote($_POST['field_orig'][$i]) + ) !== false) { + // delete the whole remembered sort expression + $this->table_obj->removeUiProp(Table::PROP_SORTED_COLUMN); + } + + if (isset($_POST['field_adjust_privileges'][$i]) + && ! empty($_POST['field_adjust_privileges'][$i]) + && $_POST['field_orig'][$i] != $_POST['field_name'][$i] + ) { + $adjust_privileges[$_POST['field_orig'][$i]] + = $_POST['field_name'][$i]; + } + } // end for + + if (count($changes) > 0 || isset($_POST['preview_sql'])) { + // Builds the primary keys statements and updates the table + $key_query = ''; + /** + * this is a little bit more complex + * + * @todo if someone selects A_I when altering a column we need to check: + * - no other column with A_I + * - the column has an index, if not create one + * + */ + + // To allow replication, we first select the db to use + // and then run queries on this db. + if (! $this->dbi->selectDb($this->db)) { + Util::mysqlDie( + $this->dbi->getError(), + 'USE ' . Util::backquote($this->db) . ';', + false, + $err_url + ); + } + $sql_query = 'ALTER TABLE ' . Util::backquote($this->table) . ' '; + $sql_query .= implode(', ', $changes) . $key_query; + $sql_query .= ';'; + + // If there is a request for SQL previewing. + if (isset($_POST['preview_sql'])) { + Core::previewSQL(count($changes) > 0 ? $sql_query : ''); + } + + $columns_with_index = $this->dbi + ->getTable($this->db, $this->table) + ->getColumnsWithIndex( + Index::PRIMARY | Index::UNIQUE | Index::INDEX + | Index::SPATIAL | Index::FULLTEXT + ); + + $changedToBlob = []; + // While changing the Column Collation + // First change to BLOB + for ($i = 0; $i < $field_cnt; $i++) { + if (isset($_POST['field_collation'][$i]) + && isset($_POST['field_collation_orig'][$i]) + && $_POST['field_collation'][$i] !== $_POST['field_collation_orig'][$i] + && ! in_array($_POST['field_orig'][$i], $columns_with_index) + ) { + $secondary_query = 'ALTER TABLE ' . Util::backquote( + $this->table + ) + . ' CHANGE ' . Util::backquote( + $_POST['field_orig'][$i] + ) + . ' ' . Util::backquote($_POST['field_orig'][$i]) + . ' BLOB'; + + if (isset($_POST['field_virtuality'][$i]) + && isset($_POST['field_expression'][$i])) { + if ($_POST['field_virtuality'][$i]) { + $secondary_query .= ' AS (' . $_POST['field_expression'][$i] . ') ' + . $_POST['field_virtuality'][$i]; + } + } + + $secondary_query .= ';'; + + $this->dbi->query($secondary_query); + $changedToBlob[$i] = true; + } else { + $changedToBlob[$i] = false; + } + } + + // Then make the requested changes + $result = $this->dbi->tryQuery($sql_query); + + if ($result !== false) { + $changed_privileges = $this->adjustColumnPrivileges( + $adjust_privileges + ); + + if ($changed_privileges) { + $message = Message::success( + __( + 'Table %1$s has been altered successfully. Privileges ' . + 'have been adjusted.' + ) + ); + } else { + $message = Message::success( + __('Table %1$s has been altered successfully.') + ); + } + $message->addParam($this->table); + + $this->response->addHTML( + Util::getMessage($message, $sql_query, 'success') + ); + } else { + // An error happened while inserting/updating a table definition + + // Save the Original Error + $orig_error = $this->dbi->getError(); + $changes_revert = []; + + // Change back to Original Collation and data type + for ($i = 0; $i < $field_cnt; $i++) { + if ($changedToBlob[$i]) { + $changes_revert[] = 'CHANGE ' . Table::generateAlter( + Util::getValueByKey($_POST, "field_orig.${i}", ''), + $_POST['field_name'][$i], + $_POST['field_type_orig'][$i], + $_POST['field_length_orig'][$i], + $_POST['field_attribute_orig'][$i], + Util::getValueByKey($_POST, "field_collation_orig.${i}", ''), + Util::getValueByKey($_POST, "field_null_orig.${i}", 'NO'), + $_POST['field_default_type_orig'][$i], + $_POST['field_default_value_orig'][$i], + Util::getValueByKey($_POST, "field_extra_orig.${i}", false), + Util::getValueByKey($_POST, "field_comments_orig.${i}", ''), + Util::getValueByKey($_POST, "field_virtuality_orig.${i}", ''), + Util::getValueByKey($_POST, "field_expression_orig.${i}", ''), + Util::getValueByKey($_POST, "field_move_to_orig.${i}", '') + ); + } + } + + $revert_query = 'ALTER TABLE ' . Util::backquote($this->table) + . ' '; + $revert_query .= implode(', ', $changes_revert) . ''; + $revert_query .= ';'; + + // Column reverted back to original + $this->dbi->query($revert_query); + + $this->response->setRequestStatus(false); + $this->response->addJSON( + 'message', + Message::rawError( + __('Query error') . ':
' . $orig_error + ) + ); + $regenerate = true; + } + } + + // update field names in relation + if (isset($_POST['field_orig']) && is_array($_POST['field_orig'])) { + foreach ($_POST['field_orig'] as $fieldindex => $fieldcontent) { + if ($_POST['field_name'][$fieldindex] != $fieldcontent) { + $this->relation->renameField( + $this->db, + $this->table, + $fieldcontent, + $_POST['field_name'][$fieldindex] + ); + } + } + } + + // update mime types + if (isset($_POST['field_mimetype']) + && is_array($_POST['field_mimetype']) + && $GLOBALS['cfg']['BrowseMIME'] + ) { + foreach ($_POST['field_mimetype'] as $fieldindex => $mimetype) { + if (isset($_POST['field_name'][$fieldindex]) + && strlen($_POST['field_name'][$fieldindex]) > 0 + ) { + $this->transformations->setMime( + $this->db, + $this->table, + $_POST['field_name'][$fieldindex], + $mimetype, + $_POST['field_transformation'][$fieldindex], + $_POST['field_transformation_options'][$fieldindex], + $_POST['field_input_transformation'][$fieldindex], + $_POST['field_input_transformation_options'][$fieldindex] + ); + } + } + } + return $regenerate; + } + + /** + * Adjusts the Privileges for all the columns whose names have changed + * + * @param array $adjust_privileges assoc array of old col names mapped to new + * cols + * + * @return boolean boolean whether at least one column privileges + * adjusted + */ + protected function adjustColumnPrivileges(array $adjust_privileges) + { + $changed = false; + + if (Util::getValueByKey($GLOBALS, 'col_priv', false) + && Util::getValueByKey($GLOBALS, 'is_reload_priv', false) + ) { + $this->dbi->selectDb('mysql'); + + // For Column specific privileges + foreach ($adjust_privileges as $oldCol => $newCol) { + $this->dbi->query( + sprintf( + 'UPDATE %s SET Column_name = "%s" + WHERE Db = "%s" + AND Table_name = "%s" + AND Column_name = "%s";', + Util::backquote('columns_priv'), + $newCol, + $this->db, + $this->table, + $oldCol + ) + ); + + // i.e. if atleast one column privileges adjusted + $changed = true; + } + + if ($changed) { + // Finally FLUSH the new privileges + $this->dbi->query("FLUSH PRIVILEGES;"); + } + } + + return $changed; + } + + /** + * Verifies if some elements of a column have changed + * + * @param integer $i column index in the request + * + * @return boolean true if we need to generate ALTER TABLE + * + */ + protected function columnNeedsAlterTable($i) + { + // these two fields are checkboxes so might not be part of the + // request; therefore we define them to avoid notices below + if (! isset($_POST['field_null'][$i])) { + $_POST['field_null'][$i] = 'NO'; + } + if (! isset($_POST['field_extra'][$i])) { + $_POST['field_extra'][$i] = ''; + } + + // field_name does not follow the convention (corresponds to field_orig) + if ($_POST['field_name'][$i] != $_POST['field_orig'][$i]) { + return true; + } + + $fields = [ + 'field_attribute', + 'field_collation', + 'field_comments', + 'field_default_value', + 'field_default_type', + 'field_extra', + 'field_length', + 'field_null', + 'field_type', + ]; + foreach ($fields as $field) { + if ($_POST[$field][$i] != $_POST[$field . '_orig'][$i]) { + return true; + } + } + return ! empty($_POST['field_move_to'][$i]); + } + + /** + * Displays the table structure ('show table' works correct since 3.23.03) + * + * @param array $cfgRelation current relation parameters + * @param array $columns_with_unique_index Columns with unique index + * @param mixed $url_params Contains an associative + * array with url params + * @param Index|false $primary_index primary index or false if + * no one exists + * @param array $fields Fields + * @param array $columns_with_index Columns with index + * + * @return string + */ + protected function displayStructure( + array $cfgRelation, + array $columns_with_unique_index, + $url_params, + $primary_index, + array $fields, + array $columns_with_index + ) { + // prepare comments + $comments_map = []; + $mime_map = []; + + if ($GLOBALS['cfg']['ShowPropertyComments']) { + $comments_map = $this->relation->getComments($this->db, $this->table); + if ($cfgRelation['mimework'] && $GLOBALS['cfg']['BrowseMIME']) { + $mime_map = $this->transformations->getMime($this->db, $this->table, true); + } + } + $centralColumns = new CentralColumns($this->dbi); + $central_list = $centralColumns->getFromTable( + $this->db, + $this->table + ); + $columns_list = []; + + $titles = [ + 'Change' => Util::getIcon('b_edit', __('Change')), + 'Drop' => Util::getIcon('b_drop', __('Drop')), + 'NoDrop' => Util::getIcon('b_drop', __('Drop')), + 'Primary' => Util::getIcon('b_primary', __('Primary')), + 'Index' => Util::getIcon('b_index', __('Index')), + 'Unique' => Util::getIcon('b_unique', __('Unique')), + 'Spatial' => Util::getIcon('b_spatial', __('Spatial')), + 'IdxFulltext' => Util::getIcon('b_ftext', __('Fulltext')), + 'NoPrimary' => Util::getIcon('bd_primary', __('Primary')), + 'NoIndex' => Util::getIcon('bd_index', __('Index')), + 'NoUnique' => Util::getIcon('bd_unique', __('Unique')), + 'NoSpatial' => Util::getIcon('bd_spatial', __('Spatial')), + 'NoIdxFulltext' => Util::getIcon('bd_ftext', __('Fulltext')), + 'DistinctValues' => Util::getIcon('b_browse', __('Distinct values')), + ]; + + $edit_view_url = ''; + if ($this->_tbl_is_view && ! $this->_db_is_system_schema) { + $edit_view_url = Url::getCommon([ + 'db' => $this->db, + 'table' => $this->table, + ]); + } + + /** + * Displays Space usage and row statistics + */ + // BEGIN - Calc Table Space + // Get valid statistics whatever is the table type + if ($GLOBALS['cfg']['ShowStats']) { + //get table stats in HTML format + $tablestats = $this->getTableStats(); + //returning the response in JSON format to be used by Ajax + $this->response->addJSON('tableStat', $tablestats); + } + // END - Calc Table Space + + $hideStructureActions = false; + if ($GLOBALS['cfg']['HideStructureActions'] === true) { + $hideStructureActions = true; + } + + // logic removed from Template + $rownum = 0; + $columns_list = []; + $attributes = []; + $displayed_fields = []; + $row_comments = []; + $extracted_columnspecs = []; + $collations = []; + foreach ($fields as &$field) { + $rownum += 1; + $columns_list[] = $field['Field']; + + $extracted_columnspecs[$rownum] = Util::extractColumnSpec($field['Type']); + $attributes[$rownum] = $extracted_columnspecs[$rownum]['attribute']; + if (strpos($field['Extra'], 'on update CURRENT_TIMESTAMP') !== false) { + $attributes[$rownum] = 'on update CURRENT_TIMESTAMP'; + } + + $displayed_fields[$rownum] = new stdClass(); + $displayed_fields[$rownum]->text = $field['Field']; + $displayed_fields[$rownum]->icon = ""; + $row_comments[$rownum] = ''; + + if (isset($comments_map[$field['Field']])) { + $displayed_fields[$rownum]->comment = $comments_map[$field['Field']]; + $row_comments[$rownum] = $comments_map[$field['Field']]; + } + + if ($primary_index && $primary_index->hasColumn($field['Field'])) { + $displayed_fields[$rownum]->icon .= + Util::getImage('b_primary', __('Primary')); + } + + if (in_array($field['Field'], $columns_with_index)) { + $displayed_fields[$rownum]->icon .= + Util::getImage('bd_primary', __('Index')); + } + + $collation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $field['Collation'] ?? '' + ); + if ($collation !== null) { + $collations[$collation->getName()] = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + } + + $engine = $this->table_obj->getStorageEngine(); + return $this->template->render('table/structure/display_structure', [ + 'url_params' => [ + 'db' => $this->db, + 'table' => $this->table, + ], + 'collations' => $collations, + 'is_foreign_key_supported' => Util::isForeignKeySupported($engine), + 'displayIndexesHtml' => Index::getHtmlForDisplayIndexes(), + 'cfg_relation' => $this->relation->getRelationsParam(), + 'hide_structure_actions' => $hideStructureActions, + 'db' => $this->db, + 'table' => $this->table, + 'db_is_system_schema' => $this->_db_is_system_schema, + 'tbl_is_view' => $this->_tbl_is_view, + 'mime_map' => $mime_map, + 'url_query' => $this->_url_query, + 'titles' => $titles, + 'tbl_storage_engine' => $this->_tbl_storage_engine, + 'primary' => $primary_index, + 'columns_with_unique_index' => $columns_with_unique_index, + 'edit_view_url' => $edit_view_url, + 'columns_list' => $columns_list, + 'table_stats' => isset($tablestats) ? $tablestats : null, + 'fields' => $fields, + 'extracted_columnspecs' => $extracted_columnspecs, + 'columns_with_index' => $columns_with_index, + 'central_list' => $central_list, + 'comments_map' => $comments_map, + 'browse_mime' => $GLOBALS['cfg']['BrowseMIME'], + 'show_column_comments' => $GLOBALS['cfg']['ShowColumnComments'], + 'show_stats' => $GLOBALS['cfg']['ShowStats'], + 'relation_commwork' => $GLOBALS['cfgRelation']['commwork'], + 'relation_mimework' => $GLOBALS['cfgRelation']['mimework'], + 'central_columns_work' => $GLOBALS['cfgRelation']['centralcolumnswork'], + 'mysql_int_version' => $this->dbi->getVersion(), + 'is_mariadb' => $this->dbi->isMariaDB(), + 'pma_theme_image' => $GLOBALS['pmaThemeImage'], + 'text_dir' => $GLOBALS['text_dir'], + 'is_active' => Tracker::isActive(), + 'have_partitioning' => Partition::havePartitioning(), + 'partitions' => Partition::getPartitions($this->db, $this->table), + 'partition_names' => Partition::getPartitionNames($this->db, $this->table), + 'default_sliders_state' => $GLOBALS['cfg']['InitialSlidersState'], + 'attributes' => $attributes, + 'displayed_fields' => $displayed_fields, + 'row_comments' => $row_comments, + ]); + } + + /** + * Get HTML snippet for display table statistics + * + * @return string + */ + protected function getTableStats() + { + if (empty($this->_showtable)) { + $this->_showtable = $this->dbi->getTable( + $this->db, + $this->table + )->getStatusInfo(null, true); + } + + if (empty($this->_showtable['Data_length'])) { + $this->_showtable['Data_length'] = 0; + } + if (empty($this->_showtable['Index_length'])) { + $this->_showtable['Index_length'] = 0; + } + + $is_innodb = (isset($this->_showtable['Type']) + && $this->_showtable['Type'] == 'InnoDB'); + + $mergetable = $this->table_obj->isMerge(); + + // this is to display for example 261.2 MiB instead of 268k KiB + $max_digits = 3; + $decimals = 1; + list($data_size, $data_unit) = Util::formatByteDown( + $this->_showtable['Data_length'], + $max_digits, + $decimals + ); + if ($mergetable === false) { + list($index_size, $index_unit) = Util::formatByteDown( + $this->_showtable['Index_length'], + $max_digits, + $decimals + ); + } + if (isset($this->_showtable['Data_free'])) { + list($free_size, $free_unit) = Util::formatByteDown( + $this->_showtable['Data_free'], + $max_digits, + $decimals + ); + list($effect_size, $effect_unit) = Util::formatByteDown( + $this->_showtable['Data_length'] + + $this->_showtable['Index_length'] + - $this->_showtable['Data_free'], + $max_digits, + $decimals + ); + } else { + list($effect_size, $effect_unit) = Util::formatByteDown( + $this->_showtable['Data_length'] + + $this->_showtable['Index_length'], + $max_digits, + $decimals + ); + } + list($tot_size, $tot_unit) = Util::formatByteDown( + $this->_showtable['Data_length'] + $this->_showtable['Index_length'], + $max_digits, + $decimals + ); + if ($this->_table_info_num_rows > 0) { + list($avg_size, $avg_unit) = Util::formatByteDown( + ($this->_showtable['Data_length'] + + $this->_showtable['Index_length']) + / $this->_showtable['Rows'], + 6, + 1 + ); + } else { + $avg_size = $avg_unit = ''; + } + /** @var Innodb $innodbEnginePlugin */ + $innodbEnginePlugin = StorageEngine::getEngine('Innodb'); + $innodb_file_per_table = $innodbEnginePlugin->supportsFilePerTable(); + + $engine = $this->dbi->getTable($this->db, $this->table)->getStorageEngine(); + + $tableCollation = []; + $collation = Charsets::findCollationByName( + $this->dbi, + $GLOBALS['cfg']['Server']['DisableIS'], + $this->_tbl_collation + ); + if ($collation !== null) { + $tableCollation = [ + 'name' => $collation->getName(), + 'description' => $collation->getDescription(), + ]; + } + return $this->template->render('table/structure/display_table_stats', [ + 'url_params' => [ + 'db' => $GLOBALS['db'], + 'table' => $GLOBALS['table'], + ], + 'is_foreign_key_supported' => Util::isForeignKeySupported($engine), + 'cfg_relation' => $this->relation->getRelationsParam(), + 'showtable' => $this->_showtable, + 'table_info_num_rows' => $this->_table_info_num_rows, + 'tbl_is_view' => $this->_tbl_is_view, + 'db_is_system_schema' => $this->_db_is_system_schema, + 'tbl_storage_engine' => $this->_tbl_storage_engine, + 'url_query' => $this->_url_query, + 'table_collation' => $tableCollation, + 'is_innodb' => $is_innodb, + 'mergetable' => $mergetable, + 'avg_size' => isset($avg_size) ? $avg_size : null, + 'avg_unit' => isset($avg_unit) ? $avg_unit : null, + 'data_size' => $data_size, + 'data_unit' => $data_unit, + 'index_size' => isset($index_size) ? $index_size : null, + 'index_unit' => isset($index_unit) ? $index_unit : null, + 'innodb_file_per_table' => $innodb_file_per_table, + 'free_size' => isset($free_size) ? $free_size : null, + 'free_unit' => isset($free_unit) ? $free_unit : null, + 'effect_size' => $effect_size, + 'effect_unit' => $effect_unit, + 'tot_size' => $tot_size, + 'tot_unit' => $tot_unit, + 'table' => $GLOBALS['table'], + ]); + } + + /** + * Gets table primary key + * + * @return string + */ + protected function getKeyForTablePrimary() + { + $this->dbi->selectDb($this->db); + $result = $this->dbi->query( + 'SHOW KEYS FROM ' . Util::backquote($this->table) . ';' + ); + $primary = ''; + while ($row = $this->dbi->fetchAssoc($result)) { + // Backups the list of primary keys + if ($row['Key_name'] == 'PRIMARY') { + $primary .= $row['Column_name'] . ', '; + } + } // end while + $this->dbi->freeResult($result); + + return $primary; + } + + /** + * Get List of information for Submit Mult + * + * @param string $submit_mult mult_submit type + * @param array $selected the selected columns + * @param string $action action type + * @param ContainerBuilder $containerBuilder Container builder instance + * + * @return array + */ + protected function getDataForSubmitMult($submit_mult, $selected, $action, ContainerBuilder $containerBuilder) + { + $centralColumns = new CentralColumns($this->dbi); + $what = null; + $query_type = null; + $is_unset_submit_mult = false; + $mult_btn = null; + $centralColsError = null; + switch ($submit_mult) { + case 'drop': + $what = 'drop_fld'; + break; + case 'primary': + // Gets table primary key + $primary = $this->getKeyForTablePrimary(); + if (empty($primary)) { + // no primary key, so we can safely create new + $is_unset_submit_mult = true; + $query_type = 'primary_fld'; + $mult_btn = __('Yes'); + } else { + // primary key exists, so lets as user + $what = 'primary_fld'; + } + break; + case 'index': + $is_unset_submit_mult = true; + $query_type = 'index_fld'; + $mult_btn = __('Yes'); + break; + case 'unique': + $is_unset_submit_mult = true; + $query_type = 'unique_fld'; + $mult_btn = __('Yes'); + break; + case 'spatial': + $is_unset_submit_mult = true; + $query_type = 'spatial_fld'; + $mult_btn = __('Yes'); + break; + case 'ftext': + $is_unset_submit_mult = true; + $query_type = 'fulltext_fld'; + $mult_btn = __('Yes'); + break; + case 'add_to_central_columns': + $centralColsError = $centralColumns->syncUniqueColumns( + $selected, + false + ); + break; + case 'remove_from_central_columns': + $centralColsError = $centralColumns->deleteColumnsFromList( + $_POST['db'], + $selected, + false + ); + break; + case 'change': + $this->displayHtmlForColumnChange($selected, $action, $containerBuilder); + // execution stops here but PhpMyAdmin\Response correctly finishes + // the rendering + exit; + case 'browse': + // this should already be handled by tbl_structure.php + } + + return [ + $what, + $query_type, + $is_unset_submit_mult, + $mult_btn, + $centralColsError, + ]; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php b/srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php new file mode 100644 index 0000000..621961f --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Controllers/TransformationOverviewController.php @@ -0,0 +1,80 @@ +transformations = $transformations; + } + + /** + * @return string HTML + */ + public function indexAction(): string + { + $types = $this->transformations->getAvailableMimeTypes(); + + $mimeTypes = []; + foreach ($types['mimetype'] as $mimeType) { + $mimeTypes[] = [ + 'name' => $mimeType, + 'is_empty' => isset($types['empty_mimetype'][$mimeType]), + ]; + } + + $transformations = [ + 'transformation' => [], + 'input_transformation' => [], + ]; + + foreach (array_keys($transformations) as $type) { + foreach ($types[$type] as $key => $transformation) { + $transformations[$type][] = [ + 'name' => $transformation, + 'description' => $this->transformations->getDescription( + $types[$type . '_file'][$key] + ), + ]; + } + } + + return $this->template->render('transformation_overview', [ + 'mime_types' => $mimeTypes, + 'transformations' => $transformations, + ]); + } +} -- cgit