PageRenderTime 40ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/libraries/classes/Navigation/NavigationTree.php

http://github.com/phpmyadmin/phpmyadmin
PHP | 1467 lines | 1113 code | 152 blank | 202 comment | 180 complexity | 4682e2b9d3ccbd75aa70c943ed558b9b MD5 | raw file
Possible License(s): GPL-2.0, MIT, LGPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Functionality for the navigation tree
  4. */
  5. declare(strict_types=1);
  6. namespace PhpMyAdmin\Navigation;
  7. use PhpMyAdmin\CheckUserPrivileges;
  8. use PhpMyAdmin\DatabaseInterface;
  9. use PhpMyAdmin\Html\Generator;
  10. use PhpMyAdmin\Navigation\Nodes\Node;
  11. use PhpMyAdmin\Navigation\Nodes\NodeDatabase;
  12. use PhpMyAdmin\Navigation\Nodes\NodeTable;
  13. use PhpMyAdmin\Navigation\Nodes\NodeTableContainer;
  14. use PhpMyAdmin\Navigation\Nodes\NodeViewContainer;
  15. use PhpMyAdmin\RecentFavoriteTable;
  16. use PhpMyAdmin\ResponseRenderer;
  17. use PhpMyAdmin\Template;
  18. use PhpMyAdmin\Url;
  19. use function __;
  20. use function _ngettext;
  21. use function array_intersect_key;
  22. use function array_key_exists;
  23. use function array_keys;
  24. use function array_merge;
  25. use function array_shift;
  26. use function base64_decode;
  27. use function count;
  28. use function explode;
  29. use function floor;
  30. use function get_class;
  31. use function htmlspecialchars;
  32. use function in_array;
  33. use function is_array;
  34. use function is_bool;
  35. use function is_object;
  36. use function mb_strlen;
  37. use function mb_strpos;
  38. use function mb_substr;
  39. use function method_exists;
  40. use function sort;
  41. use function sprintf;
  42. use function strcasecmp;
  43. use function strlen;
  44. use function strnatcasecmp;
  45. use function strrpos;
  46. use function strstr;
  47. use function substr;
  48. use function trigger_error;
  49. use function trim;
  50. use function usort;
  51. use const E_USER_WARNING;
  52. /**
  53. * Displays a collapsible of database objects in the navigation frame
  54. */
  55. class NavigationTree
  56. {
  57. private const SPECIAL_NODE_NAMES = ['tables', 'views', 'functions', 'procedures', 'events'];
  58. /** @var Node Reference to the root node of the tree */
  59. private $tree;
  60. /**
  61. * @var array The actual paths to all expanded nodes in the tree
  62. * This does not include nodes created after the grouping
  63. * of nodes has been performed
  64. */
  65. private $aPath = [];
  66. /**
  67. * @var array The virtual paths to all expanded nodes in the tree
  68. * This includes nodes created after the grouping of
  69. * nodes has been performed
  70. */
  71. private $vPath = [];
  72. /**
  73. * @var int Position in the list of databases,
  74. * used for pagination
  75. */
  76. private $pos;
  77. /**
  78. * @var string[] The names of the type of items that are being paginated on
  79. * the second level of the navigation tree. These may be
  80. * tables, views, functions, procedures or events.
  81. */
  82. private $pos2Name = [];
  83. /**
  84. * @var int[] The positions of nodes in the lists of tables, views,
  85. * routines or events used for pagination
  86. */
  87. private $pos2Value = [];
  88. /**
  89. * @var string[] The names of the type of items that are being paginated
  90. * on the second level of the navigation tree.
  91. * These may be columns or indexes
  92. */
  93. private $pos3Name = [];
  94. /**
  95. * @var int[] The positions of nodes in the lists of columns or indexes
  96. * used for pagination
  97. */
  98. private $pos3Value = [];
  99. /**
  100. * @var string The search clause to use in SQL queries for
  101. * fetching databases
  102. * Used by the asynchronous fast filter
  103. */
  104. private $searchClause = '';
  105. /**
  106. * @var string The search clause to use in SQL queries for
  107. * fetching nodes
  108. * Used by the asynchronous fast filter
  109. */
  110. private $searchClause2 = '';
  111. /**
  112. * @var bool Whether a warning was raised for large item groups
  113. * which can affect performance.
  114. */
  115. private $largeGroupWarning = false;
  116. /** @var Template */
  117. private $template;
  118. /** @var DatabaseInterface */
  119. private $dbi;
  120. /**
  121. * @param Template $template Template instance
  122. * @param DatabaseInterface $dbi DatabaseInterface instance
  123. */
  124. public function __construct($template, DatabaseInterface $dbi)
  125. {
  126. $this->template = $template;
  127. $this->dbi = $dbi;
  128. $checkUserPrivileges = new CheckUserPrivileges($this->dbi);
  129. $checkUserPrivileges->getPrivileges();
  130. // Save the position at which we are in the database list
  131. if (isset($_POST['pos'])) {
  132. $this->pos = (int) $_POST['pos'];
  133. } elseif (isset($_GET['pos'])) {
  134. $this->pos = (int) $_GET['pos'];
  135. }
  136. if (! isset($this->pos)) {
  137. $this->pos = $this->getNavigationDbPos();
  138. }
  139. // Get the active node
  140. if (isset($_REQUEST['aPath'])) {
  141. $this->aPath[0] = $this->parsePath($_REQUEST['aPath']);
  142. $this->pos2Name[0] = $_REQUEST['pos2_name'] ?? '';
  143. $this->pos2Value[0] = (int) ($_REQUEST['pos2_value'] ?? 0);
  144. if (isset($_REQUEST['pos3_name'])) {
  145. $this->pos3Name[0] = $_REQUEST['pos3_name'] ?? '';
  146. $this->pos3Value[0] = (int) $_REQUEST['pos3_value'];
  147. }
  148. } else {
  149. if (isset($_POST['n0_aPath'])) {
  150. $count = 0;
  151. while (isset($_POST['n' . $count . '_aPath'])) {
  152. $this->aPath[$count] = $this->parsePath($_POST['n' . $count . '_aPath']);
  153. if (isset($_POST['n' . $count . '_pos2_name'])) {
  154. $this->pos2Name[$count] = $_POST['n' . $count . '_pos2_name'];
  155. $this->pos2Value[$count] = (int) $_POST['n' . $count . '_pos2_value'];
  156. }
  157. if (isset($_POST['n' . $count . '_pos3_name'])) {
  158. $this->pos3Name[$count] = $_POST['n' . $count . '_pos3_name'];
  159. $this->pos3Value[$count] = (int) $_POST['n' . $count . '_pos3_value'];
  160. }
  161. $count++;
  162. }
  163. }
  164. }
  165. if (isset($_REQUEST['vPath'])) {
  166. $this->vPath[0] = $this->parsePath($_REQUEST['vPath']);
  167. } else {
  168. if (isset($_POST['n0_vPath'])) {
  169. $count = 0;
  170. while (isset($_POST['n' . $count . '_vPath'])) {
  171. $this->vPath[$count] = $this->parsePath($_POST['n' . $count . '_vPath']);
  172. $count++;
  173. }
  174. }
  175. }
  176. if (isset($_REQUEST['searchClause'])) {
  177. $this->searchClause = $_REQUEST['searchClause'];
  178. }
  179. if (isset($_REQUEST['searchClause2'])) {
  180. $this->searchClause2 = $_REQUEST['searchClause2'];
  181. }
  182. // Initialize the tree by creating a root node
  183. $node = NodeFactory::getInstance('NodeDatabaseContainer', 'root');
  184. $this->tree = $node;
  185. if (! $GLOBALS['cfg']['NavigationTreeEnableGrouping'] || ! $GLOBALS['cfg']['ShowDatabasesNavigationAsTree']) {
  186. return;
  187. }
  188. $this->tree->separator = $GLOBALS['cfg']['NavigationTreeDbSeparator'];
  189. $this->tree->separatorDepth = 10000;
  190. }
  191. /**
  192. * Returns the database position for the page selector
  193. */
  194. private function getNavigationDbPos(): int
  195. {
  196. $retval = 0;
  197. if (strlen($GLOBALS['db'] ?? '') === 0) {
  198. return $retval;
  199. }
  200. /*
  201. * @todo describe a scenario where this code is executed
  202. */
  203. if (! $GLOBALS['cfg']['Server']['DisableIS']) {
  204. $dbSeparator = $this->dbi->escapeString($GLOBALS['cfg']['NavigationTreeDbSeparator']);
  205. $query = 'SELECT (COUNT(DB_first_level) DIV %d) * %d ';
  206. $query .= 'from ( ';
  207. $query .= ' SELECT distinct SUBSTRING_INDEX(SCHEMA_NAME, ';
  208. $query .= " '%s', 1) ";
  209. $query .= ' DB_first_level ';
  210. $query .= ' FROM INFORMATION_SCHEMA.SCHEMATA ';
  211. $query .= " WHERE `SCHEMA_NAME` < '%s' ";
  212. $query .= ') t ';
  213. return (int) $this->dbi->fetchValue(
  214. sprintf(
  215. $query,
  216. (int) $GLOBALS['cfg']['FirstLevelNavigationItems'],
  217. (int) $GLOBALS['cfg']['FirstLevelNavigationItems'],
  218. $dbSeparator,
  219. $this->dbi->escapeString($GLOBALS['db'])
  220. )
  221. );
  222. }
  223. $prefixMap = [];
  224. if ($GLOBALS['dbs_to_test'] === false) {
  225. $handle = $this->dbi->tryQuery('SHOW DATABASES');
  226. if ($handle !== false) {
  227. while ($arr = $this->dbi->fetchArray($handle)) {
  228. if (strcasecmp($arr[0], $GLOBALS['db']) >= 0) {
  229. break;
  230. }
  231. $prefix = strstr($arr[0], $GLOBALS['cfg']['NavigationTreeDbSeparator'], true);
  232. if ($prefix === false) {
  233. $prefix = $arr[0];
  234. }
  235. $prefixMap[$prefix] = 1;
  236. }
  237. }
  238. } else {
  239. $databases = [];
  240. foreach ($GLOBALS['dbs_to_test'] as $db) {
  241. $query = "SHOW DATABASES LIKE '" . $db . "'";
  242. $handle = $this->dbi->tryQuery($query);
  243. if ($handle === false) {
  244. continue;
  245. }
  246. while ($arr = $this->dbi->fetchArray($handle)) {
  247. $databases[] = $arr[0];
  248. }
  249. }
  250. sort($databases);
  251. foreach ($databases as $database) {
  252. if (strcasecmp($database, $GLOBALS['db']) >= 0) {
  253. break;
  254. }
  255. $prefix = strstr($database, $GLOBALS['cfg']['NavigationTreeDbSeparator'], true);
  256. if ($prefix === false) {
  257. $prefix = $database;
  258. }
  259. $prefixMap[$prefix] = 1;
  260. }
  261. }
  262. $navItems = (int) $GLOBALS['cfg']['FirstLevelNavigationItems'];
  263. return (int) floor(count($prefixMap) / $navItems) * $navItems;
  264. }
  265. /**
  266. * Converts an encoded path to a node in string format to an array
  267. *
  268. * @param string $string The path to parse
  269. *
  270. * @return array
  271. */
  272. private function parsePath($string): array
  273. {
  274. $path = explode('.', $string);
  275. foreach ($path as $key => $value) {
  276. $path[$key] = base64_decode($value);
  277. }
  278. return $path;
  279. }
  280. /**
  281. * Generates the tree structure so that it can be rendered later
  282. *
  283. * @return Node|bool The active node or false in case of failure, or true: (@see buildPathPart())
  284. */
  285. private function buildPath()
  286. {
  287. $retval = $this->tree;
  288. // Add all databases unconditionally
  289. $data = $this->tree->getData('databases', $this->pos, $this->searchClause);
  290. $hiddenCounts = $this->tree->getNavigationHidingData();
  291. foreach ($data as $db) {
  292. /** @var NodeDatabase $node */
  293. $node = NodeFactory::getInstance('NodeDatabase', $db);
  294. if (isset($hiddenCounts[$db])) {
  295. $node->setHiddenCount($hiddenCounts[$db]);
  296. }
  297. $this->tree->addChild($node);
  298. }
  299. // Whether build other parts of the tree depends
  300. // on whether we have any paths in $this->aPath
  301. foreach ($this->aPath as $key => $path) {
  302. $retval = $this->buildPathPart(
  303. $path,
  304. $this->pos2Name[$key] ?? '',
  305. $this->pos2Value[$key] ?? 0,
  306. $this->pos3Name[$key] ?? '',
  307. $this->pos3Value[$key] ?? 0
  308. );
  309. }
  310. return $retval;
  311. }
  312. /**
  313. * Builds a branch of the tree
  314. *
  315. * @param array $path A paths pointing to the branch
  316. * of the tree that needs to be built
  317. * @param string $type2 The type of item being paginated on
  318. * the second level of the tree
  319. * @param int $pos2 The position for the pagination of
  320. * the branch at the second level of the tree
  321. * @param string $type3 The type of item being paginated on
  322. * the third level of the tree
  323. * @param int $pos3 The position for the pagination of
  324. * the branch at the third level of the tree
  325. *
  326. * @return Node|bool The active node or false in case of failure, true if the path contains <= 1 items
  327. */
  328. private function buildPathPart(array $path, string $type2, int $pos2, string $type3, int $pos3)
  329. {
  330. if (count($path) <= 1) {
  331. return true;
  332. }
  333. array_shift($path); // remove 'root'
  334. /** @var NodeDatabase|null $db */
  335. $db = $this->tree->getChild($path[0]);
  336. if ($db === null) {
  337. return false;
  338. }
  339. $retval = $db;
  340. $containers = $this->addDbContainers($db, $type2, $pos2);
  341. array_shift($path); // remove db
  342. if ((count($path) <= 0 || ! array_key_exists($path[0], $containers)) && count($containers) != 1) {
  343. return $retval;
  344. }
  345. if (count($containers) === 1) {
  346. $container = array_shift($containers);
  347. } else {
  348. $container = $db->getChild($path[0], true);
  349. if ($container === null) {
  350. return false;
  351. }
  352. }
  353. $retval = $container;
  354. if (count($container->children) <= 1) {
  355. $dbData = $db->getData($container->realName, $pos2, $this->searchClause2);
  356. foreach ($dbData as $item) {
  357. switch ($container->realName) {
  358. case 'events':
  359. $node = NodeFactory::getInstance('NodeEvent', $item);
  360. break;
  361. case 'functions':
  362. $node = NodeFactory::getInstance('NodeFunction', $item);
  363. break;
  364. case 'procedures':
  365. $node = NodeFactory::getInstance('NodeProcedure', $item);
  366. break;
  367. case 'tables':
  368. $node = NodeFactory::getInstance('NodeTable', $item);
  369. break;
  370. case 'views':
  371. $node = NodeFactory::getInstance('NodeView', $item);
  372. break;
  373. default:
  374. break;
  375. }
  376. if (! isset($node)) {
  377. continue;
  378. }
  379. if ($type2 == $container->realName) {
  380. $node->pos2 = $pos2;
  381. }
  382. $container->addChild($node);
  383. }
  384. }
  385. if (count($path) > 1 && $path[0] !== 'tables') {
  386. return false;
  387. }
  388. array_shift($path); // remove container
  389. if (count($path) <= 0) {
  390. return $retval;
  391. }
  392. /** @var NodeTable|null $table */
  393. $table = $container->getChild($path[0], true);
  394. if ($table === null) {
  395. if (! $db->getPresence('tables', $path[0])) {
  396. return false;
  397. }
  398. $node = NodeFactory::getInstance('NodeTable', $path[0]);
  399. if ($type2 == $container->realName) {
  400. $node->pos2 = $pos2;
  401. }
  402. $container->addChild($node);
  403. $table = $container->getChild($path[0], true);
  404. }
  405. $retval = $table ?? false;
  406. $containers = $this->addTableContainers($table, $pos2, $type3, $pos3);
  407. array_shift($path); // remove table
  408. if (count($path) <= 0 || ! array_key_exists($path[0], $containers)) {
  409. return $retval;
  410. }
  411. $container = $table->getChild($path[0], true);
  412. $retval = $container ?? false;
  413. $tableData = $table->getData($container->realName, $pos3);
  414. foreach ($tableData as $item) {
  415. switch ($container->realName) {
  416. case 'indexes':
  417. $node = NodeFactory::getInstance('NodeIndex', $item);
  418. break;
  419. case 'columns':
  420. $node = NodeFactory::getInstance('NodeColumn', $item);
  421. break;
  422. case 'triggers':
  423. $node = NodeFactory::getInstance('NodeTrigger', $item);
  424. break;
  425. default:
  426. break;
  427. }
  428. if (! isset($node)) {
  429. continue;
  430. }
  431. $node->pos2 = $container->parent->pos2;
  432. if ($type3 == $container->realName) {
  433. $node->pos3 = $pos3;
  434. }
  435. $container->addChild($node);
  436. }
  437. return $retval;
  438. }
  439. /**
  440. * Adds containers to a node that is a table
  441. *
  442. * References to existing children are returned
  443. * if this function is called twice on the same node
  444. *
  445. * @param NodeTable $table The table node, new containers will be
  446. * attached to this node
  447. * @param int $pos2 The position for the pagination of
  448. * the branch at the second level of the tree
  449. * @param string $type3 The type of item being paginated on
  450. * the third level of the tree
  451. * @param int $pos3 The position for the pagination of
  452. * the branch at the third level of the tree
  453. *
  454. * @return array An array of new nodes
  455. */
  456. private function addTableContainers(NodeTable $table, int $pos2, string $type3, int $pos3): array
  457. {
  458. $retval = [];
  459. if ($table->hasChildren(true) == 0) {
  460. if ($table->getPresence('columns')) {
  461. $retval['columns'] = NodeFactory::getInstance('NodeColumnContainer');
  462. }
  463. if ($table->getPresence('indexes')) {
  464. $retval['indexes'] = NodeFactory::getInstance('NodeIndexContainer');
  465. }
  466. if ($table->getPresence('triggers')) {
  467. $retval['triggers'] = NodeFactory::getInstance('NodeTriggerContainer');
  468. }
  469. // Add all new Nodes to the tree
  470. foreach ($retval as $node) {
  471. $node->pos2 = $pos2;
  472. if ($type3 == $node->realName) {
  473. $node->pos3 = $pos3;
  474. }
  475. $table->addChild($node);
  476. }
  477. } else {
  478. foreach ($table->children as $node) {
  479. if ($type3 == $node->realName) {
  480. $node->pos3 = $pos3;
  481. }
  482. $retval[$node->realName] = $node;
  483. }
  484. }
  485. return $retval;
  486. }
  487. /**
  488. * Adds containers to a node that is a database
  489. *
  490. * References to existing children are returned
  491. * if this function is called twice on the same node
  492. *
  493. * @param NodeDatabase $db The database node, new containers will be
  494. * attached to this node
  495. * @param string $type The type of item being paginated on
  496. * the second level of the tree
  497. * @param int $pos2 The position for the pagination of
  498. * the branch at the second level of the tree
  499. *
  500. * @return array An array of new nodes
  501. */
  502. private function addDbContainers(NodeDatabase $db, string $type, int $pos2): array
  503. {
  504. // Get items to hide
  505. $hidden = $db->getHiddenItems('group');
  506. if (! $GLOBALS['cfg']['NavigationTreeShowTables'] && ! in_array('tables', $hidden)) {
  507. $hidden[] = 'tables';
  508. }
  509. if (! $GLOBALS['cfg']['NavigationTreeShowViews'] && ! in_array('views', $hidden)) {
  510. $hidden[] = 'views';
  511. }
  512. if (! $GLOBALS['cfg']['NavigationTreeShowFunctions'] && ! in_array('functions', $hidden)) {
  513. $hidden[] = 'functions';
  514. }
  515. if (! $GLOBALS['cfg']['NavigationTreeShowProcedures'] && ! in_array('procedures', $hidden)) {
  516. $hidden[] = 'procedures';
  517. }
  518. if (! $GLOBALS['cfg']['NavigationTreeShowEvents'] && ! in_array('events', $hidden)) {
  519. $hidden[] = 'events';
  520. }
  521. $retval = [];
  522. if ($db->hasChildren(true) == 0) {
  523. if (! in_array('tables', $hidden) && $db->getPresence('tables')) {
  524. $retval['tables'] = NodeFactory::getInstance('NodeTableContainer');
  525. }
  526. if (! in_array('views', $hidden) && $db->getPresence('views')) {
  527. $retval['views'] = NodeFactory::getInstance('NodeViewContainer');
  528. }
  529. if (! in_array('functions', $hidden) && $db->getPresence('functions')) {
  530. $retval['functions'] = NodeFactory::getInstance('NodeFunctionContainer');
  531. }
  532. if (! in_array('procedures', $hidden) && $db->getPresence('procedures')) {
  533. $retval['procedures'] = NodeFactory::getInstance('NodeProcedureContainer');
  534. }
  535. if (! in_array('events', $hidden) && $db->getPresence('events')) {
  536. $retval['events'] = NodeFactory::getInstance('NodeEventContainer');
  537. }
  538. // Add all new Nodes to the tree
  539. foreach ($retval as $node) {
  540. if ($type == $node->realName) {
  541. $node->pos2 = $pos2;
  542. }
  543. $db->addChild($node);
  544. }
  545. } else {
  546. foreach ($db->children as $node) {
  547. if ($type == $node->realName) {
  548. $node->pos2 = $pos2;
  549. }
  550. $retval[$node->realName] = $node;
  551. }
  552. }
  553. return $retval;
  554. }
  555. /**
  556. * Recursively groups tree nodes given a separator
  557. *
  558. * @param Node $node The node to group or null
  559. * to group the whole tree. If
  560. * passed as an argument, $node
  561. * must be of type CONTAINER
  562. */
  563. public function groupTree(?Node $node = null): void
  564. {
  565. if ($node === null) {
  566. $node = $this->tree;
  567. }
  568. $this->groupNode($node);
  569. foreach ($node->children as $child) {
  570. $this->groupTree($child);
  571. }
  572. }
  573. /**
  574. * Recursively groups tree nodes given a separator
  575. *
  576. * @param Node $node The node to group
  577. */
  578. public function groupNode($node): void
  579. {
  580. if ($node->type != Node::CONTAINER || ! $GLOBALS['cfg']['NavigationTreeEnableExpansion']) {
  581. return;
  582. }
  583. $separators = [];
  584. if (is_array($node->separator)) {
  585. $separators = $node->separator;
  586. } else {
  587. if (strlen($node->separator)) {
  588. $separators[] = $node->separator;
  589. }
  590. }
  591. $prefixes = [];
  592. if ($node->separatorDepth > 0) {
  593. foreach ($node->children as $child) {
  594. $prefixPos = false;
  595. foreach ($separators as $separator) {
  596. $sepPos = mb_strpos((string) $child->name, $separator);
  597. if (
  598. $sepPos == false
  599. || $sepPos == mb_strlen($child->name)
  600. || $sepPos == 0
  601. || ($prefixPos !== false && $sepPos >= $prefixPos)
  602. ) {
  603. continue;
  604. }
  605. $prefixPos = $sepPos;
  606. }
  607. if ($prefixPos !== false) {
  608. $prefix = mb_substr($child->name, 0, $prefixPos);
  609. if (! isset($prefixes[$prefix])) {
  610. $prefixes[$prefix] = 1;
  611. } else {
  612. $prefixes[$prefix]++;
  613. }
  614. }
  615. //Bug #4375: Check if prefix is the name of a DB, to create a group.
  616. foreach ($node->children as $otherChild) {
  617. if (! array_key_exists($otherChild->name, $prefixes)) {
  618. continue;
  619. }
  620. $prefixes[$otherChild->name]++;
  621. }
  622. }
  623. //Check if prefix is the name of a DB, to create a group.
  624. foreach ($node->children as $child) {
  625. if (! array_key_exists($child->name, $prefixes)) {
  626. continue;
  627. }
  628. $prefixes[$child->name]++;
  629. }
  630. }
  631. // It is not a group if it has only one item
  632. foreach ($prefixes as $key => $value) {
  633. if ($value > 1) {
  634. continue;
  635. }
  636. unset($prefixes[$key]);
  637. }
  638. $numChildren = count($node->children);
  639. // rfe #1634 Don't group if there's only one group and no other items
  640. if (count($prefixes) === 1) {
  641. $keys = array_keys($prefixes);
  642. $key = $keys[0];
  643. if ($prefixes[$key] == $numChildren - 1) {
  644. unset($prefixes[$key]);
  645. }
  646. }
  647. if (! count($prefixes)) {
  648. return;
  649. }
  650. /** @var Node[] $groups */
  651. $groups = [];
  652. foreach ($prefixes as $key => $value) {
  653. // warn about large groups
  654. if ($value > 500 && ! $this->largeGroupWarning) {
  655. trigger_error(
  656. __(
  657. 'There are large item groups in navigation panel which '
  658. . 'may affect the performance. Consider disabling item '
  659. . 'grouping in the navigation panel.'
  660. ),
  661. E_USER_WARNING
  662. );
  663. $this->largeGroupWarning = true;
  664. }
  665. $newChildren = [];
  666. foreach ($separators as $separator) {
  667. $separatorLength = strlen($separator);
  668. // FIXME: this could be more efficient
  669. foreach ($node->children as $child) {
  670. $keySeparatorLength = mb_strlen((string) $key) + $separatorLength;
  671. $nameSubstring = mb_substr((string) $child->name, 0, $keySeparatorLength);
  672. if (($nameSubstring != $key . $separator && $child->name != $key) || $child->type != Node::OBJECT) {
  673. continue;
  674. }
  675. $class = get_class($child);
  676. $className = substr($class, strrpos($class, '\\') + 1);
  677. unset($class);
  678. /** @var NodeDatabase $newChild */
  679. $newChild = NodeFactory::getInstance(
  680. $className,
  681. mb_substr(
  682. $child->name,
  683. $keySeparatorLength
  684. )
  685. );
  686. if ($child instanceof NodeDatabase && $child->getHiddenCount() > 0) {
  687. $newChild->setHiddenCount($child->getHiddenCount());
  688. }
  689. $newChild->realName = $child->realName;
  690. $newChild->icon = $child->icon;
  691. $newChild->links = $child->links;
  692. $newChild->pos2 = $child->pos2;
  693. $newChild->pos3 = $child->pos3;
  694. foreach ($child->children as $elm) {
  695. $newChild->addChild($elm);
  696. }
  697. $newChildren[] = [
  698. 'node' => $newChild,
  699. 'replaces_name' => $child->name,
  700. ];
  701. }
  702. }
  703. if (count($newChildren) === 0) {
  704. continue;
  705. }
  706. // If the current node is a standard group (not NodeTableContainer, etc.)
  707. // and the new group contains all of the current node's children, combine them
  708. $class = get_class($node);
  709. if (count($newChildren) === $numChildren && substr($class, strrpos($class, '\\') + 1) === 'Node') {
  710. $node->name .= $separators[0] . htmlspecialchars((string) $key);
  711. $node->realName .= $separators[0] . htmlspecialchars((string) $key);
  712. $node->separatorDepth--;
  713. foreach ($newChildren as $newChild) {
  714. $node->removeChild($newChild['replaces_name']);
  715. $node->addChild($newChild['node']);
  716. }
  717. } else {
  718. $groups[$key] = new Node(
  719. htmlspecialchars((string) $key),
  720. Node::CONTAINER,
  721. true
  722. );
  723. $groups[$key]->separator = $node->separator;
  724. $groups[$key]->separatorDepth = $node->separatorDepth - 1;
  725. $groups[$key]->icon = ['image' => 'b_group', 'title' => __('Groups')];
  726. $groups[$key]->pos2 = $node->pos2;
  727. $groups[$key]->pos3 = $node->pos3;
  728. if ($node instanceof NodeTableContainer || $node instanceof NodeViewContainer) {
  729. $groups[$key]->links = [
  730. 'text' => [
  731. 'route' => $node->links['text']['route'],
  732. 'params' => array_merge($node->links['text']['params'], ['tbl_group' => $key]),
  733. ],
  734. 'icon' => [
  735. 'route' => $node->links['icon']['route'],
  736. 'params' => array_merge($node->links['icon']['params'], ['tbl_group' => $key]),
  737. ],
  738. ];
  739. }
  740. foreach ($newChildren as $newChild) {
  741. $node->removeChild($newChild['replaces_name']);
  742. $groups[$key]->addChild($newChild['node']);
  743. }
  744. }
  745. }
  746. foreach ($groups as $group) {
  747. if (count($group->children) === 0) {
  748. continue;
  749. }
  750. $node->addChild($group);
  751. $this->groupNode($group);
  752. $group->classes = 'navGroup';
  753. }
  754. }
  755. /**
  756. * Renders a state of the tree, used in light mode when
  757. * either JavaScript and/or Ajax are disabled
  758. *
  759. * @return string HTML code for the navigation tree
  760. */
  761. public function renderState(): string
  762. {
  763. $this->buildPath();
  764. $quickWarp = $this->quickWarp();
  765. $fastFilter = $this->fastFilterHtml($this->tree);
  766. $controls = '';
  767. if ($GLOBALS['cfg']['NavigationTreeEnableExpansion']) {
  768. $controls = $this->controls();
  769. }
  770. $pageSelector = $this->getPageSelector($this->tree);
  771. $this->groupTree();
  772. $children = $this->tree->children;
  773. usort($children, [
  774. self::class,
  775. 'sortNode',
  776. ]);
  777. $this->setVisibility();
  778. $nodes = $this->renderNodes($children);
  779. return $this->template->render('navigation/tree/state', [
  780. 'quick_warp' => $quickWarp,
  781. 'fast_filter' => $fastFilter,
  782. 'controls' => $controls,
  783. 'page_selector' => $pageSelector,
  784. 'nodes' => $nodes,
  785. ]);
  786. }
  787. /**
  788. * Renders a part of the tree, used for Ajax requests in light mode
  789. *
  790. * @return string|false HTML code for the navigation tree
  791. */
  792. public function renderPath()
  793. {
  794. $node = $this->buildPath();
  795. if (! is_bool($node)) {
  796. $this->groupTree();
  797. $listContent = $this->fastFilterHtml($node);
  798. $listContent .= $this->getPageSelector($node);
  799. $children = $node->children;
  800. usort($children, [
  801. self::class,
  802. 'sortNode',
  803. ]);
  804. $listContent .= $this->renderNodes($children, false);
  805. if (! $GLOBALS['cfg']['ShowDatabasesNavigationAsTree']) {
  806. $parents = $node->parents(true);
  807. $parentName = $parents[0]->realName;
  808. }
  809. }
  810. $hasSearchClause = ! empty($this->searchClause) || ! empty($this->searchClause2);
  811. if ($hasSearchClause && ! is_bool($node)) {
  812. $results = 0;
  813. if (! empty($this->searchClause2)) {
  814. if (is_object($node->realParent())) {
  815. $results = $node->realParent()
  816. ->getPresence($node->realName, $this->searchClause2);
  817. }
  818. } else {
  819. $results = $this->tree->getPresence('databases', $this->searchClause);
  820. }
  821. $results = sprintf(
  822. _ngettext(
  823. '%s result found',
  824. '%s results found',
  825. $results
  826. ),
  827. $results
  828. );
  829. ResponseRenderer::getInstance()
  830. ->addJSON('results', $results);
  831. }
  832. if ($node !== false) {
  833. return $this->template->render('navigation/tree/path', [
  834. 'has_search_results' => ! empty($this->searchClause) || ! empty($this->searchClause2),
  835. 'list_content' => $listContent ?? '',
  836. 'is_tree' => $GLOBALS['cfg']['ShowDatabasesNavigationAsTree'],
  837. 'parent_name' => $parentName ?? '',
  838. ]);
  839. }
  840. return false;
  841. }
  842. /**
  843. * Renders the parameters that are required on the client
  844. * side to know which page(s) we will be requesting data from
  845. *
  846. * @param Node $node The node to create the pagination parameters for
  847. *
  848. * @return array<string, string>
  849. */
  850. private function getPaginationParamsHtml(Node $node): array
  851. {
  852. $renderDetails = [];
  853. $paths = $node->getPaths();
  854. if (isset($paths['aPath_clean'][2])) {
  855. $renderDetails['position'] = 'pos2_nav';
  856. $renderDetails['data_name'] = (string) $paths['aPath_clean'][2];
  857. $renderDetails['data_value'] = (string) $node->pos2;
  858. }
  859. if (isset($paths['aPath_clean'][4])) {
  860. $renderDetails['position'] = 'pos3_nav';
  861. $renderDetails['data_name'] = (string) $paths['aPath_clean'][4];
  862. $renderDetails['data_value'] = (string) $node->pos3;
  863. }
  864. return $renderDetails;
  865. }
  866. /**
  867. * Finds whether given tree matches this tree.
  868. *
  869. * @param array $tree Tree to check
  870. * @param array $paths Paths to check
  871. */
  872. private function findTreeMatch(array $tree, array $paths): bool
  873. {
  874. $match = false;
  875. foreach ($tree as $path) {
  876. $match = true;
  877. foreach ($paths as $key => $part) {
  878. if (! isset($path[$key]) || $part != $path[$key]) {
  879. $match = false;
  880. break;
  881. }
  882. }
  883. if ($match) {
  884. break;
  885. }
  886. }
  887. return $match;
  888. }
  889. /**
  890. * @param Node[] $children
  891. */
  892. private function renderNodes(array $children, bool $hasFirstClass = true): string
  893. {
  894. $nodes = '';
  895. for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) {
  896. if ($i === 0) {
  897. $nodes .= $this->renderNode($children[0], $hasFirstClass ? 'first' : '');
  898. } elseif ($i + 1 !== $nbChildren) {
  899. $nodes .= $this->renderNode($children[$i]);
  900. } else {
  901. $nodes .= $this->renderNode($children[$i], 'last');
  902. }
  903. }
  904. return $nodes;
  905. }
  906. /**
  907. * Renders a single node or a branch of the tree
  908. *
  909. * @param Node $node The node to render
  910. * @param string $class An additional class for the list item
  911. *
  912. * @return string HTML code for the tree node or branch
  913. */
  914. private function renderNode(Node $node, string $class = ''): string
  915. {
  916. $controlButtons = '';
  917. $paths = $node->getPaths();
  918. $nodeIsContainer = $node->type === Node::CONTAINER;
  919. $liClasses = '';
  920. // Whether to show the node in the tree (true for all nodes but root)
  921. // If false, the node's children will still be shown, but as children of the node's parent
  922. $showNode = $node->hasSiblings() || count($node->parents(false, true)) > 0;
  923. // Don't show the 'Tables' node under each database unless it has 'Views', etc. as a sibling
  924. if ($node instanceof NodeTableContainer && ! $node->hasSiblings()) {
  925. $showNode = false;
  926. }
  927. if ($showNode) {
  928. $response = ResponseRenderer::getInstance();
  929. if ($nodeIsContainer && count($node->children) === 0 && ! $response->isAjax()) {
  930. return '';
  931. }
  932. $liClasses = trim($class . ' ' . $node->classes);
  933. $sterile = ['events', 'triggers', 'functions', 'procedures', 'views', 'columns', 'indexes'];
  934. $parentName = '';
  935. $parents = $node->parents(false, true);
  936. if (count($parents)) {
  937. $parentName = $parents[0]->realName;
  938. }
  939. // if node name itself is in sterile, then allow
  940. $nodeIsGroup = $node->isGroup
  941. || (! in_array($parentName, $sterile) && ! $node->isNew)
  942. || (in_array($node->realName, $sterile) && ! empty($node->children));
  943. if ($nodeIsGroup) {
  944. $match = $this->findTreeMatch($this->vPath, $paths['vPath_clean']);
  945. $linkClasses = $node->getCssClasses($match);
  946. if ($GLOBALS['cfg']['ShowDatabasesNavigationAsTree'] || $parentName !== 'root') {
  947. $nodeIcon = $node->getIcon($match);
  948. }
  949. }
  950. $paginationParams = $this->getPaginationParamsHtml($node);
  951. $haveAjax = [
  952. 'functions',
  953. 'procedures',
  954. 'events',
  955. 'triggers',
  956. 'indexes',
  957. ];
  958. $parent = $node->parents(false, true);
  959. $isNewView = $parent[0]->realName === 'views' && $node->isNew === true;
  960. $linkHasAjaxClass = $parent[0]->type == Node::CONTAINER
  961. && (in_array($parent[0]->realName, $haveAjax) || $isNewView);
  962. if (! $node->isGroup) {
  963. $args = [];
  964. $parents = $node->parents(true);
  965. foreach ($parents as $parent) {
  966. if (! isset($parent->urlParamName)) {
  967. continue;
  968. }
  969. $args[$parent->urlParamName] = $parent->realName;
  970. }
  971. $iconLinks = [];
  972. $iconLinks[] = [
  973. 'route' => $node->links['icon']['route'],
  974. 'params' => array_merge(
  975. $node->links['icon']['params'],
  976. array_intersect_key($args, $node->links['icon']['params'])
  977. ),
  978. 'is_ajax' => $linkHasAjaxClass,
  979. 'image' => $node->icon['image'],
  980. 'title' => $node->icon['title'],
  981. ];
  982. if (isset($node->links['second_icon'], $node->secondIcon)) {
  983. $iconLinks[] = [
  984. 'route' => $node->links['second_icon']['route'],
  985. 'params' => array_merge(
  986. $node->links['second_icon']['params'],
  987. array_intersect_key($args, $node->links['second_icon']['params'])
  988. ),
  989. 'is_ajax' => $linkHasAjaxClass,
  990. 'image' => $node->secondIcon['image'],
  991. 'title' => $node->secondIcon['title'],
  992. ];
  993. }
  994. $textLink = [
  995. 'route' => $node->links['text']['route'],
  996. 'params' => array_merge(
  997. $node->links['text']['params'],
  998. array_intersect_key($args, $node->links['text']['params'])
  999. ),
  1000. 'is_ajax' => $linkHasAjaxClass,
  1001. 'title' => $node->links['title'] ?? $node->title ?? '',
  1002. ];
  1003. }
  1004. $controlButtons .= $node->getHtmlForControlButtons();
  1005. $wrap = true;
  1006. } else {
  1007. $node->visible = true;
  1008. $wrap = false;
  1009. $paginationParams = $this->getPaginationParamsHtml($node);
  1010. }
  1011. $children = $node->children;
  1012. usort($children, [self::class, 'sortNode']);
  1013. $buffer = '';
  1014. $extraClass = '';
  1015. for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) {
  1016. if ($i + 1 == $nbChildren) {
  1017. $extraClass = ' last';
  1018. }
  1019. $buffer .= $this->renderNode($children[$i], $children[$i]->classes . $extraClass);
  1020. }
  1021. if (! empty($buffer)) {
  1022. $recursiveHtml = $this->fastFilterHtml($node);
  1023. $recursiveHtml .= $this->getPageSelector($node);
  1024. $recursiveHtml .= $buffer;
  1025. }
  1026. return $this->template->render('navigation/tree/node', [
  1027. 'node' => $node,
  1028. 'class' => $class,
  1029. 'show_node' => $showNode,
  1030. 'has_siblings' => $node->hasSiblings(),
  1031. 'li_classes' => $liClasses,
  1032. 'control_buttons' => $controlButtons,
  1033. 'node_is_container' => $nodeIsContainer,
  1034. 'has_second_icon' => isset($node->secondIcon),
  1035. 'recursive' => ['html' => $recursiveHtml ?? '', 'has_wrapper' => $wrap, 'is_hidden' => ! $node->visible],
  1036. 'icon_links' => $iconLinks ?? [],
  1037. 'text_link' => $textLink ?? [],
  1038. 'pagination_params' => $paginationParams,
  1039. 'node_is_group' => $nodeIsGroup ?? false,
  1040. 'link_classes' => $linkClasses ?? '',
  1041. 'paths' => ['a_path' => $paths['aPath'] ?? '', 'v_path' => $paths['vPath'] ?? '', 'pos' => $this->pos],
  1042. 'node_icon' => $nodeIcon ?? '',
  1043. ]);
  1044. }
  1045. /**
  1046. * Renders a database select box like the pre-4.0 navigation panel
  1047. *
  1048. * @return string HTML code
  1049. */
  1050. public function renderDbSelect(): string
  1051. {
  1052. $this->buildPath();
  1053. $quickWarp = $this->quickWarp();
  1054. $this->tree->isGroup = false;
  1055. // Provide for pagination in database select
  1056. $listNavigator = Generator::getListNavigator(
  1057. $this->tree->getPresence('databases', ''),
  1058. $this->pos,
  1059. ['server' => $GLOBALS['server']],
  1060. Url::getFromRoute('/navigation'),
  1061. 'frame_navigation',
  1062. $GLOBALS['cfg']['FirstLevelNavigationItems'],
  1063. 'pos',
  1064. ['dbselector']
  1065. );
  1066. $children = $this->tree->children;
  1067. $selected = $GLOBALS['db'];
  1068. $options = [];
  1069. foreach ($children as $node) {
  1070. if ($node->isNew) {
  1071. continue;
  1072. }
  1073. $paths = $node->getPaths();
  1074. if (! isset($node->links['text'])) {
  1075. continue;
  1076. }
  1077. $title = $node->links['title'] ?? '';
  1078. $options[] = [
  1079. 'title' => $title,
  1080. 'name' => $node->realName,
  1081. 'data' => [
  1082. 'apath' => $paths['aPath'],
  1083. 'vpath' => $paths['vPath'],
  1084. 'pos' => $this->pos,
  1085. ],
  1086. 'isSelected' => $node->realName === $selected,
  1087. ];
  1088. }
  1089. $children = $this->tree->children;
  1090. usort($children, [
  1091. self::class,
  1092. 'sortNode',
  1093. ]);
  1094. $this->setVisibility();
  1095. $nodes = $this->renderNodes($children);
  1096. return $this->template->render('navigation/tree/database_select', [
  1097. 'quick_warp' => $quickWarp,
  1098. 'list_navigator' => $listNavigator,
  1099. 'server' => $GLOBALS['server'],
  1100. 'options' => $options,
  1101. 'nodes' => $nodes,
  1102. ]);
  1103. }
  1104. /**
  1105. * Makes some nodes visible based on the which node is active
  1106. */
  1107. private function setVisibility(): void
  1108. {
  1109. foreach ($this->vPath as $path) {
  1110. $node = $this->tree;
  1111. foreach ($path as $value) {
  1112. $child = $node->getChild($value);
  1113. if ($child === null) {
  1114. continue;
  1115. }
  1116. $child->visible = true;
  1117. $node = $child;
  1118. }
  1119. }
  1120. }
  1121. /**
  1122. * Generates the HTML code for displaying the fast filter for tables
  1123. *
  1124. * @param Node $node The node for which to generate the fast filter html
  1125. *
  1126. * @return string LI element used for the fast filter
  1127. */
  1128. private function fastFilterHtml(Node $node): string
  1129. {
  1130. $filterDbMin = (int) $GLOBALS['cfg']['NavigationTreeDisplayDbFilterMinimum'];
  1131. $filterItemMin = (int) $GLOBALS['cfg']['NavigationTreeDisplayItemFilterMinimum'];
  1132. $urlParams = [];
  1133. $isRootNode = $node === $this->tree && $this->tree->getPresence() >= $filterDbMin;
  1134. if ($isRootNode) {
  1135. $urlParams = ['pos' => 0];
  1136. } else {
  1137. $nodeIsContainer = $node->type === Node::CONTAINER;
  1138. $nodeIsSpecial = in_array($node->realName, self::SPECIAL_NODE_NAMES, true);
  1139. /** @var Node $realParent */
  1140. $realParent = $node->realParent();
  1141. if (
  1142. ($nodeIsContainer && $nodeIsSpecial)
  1143. && method_exists($realParent, 'getPresence')
  1144. && $realParent->getPresence($node->realName) >= $filterItemMin
  1145. ) {
  1146. $paths = $node->getPaths();
  1147. $urlParams = [
  1148. 'pos' => $this->pos,
  1149. 'aPath' => $paths['aPath'],
  1150. 'vPath' => $paths['vPath'],
  1151. 'pos2_name' => $node->realName,
  1152. 'pos2_value' => 0,
  1153. ];
  1154. }
  1155. }
  1156. return $this->template->render('navigation/tree/fast_filter', [
  1157. 'url_params' => $urlParams,
  1158. 'is_root_node' => $isRootNode,
  1159. ]);
  1160. }
  1161. /**
  1162. * Creates the code for displaying the controls
  1163. * at the top of the navigation tree
  1164. *
  1165. * @return string HTML code for the controls
  1166. */
  1167. private function controls(): string
  1168. {
  1169. // always iconic
  1170. $showIcon = true;
  1171. $showText = false;
  1172. $collapseAll = Generator::getNavigationLink(
  1173. '#',
  1174. $showText,
  1175. __('Collapse all'),
  1176. $showIcon,
  1177. 's_collapseall',
  1178. 'pma_navigation_collapse'
  1179. );
  1180. $syncImage = 's_unlink';
  1181. $title = __('Link with main panel');
  1182. if ($GLOBALS['cfg']['NavigationLinkWithMainPanel']) {
  1183. $syncImage = 's_link';
  1184. $title = __('Unlink from main panel');
  1185. }
  1186. $unlink = Generator::getNavigationLink('#', $showText, $title, $showIcon, $syncImage, 'pma_navigation_sync');
  1187. return $this->template->render('navigation/tree/controls', [
  1188. 'collapse_all' => $collapseAll,
  1189. 'unlink' => $unlink,
  1190. ]);
  1191. }
  1192. /**
  1193. * Generates the HTML code for displaying the list pagination
  1194. *
  1195. * @param Node $node The node for whose children the page
  1196. * selector will be created
  1197. */
  1198. private function getPageSelector(Node $node): string
  1199. {
  1200. $retval = '';
  1201. if ($node === $this->tree) {
  1202. $retval .= Generator::getListNavigator(
  1203. $this->tree->getPresence('databases', $this->searchClause),
  1204. $this->pos,
  1205. ['server' => $GLOBALS['server']],
  1206. Url::getFromRoute('/navigation'),
  1207. 'frame_navigation',
  1208. $GLOBALS['cfg']['FirstLevelNavigationItems'],
  1209. 'pos',
  1210. ['dbselector']
  1211. );
  1212. } else {
  1213. if ($node->type == Node::CONTAINER && ! $node->isGroup) {
  1214. $paths = $node->getPaths();
  1215. $level = isset($paths['aPath_clean'][4]) ? 3 : 2;
  1216. $urlParams = [
  1217. 'aPath' => $paths['aPath'],
  1218. 'vPath' => $paths['vPath'],
  1219. 'pos' => $this->pos,
  1220. 'server' => $GLOBALS['server'],
  1221. 'pos2_name' => $paths['aPath_clean'][2],
  1222. ];
  1223. if ($level == 3) {
  1224. $pos = $node->pos3;
  1225. $urlParams['pos2_value'] = $node->pos2;
  1226. $urlParams['pos3_name'] = $paths['aPath_clean'][4];
  1227. } else {
  1228. $pos = $node->pos2;
  1229. }
  1230. /** @var Node $realParent */
  1231. $realParent = $node->realParent();
  1232. $num = $realParent->getPresence($node->realName, $this->searchClause2);
  1233. $retval .= Generator::getListNavigator(
  1234. $num,
  1235. $pos,
  1236. $urlParams,
  1237. Url::getFromRoute('/navigation'),
  1238. 'frame_navigation',
  1239. $GLOBALS['cfg']['MaxNavigationItems'],
  1240. 'pos' . $level . '_value'
  1241. );
  1242. }
  1243. }
  1244. return $retval;
  1245. }
  1246. /**
  1247. * Called by usort() for sorting the nodes in a container
  1248. *
  1249. * @param Node $a The first element used in the comparison
  1250. * @param Node $b The second element used in the comparison
  1251. *
  1252. * @return int See strnatcmp() and strcmp()
  1253. */
  1254. public static function sortNode(Node $a, Node $b): int
  1255. {
  1256. if ($a->isNew) {
  1257. return -1;
  1258. }
  1259. if ($b->isNew) {
  1260. return 1;
  1261. }
  1262. if ($GLOBALS['cfg']['NaturalOrder']) {
  1263. return strnatcasecmp($a->name, $b->name);
  1264. }
  1265. return strcasecmp($a->name, $b->name);
  1266. }
  1267. /**
  1268. * Display quick warp links, contain Recents and Favorites
  1269. *
  1270. * @return string HTML code

Large files files are truncated, but you can click here to view the full file