diff options
Diffstat (limited to 'srcs/phpmyadmin/libraries/classes/Navigation')
24 files changed, 4814 insertions, 0 deletions
diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/Navigation.php b/srcs/phpmyadmin/libraries/classes/Navigation/Navigation.php new file mode 100644 index 0000000..49d40df --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/Navigation.php @@ -0,0 +1,280 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * This class is responsible for instantiating + * the various components of the navigation panel + * + * @package PhpMyAdmin-navigation + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Navigation; + +use PhpMyAdmin\Config\PageSettings; +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Relation; +use PhpMyAdmin\Response; +use PhpMyAdmin\Sanitize; +use PhpMyAdmin\Server\Select; +use PhpMyAdmin\Template; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * The navigation panel - displays server, db and table selection tree + * + * @package PhpMyAdmin-Navigation + */ +class Navigation +{ + /** + * @var Template + */ + private $template; + + /** + * @var Relation + */ + private $relation; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * @var NavigationTree + */ + private $tree; + + /** + * Navigation constructor. + * @param Template $template Template instance + * @param Relation $relation Relation instance + * @param DatabaseInterface $dbi DatabaseInterface instance + */ + public function __construct($template, $relation, $dbi) + { + $this->template = $template; + $this->relation = $relation; + $this->dbi = $dbi; + $this->tree = new NavigationTree($this->template, $this->dbi); + } + + /** + * Renders the navigation tree, or part of it + * + * @return string The navigation tree + */ + public function getDisplay(): string + { + global $cfg; + + $logo = [ + 'is_displayed' => $cfg['NavigationDisplayLogo'], + 'has_link' => false, + 'link' => '#', + 'attributes' => ' target="_blank" rel="noopener noreferrer"', + 'source' => '', + ]; + + $response = Response::getInstance(); + if (! $response->isAjax()) { + $logo['source'] = $this->getLogoSource(); + $logo['has_link'] = (string) $cfg['NavigationLogoLink'] !== ''; + $logo['link'] = trim((string) $cfg['NavigationLogoLink']); + if (! Sanitize::checkLink($logo['link'], true)) { + $logo['link'] = 'index.php'; + } + if ($cfg['NavigationLogoLinkWindow'] === 'main') { + if (empty(parse_url($logo['link'], PHP_URL_HOST))) { + $hasStartChar = strpos($logo['link'], '?'); + $logo['link'] .= Url::getCommon( + [], + is_bool($hasStartChar) ? '?' : Url::getArgSeparator() + ); + } + $logo['attributes'] = ''; + } + + if ($cfg['NavigationDisplayServers'] && count($cfg['Servers']) > 1) { + $serverSelect = Select::render(true, true); + } + + if (! defined('PMA_DISABLE_NAVI_SETTINGS')) { + $navigationSettings = PageSettings::getNaviSettings(); + } + } + if (! $response->isAjax() + || ! empty($_POST['full']) + || ! empty($_POST['reload']) + ) { + if ($cfg['ShowDatabasesNavigationAsTree']) { + // provide database tree in navigation + $navRender = $this->tree->renderState(); + } else { + // provide legacy pre-4.0 navigation + $navRender = $this->tree->renderDbSelect(); + } + } else { + $navRender = $this->tree->renderPath(); + } + + return $this->template->render('navigation/main', [ + 'is_ajax' => $response->isAjax(), + 'logo' => $logo, + 'is_synced' => $cfg['NavigationLinkWithMainPanel'], + 'is_highlighted' => $cfg['NavigationTreePointerEnable'], + 'is_autoexpanded' => $cfg['NavigationTreeAutoexpandSingleDb'], + 'server' => $GLOBALS['server'], + 'auth_type' => $cfg['Server']['auth_type'], + 'is_servers_displayed' => $cfg['NavigationDisplayServers'], + 'servers' => $cfg['Servers'], + 'server_select' => $serverSelect ?? '', + 'navigation_tree' => $navRender, + 'is_navigation_settings_enabled' => ! defined('PMA_DISABLE_NAVI_SETTINGS'), + 'navigation_settings' => $navigationSettings ?? '', + 'is_drag_drop_import_enabled' => $cfg['enable_drag_drop_import'] === true, + ]); + } + + /** + * Add an item of navigation tree to the hidden items list in PMA database. + * + * @param string $itemName name of the navigation tree item + * @param string $itemType type of the navigation tree item + * @param string $dbName database name + * @param string $tableName table name if applicable + * + * @return void + */ + public function hideNavigationItem( + $itemName, + $itemType, + $dbName, + $tableName = null + ) { + $navTable = Util::backquote($GLOBALS['cfgRelation']['db']) + . "." . Util::backquote($GLOBALS['cfgRelation']['navigationhiding']); + $sqlQuery = "INSERT INTO " . $navTable + . "(`username`, `item_name`, `item_type`, `db_name`, `table_name`)" + . " VALUES (" + . "'" . $this->dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'," + . "'" . $this->dbi->escapeString($itemName) . "'," + . "'" . $this->dbi->escapeString($itemType) . "'," + . "'" . $this->dbi->escapeString($dbName) . "'," + . "'" . (! empty($tableName) ? $this->dbi->escapeString($tableName) : "" ) + . "')"; + $this->relation->queryAsControlUser($sqlQuery, false); + } + + /** + * Remove a hidden item of navigation tree from the + * list of hidden items in PMA database. + * + * @param string $itemName name of the navigation tree item + * @param string $itemType type of the navigation tree item + * @param string $dbName database name + * @param string $tableName table name if applicable + * + * @return void + */ + public function unhideNavigationItem( + $itemName, + $itemType, + $dbName, + $tableName = null + ) { + $navTable = Util::backquote($GLOBALS['cfgRelation']['db']) + . "." . Util::backquote($GLOBALS['cfgRelation']['navigationhiding']); + $sqlQuery = "DELETE FROM " . $navTable + . " WHERE" + . " `username`='" + . $this->dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'" + . " AND `item_name`='" . $this->dbi->escapeString($itemName) . "'" + . " AND `item_type`='" . $this->dbi->escapeString($itemType) . "'" + . " AND `db_name`='" . $this->dbi->escapeString($dbName) . "'" + . (! empty($tableName) + ? " AND `table_name`='" . $this->dbi->escapeString($tableName) . "'" + : "" + ); + $this->relation->queryAsControlUser($sqlQuery, false); + } + + /** + * Returns HTML for the dialog to show hidden navigation items. + * + * @param string $database database name + * @param string $itemType type of the items to include + * @param string $table table name + * + * @return string HTML for the dialog to show hidden navigation items + */ + public function getItemUnhideDialog($database, $itemType = null, $table = null) + { + $hidden = $this->getHiddenItems($database, $table); + + $typeMap = [ + 'group' => __('Groups:'), + 'event' => __('Events:'), + 'function' => __('Functions:'), + 'procedure' => __('Procedures:'), + 'table' => __('Tables:'), + 'view' => __('Views:'), + ]; + + return $this->template->render('navigation/item_unhide_dialog', [ + 'database' => $database, + 'table' => $table, + 'hidden' => $hidden, + 'types' => $typeMap, + 'item_type' => $itemType, + ]); + } + + /** + * @param string $database Database name + * @param string|null $table Table name + * @return array + */ + private function getHiddenItems(string $database, ?string $table): array + { + $navTable = Util::backquote($GLOBALS['cfgRelation']['db']) + . "." . Util::backquote($GLOBALS['cfgRelation']['navigationhiding']); + $sqlQuery = "SELECT `item_name`, `item_type` FROM " . $navTable + . " WHERE `username`='" + . $this->dbi->escapeString($GLOBALS['cfg']['Server']['user']) . "'" + . " AND `db_name`='" . $this->dbi->escapeString($database) . "'" + . " AND `table_name`='" + . (! empty($table) ? $this->dbi->escapeString($table) : '') . "'"; + $result = $this->relation->queryAsControlUser($sqlQuery, false); + + $hidden = []; + if ($result) { + while ($row = $this->dbi->fetchArray($result)) { + $type = $row['item_type']; + if (! isset($hidden[$type])) { + $hidden[$type] = []; + } + $hidden[$type][] = $row['item_name']; + } + } + $this->dbi->freeResult($result); + return $hidden; + } + + /** + * @return string Logo source + */ + private function getLogoSource(): string + { + global $pmaThemeImage; + + if (isset($pmaThemeImage) && @file_exists($pmaThemeImage . 'logo_left.png')) { + return $pmaThemeImage . 'logo_left.png'; + } elseif (isset($pmaThemeImage) && @file_exists($pmaThemeImage . 'pma_logo2.png')) { + return $pmaThemeImage . 'pma_logo2.png'; + } + return ''; + } +} diff --git a/srcs/phpmyadmin/libraries/classes/Navigation/NavigationTree.php b/srcs/phpmyadmin/libraries/classes/Navigation/NavigationTree.php new file mode 100644 index 0000000..289ed6e --- /dev/null +++ b/srcs/phpmyadmin/libraries/classes/Navigation/NavigationTree.php @@ -0,0 +1,1581 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * Functionality for the navigation tree + * + * @package PhpMyAdmin-Navigation + */ +declare(strict_types=1); + +namespace PhpMyAdmin\Navigation; + +use PhpMyAdmin\CheckUserPrivileges; +use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Navigation\Nodes\Node; +use PhpMyAdmin\Navigation\Nodes\NodeDatabase; +use PhpMyAdmin\Navigation\Nodes\NodeTable; +use PhpMyAdmin\Navigation\Nodes\NodeTableContainer; +use PhpMyAdmin\Navigation\Nodes\NodeViewContainer; +use PhpMyAdmin\RecentFavoriteTable; +use PhpMyAdmin\Response; +use PhpMyAdmin\Template; +use PhpMyAdmin\Url; +use PhpMyAdmin\Util; + +/** + * Displays a collapsible of database objects in the navigation frame + * + * @package PhpMyAdmin-Navigation + */ +class NavigationTree +{ + /** + * @var Node Reference to the root node of the tree + */ + private $tree; + /** + * @var array The actual paths to all expanded nodes in the tree + * This does not include nodes created after the grouping + * of nodes has been performed + */ + private $aPath = []; + /** + * @var array The virtual paths to all expanded nodes in the tree + * This includes nodes created after the grouping of + * nodes has been performed + */ + private $vPath = []; + /** + * @var int Position in the list of databases, + * used for pagination + */ + private $pos; + /** + * @var array The names of the type of items that are being paginated on + * the second level of the navigation tree. These may be + * tables, views, functions, procedures or events. + */ + private $pos2Name = []; + /** + * @var array The positions of nodes in the lists of tables, views, + * routines or events used for pagination + */ + private $pos2Value = []; + /** + * @var array The names of the type of items that are being paginated + * on the second level of the navigation tree. + * These may be columns or indexes + */ + private $pos3Name = []; + /** + * @var array The positions of nodes in the lists of columns or indexes + * used for pagination + */ + private $pos3Value = []; + /** + * @var string The search clause to use in SQL queries for + * fetching databases + * Used by the asynchronous fast filter + */ + private $searchClause = ''; + /** + * @var string The search clause to use in SQL queries for + * fetching nodes + * Used by the asynchronous fast filter + */ + private $searchClause2 = ''; + /** + * @var bool Whether a warning was raised for large item groups + * which can affect performance. + */ + private $largeGroupWarning = false; + + /** + * @var Template + */ + private $template; + + /** + * @var DatabaseInterface + */ + private $dbi; + + /** + * NavigationTree constructor. + * @param Template $template Template instance + * @param DatabaseInterface $dbi DatabaseInterface instance + */ + public function __construct($template, $dbi) + { + $this->template = $template; + $this->dbi = $dbi; + + $checkUserPrivileges = new CheckUserPrivileges($this->dbi); + $checkUserPrivileges->getPrivileges(); + + // Save the position at which we are in the database list + if (isset($_POST['pos'])) { + $this->pos = (int) $_POST['pos']; + } elseif (isset($_GET['pos'])) { + $this->pos = (int) $_GET['pos']; + } + if (! isset($this->pos)) { + $this->pos = $this->getNavigationDbPos(); + } + // Get the active node + if (isset($_REQUEST['aPath'])) { + $this->aPath[0] = $this->parsePath($_REQUEST['aPath']); + $this->pos2Name[0] = $_REQUEST['pos2_name']; + $this->pos2Value[0] = $_REQUEST['pos2_value']; + if (isset($_REQUEST['pos3_name'])) { + $this->pos3Name[0] = $_REQUEST['pos3_name']; + $this->pos3Value[0] = $_REQUEST['pos3_value']; + } + } else { + if (isset($_POST['n0_aPath'])) { + $count = 0; + while (isset($_POST['n' . $count . '_aPath'])) { + $this->aPath[$count] = $this->parsePath( + $_POST['n' . $count . '_aPath'] + ); + $index = 'n' . $count . '_pos2_'; + $this->pos2Name[$count] = $_POST[$index . 'name']; + $this->pos2Value[$count] = $_POST[$index . 'value']; + $index = 'n' . $count . '_pos3_'; + if (isset($_POST[$index])) { + $this->pos3Name[$count] = $_POST[$index . 'name']; + $this->pos3Value[$count] = $_POST[$index . 'value']; + } + $count++; + } + } + } + if (isset($_REQUEST['vPath'])) { + $this->vPath[0] = $this->parsePath($_REQUEST['vPath']); + } else { + if (isset($_POST['n0_vPath'])) { + $count = 0; + while (isset($_POST['n' . $count . '_vPath'])) { + $this->vPath[$count] = $this->parsePath( + $_POST['n' . $count . '_vPath'] + ); + $count++; + } + } + } + if (isset($_REQUEST['searchClause'])) { + $this->searchClause = $_REQUEST['searchClause']; + } + if (isset($_REQUEST['searchClause2'])) { + $this->searchClause2 = $_REQUEST['searchClause2']; + } + // Initialise the tree by creating a root node + $node = NodeFactory::getInstance('NodeDatabaseContainer', 'root'); + $this->tree = $node; + if ($GLOBALS['cfg']['NavigationTreeEnableGrouping'] + && $GLOBALS['cfg']['ShowDatabasesNavigationAsTree'] + ) { + $this->tree->separator = $GLOBALS['cfg']['NavigationTreeDbSeparator']; + $this->tree->separatorDepth = 10000; + } + } + + /** + * Returns the database position for the page selector + * + * @return int + */ + private function getNavigationDbPos() + { + $retval = 0; + + if (strlen($GLOBALS['db']) == 0) { + return $retval; + } + + /* + * @todo describe a scenario where this code is executed + */ + if (! $GLOBALS['cfg']['Server']['DisableIS']) { + $dbSeparator = $this->dbi->escapeString( + $GLOBALS['cfg']['NavigationTreeDbSeparator'] + ); + $query = "SELECT (COUNT(DB_first_level) DIV %d) * %d "; + $query .= "from ( "; + $query .= " SELECT distinct SUBSTRING_INDEX(SCHEMA_NAME, "; + $query .= " '%s', 1) "; + $query .= " DB_first_level "; + $query .= " FROM INFORMATION_SCHEMA.SCHEMATA "; + $query .= " WHERE `SCHEMA_NAME` < '%s' "; + $query .= ") t "; + + $retval = $this->dbi->fetchValue( + sprintf( + $query, + (int) $GLOBALS['cfg']['FirstLevelNavigationItems'], + (int) $GLOBALS['cfg']['FirstLevelNavigationItems'], + $dbSeparator, + $this->dbi->escapeString($GLOBALS['db']) + ) + ); + + return $retval; + } + + $prefixMap = []; + if ($GLOBALS['dbs_to_test'] === false) { + $handle = $this->dbi->tryQuery("SHOW DATABASES"); + if ($handle !== false) { + while ($arr = $this->dbi->fetchArray($handle)) { + if (strcasecmp($arr[0], $GLOBALS['db']) >= 0) { + break; + } + + $prefix = strstr( + $arr[0], + $GLOBALS['cfg']['NavigationTreeDbSeparator'], + true + ); + if ($prefix === false) { + $prefix = $arr[0]; + } + $prefixMap[$prefix] = 1; + } + } + } else { + $databases = []; + foreach ($GLOBALS['dbs_to_test'] as $db) { + $query = "SHOW DATABASES LIKE '" . $db . "'"; + $handle = $this->dbi->tryQuery($query); + if ($handle === false) { + continue; + } + while ($arr = $this->dbi->fetchArray($handle)) { + $databases[] = $arr[0]; + } + } + sort($databases); + foreach ($databases as $database) { + if (strcasecmp($database, $GLOBALS['db']) >= 0) { + break; + } + + $prefix = strstr( + $database, + $GLOBALS['cfg']['NavigationTreeDbSeparator'], + true + ); + if ($prefix === false) { + $prefix = $database; + } + $prefixMap[$prefix] = 1; + } + } + + $navItems = (int) $GLOBALS['cfg']['FirstLevelNavigationItems']; + $retval = (int) floor(count($prefixMap) / $navItems) * $navItems; + + return $retval; + } + + /** + * Converts an encoded path to a node in string format to an array + * + * @param string $string The path to parse + * + * @return array + */ + private function parsePath($string) + { + $path = explode('.', $string); + foreach ($path as $key => $value) { + $path[$key] = base64_decode($value); + } + + return $path; + } + + /** + * Generates the tree structure so that it can be rendered later + * + * @return Node|false The active node or false in case of failure + */ + private function buildPath() + { + $retval = $this->tree; + + // Add all databases unconditionally + $data = $this->tree->getData( + 'databases', + $this->pos, + $this->searchClause + ); + $hiddenCounts = $this->tree->getNavigationHidingData(); + foreach ($data as $db) { + $node = NodeFactory::getInstance('NodeDatabase', $db); + if (isset($hiddenCounts[$db])) { + $node->setHiddenCount($hiddenCounts[$db]); + } + $this->tree->addChild($node); + } + + // Whether build other parts of the tree depends + // on whether we have any paths in $this->_aPath + foreach ($this->aPath as $key => $path) { + $retval = $this->buildPathPart( + $path, + $this->pos2Name[$key], + $this->pos2Value[$key], + isset($this->pos3Name[$key]) ? $this->pos3Name[$key] : '', + isset($this->pos3Value[$key]) ? $this->pos3Value[$key] : '' + ); + } + + return $retval; + } + + /** + * Builds a branch of the tree + * + * @param array $path A paths pointing to the branch + * of the tree that needs to be built + * @param string $type2 The type of item being paginated on + * the second level of the tree + * @param int $pos2 The position for the pagination of + * the branch at the second level of the tree + * @param string $type3 The type of item being paginated on + * the third level of the tree + * @param int $pos3 The position for the pagination of + * the branch at the third level of the tree + * + * @return Node|bool The active node or false in case of failure, true if the path contains <= 1 items + */ + private function buildPathPart(array $path, $type2, $pos2, $type3, $pos3) + { + if (empty($pos2)) { + $pos2 = 0; + } + if (empty($pos3)) { + $pos3 = 0; + } + + $retval = true; + if (count($path) <= 1) { + return $retval; + } + + array_shift($path); // remove 'root' + /** @var NodeDatabase $db */ + $db = $this->tree->getChild($path[0]); + $retval = $db; + + if ($db === false) { + return false; + } + + $containers = $this->addDbContainers($db, $type2, $pos2); + + array_shift($path); // remove db + + if ((count($path) <= 0 || ! array_key_exists($path[0], $containers)) + && count($containers) != 1 + ) { + return $retval; + } + + if (count($containers) === 1) { + $container = array_shift($containers); + } else { + $container = $db->getChild($path[0], true); + if ($container === false) { + return false; + } + } + $retval = $container; + + if (count($container->children) <= 1) { + $dbData = $db->getData( + $container->realName, + $pos2, + $this->searchClause2 + ); + foreach ($dbData as $item) { + switch ($container->realName) { + case 'events': + $node = NodeFactory::getInstance( + 'NodeEvent', + $item + ); + break; + case 'functions': + $node = NodeFactory::getInstance( + 'NodeFunction', + $item + ); + break; + case 'procedures': + $node = NodeFactory::getInstance( + 'NodeProcedure', + $item + ); + break; + case 'tables': + $node = NodeFactory::getInstance( + 'NodeTable', + $item + ); + break; + case 'views': + $node = NodeFactory::getInstance( + 'NodeView', + $item + ); + break; + default: + break; + } + if (isset($node)) { + if ($type2 == $container->realName) { + $node->pos2 = $pos2; + } + $container->addChild($node); + } + } + } + if (count($path) > 1 && $path[0] != 'tables') { + $retval = false; + + return $retval; + } + + array_shift($path); // remove container + if (count($path) <= 0) { + return $retval; + } + + /** @var NodeTable $table */ + $table = $container->getChild($path[0], true); + if ($table === false) { + if (! $db->getPresence('tables', $path[0])) { + return false; + } + + $node = NodeFactory::getInstance( + 'NodeTable', + $path[0] + ); + if ($type2 == $container->realName) { + $node->pos2 = $pos2; + } + $container->addChild($node); + $table = $container->getChild($path[0], true); + } + $retval = $table; + $containers = $this->addTableContainers( + $table, + $pos2, + $type3, + $pos3 + ); + array_shift($path); // remove table + if (count($path) <= 0 + || ! array_key_exists($path[0], $containers) + ) { + return $retval; + } + + $container = $table->getChild($path[0], true); + $retval = $container; + $tableData = $table->getData( + $container->realName, + $pos3 + ); + foreach ($tableData as $item) { + switch ($container->realName) { + case 'indexes': + $node = NodeFactory::getInstance( + 'NodeIndex', + $item + ); + break; + case 'columns': + $node = NodeFactory::getInstance( + 'NodeColumn', + $item + ); + break; + case 'triggers': + $node = NodeFactory::getInstance( + 'NodeTrigger', + $item + ); + break; + default: + break; + } + if (isset($node)) { + $node->pos2 = $container->parent->pos2; + if ($type3 == $container-> |
