1<?php
2/* vim: set expandtab sw=4 ts=4 sts=4: */
3/**
4 * Functionality for the navigation tree
5 *
6 * @package PhpMyAdmin-Navigation
7 */
8namespace PhpMyAdmin\Navigation;
9
10use PhpMyAdmin\Navigation\Nodes\Node;
11use PhpMyAdmin\Navigation\Nodes\NodeDatabase;
12use PhpMyAdmin\Navigation\Nodes\NodeTable;
13use PhpMyAdmin\Navigation\Nodes\NodeTableContainer;
14use PhpMyAdmin\Navigation\Nodes\NodeViewContainer;
15use PhpMyAdmin\RecentFavoriteTable;
16use PhpMyAdmin\Response;
17use PhpMyAdmin\Util;
18use PhpMyAdmin\Url;
19
20require_once 'libraries/check_user_privileges.inc.php';
21
22/**
23 * Displays a collapsible of database objects in the navigation frame
24 *
25 * @package PhpMyAdmin-Navigation
26 */
27class NavigationTree
28{
29    /**
30     * @var Node Reference to the root node of the tree
31     */
32    private $_tree;
33    /**
34     * @var array The actual paths to all expanded nodes in the tree
35     *            This does not include nodes created after the grouping
36     *            of nodes has been performed
37     */
38    private $_aPath = array();
39    /**
40     * @var array The virtual paths to all expanded nodes in the tree
41     *            This includes nodes created after the grouping of
42     *            nodes has been performed
43     */
44    private $_vPath = array();
45    /**
46     * @var int Position in the list of databases,
47     *          used for pagination
48     */
49    private $_pos;
50    /**
51     * @var array The names of the type of items that are being paginated on
52     *            the second level of the navigation tree. These may be
53     *            tables, views, functions, procedures or events.
54     */
55    private $_pos2_name = array();
56    /**
57     * @var array The positions of nodes in the lists of tables, views,
58     *            routines or events used for pagination
59     */
60    private $_pos2_value = array();
61    /**
62     * @var array The names of the type of items that are being paginated
63     *            on the second level of the navigation tree.
64     *            These may be columns or indexes
65     */
66    private $_pos3_name = array();
67    /**
68     * @var array The positions of nodes in the lists of columns or indexes
69     *            used for pagination
70     */
71    private $_pos3_value = array();
72    /**
73     * @var string The search clause to use in SQL queries for
74     *             fetching databases
75     *             Used by the asynchronous fast filter
76     */
77    private $_searchClause = '';
78    /**
79     * @var string The search clause to use in SQL queries for
80     *             fetching nodes
81     *             Used by the asynchronous fast filter
82     */
83    private $_searchClause2 = '';
84    /**
85     * @var bool Whether a warning was raised for large item groups
86     *           which can affect performance.
87     */
88    private $_largeGroupWarning = false;
89
90    /**
91     * Initialises the class
92     */
93    public function __construct()
94    {
95        // Save the position at which we are in the database list
96        if (isset($_POST['pos'])) {
97            $this->_pos = (int) $_POST['pos'];
98        } elseif (isset($_GET['pos'])) {
99            $this->_pos = (int) $_GET['pos'];
100        }
101        if (!isset($this->_pos)) {
102            $this->_pos = $this->_getNavigationDbPos();
103        }
104        // Get the active node
105        if (isset($_REQUEST['aPath'])) {
106            $this->_aPath[0] = $this->_parsePath($_REQUEST['aPath']);
107            $this->_pos2_name[0] = $_REQUEST['pos2_name'];
108            $this->_pos2_value[0] = $_REQUEST['pos2_value'];
109            if (isset($_REQUEST['pos3_name'])) {
110                $this->_pos3_name[0] = $_REQUEST['pos3_name'];
111                $this->_pos3_value[0] = $_REQUEST['pos3_value'];
112            }
113        } else {
114            if (isset($_POST['n0_aPath'])) {
115                $count = 0;
116                while (isset($_POST['n' . $count . '_aPath'])) {
117                    $this->_aPath[$count] = $this->_parsePath(
118                        $_POST['n' . $count . '_aPath']
119                    );
120                    $index = 'n' . $count . '_pos2_';
121                    $this->_pos2_name[$count] = $_POST[$index . 'name'];
122                    $this->_pos2_value[$count] = $_POST[$index . 'value'];
123                    $index = 'n' . $count . '_pos3_';
124                    if (isset($_POST[$index])) {
125                        $this->_pos3_name[$count] = $_POST[$index . 'name'];
126                        $this->_pos3_value[$count] = $_POST[$index . 'value'];
127                    }
128                    $count++;
129                }
130            }
131        }
132        if (isset($_REQUEST['vPath'])) {
133            $this->_vPath[0] = $this->_parsePath($_REQUEST['vPath']);
134        } else {
135            if (isset($_POST['n0_vPath'])) {
136                $count = 0;
137                while (isset($_POST['n' . $count . '_vPath'])) {
138                    $this->_vPath[$count] = $this->_parsePath(
139                        $_POST['n' . $count . '_vPath']
140                    );
141                    $count++;
142                }
143            }
144        }
145        if (isset($_REQUEST['searchClause'])) {
146            $this->_searchClause = $_REQUEST['searchClause'];
147        }
148        if (isset($_REQUEST['searchClause2'])) {
149            $this->_searchClause2 = $_REQUEST['searchClause2'];
150        }
151        // Initialise the tree by creating a root node
152        $node = NodeFactory::getInstance('NodeDatabaseContainer', 'root');
153        $this->_tree = $node;
154        if ($GLOBALS['cfg']['NavigationTreeEnableGrouping']
155            && $GLOBALS['cfg']['ShowDatabasesNavigationAsTree']
156        ) {
157            $this->_tree->separator = $GLOBALS['cfg']['NavigationTreeDbSeparator'];
158            $this->_tree->separator_depth = 10000;
159        }
160    }
161
162    /**
163     * Returns the database position for the page selector
164     *
165     * @return int
166     */
167    private function _getNavigationDbPos()
168    {
169        $retval = 0;
170
171        if (strlen($GLOBALS['db']) == 0) {
172            return $retval;
173        }
174
175        /*
176         * @todo describe a scenario where this code is executed
177         */
178        if (!$GLOBALS['cfg']['Server']['DisableIS']) {
179            $dbSeparator = $GLOBALS['dbi']->escapeString(
180                $GLOBALS['cfg']['NavigationTreeDbSeparator']
181            );
182            $query = "SELECT (COUNT(DB_first_level) DIV %d) * %d ";
183            $query .= "from ( ";
184            $query .= " SELECT distinct SUBSTRING_INDEX(SCHEMA_NAME, ";
185            $query .= " '%s', 1) ";
186            $query .= " DB_first_level ";
187            $query .= " FROM INFORMATION_SCHEMA.SCHEMATA ";
188            $query .= " WHERE `SCHEMA_NAME` < '%s' ";
189            $query .= ") t ";
190
191            $retval = $GLOBALS['dbi']->fetchValue(
192                sprintf(
193                    $query,
194                    (int)$GLOBALS['cfg']['FirstLevelNavigationItems'],
195                    (int)$GLOBALS['cfg']['FirstLevelNavigationItems'],
196                    $dbSeparator,
197                    $GLOBALS['dbi']->escapeString($GLOBALS['db'])
198                )
199            );
200
201            return $retval;
202        }
203
204        $prefixMap = array();
205        if ($GLOBALS['dbs_to_test'] === false) {
206            $handle = $GLOBALS['dbi']->tryQuery("SHOW DATABASES");
207            if ($handle !== false) {
208                while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
209                    if (strcasecmp($arr[0], $GLOBALS['db']) >= 0) {
210                        break;
211                    }
212
213                    $prefix = strstr(
214                        $arr[0],
215                        $GLOBALS['cfg']['NavigationTreeDbSeparator'],
216                        true
217                    );
218                    if ($prefix === false) {
219                        $prefix = $arr[0];
220                    }
221                    $prefixMap[$prefix] = 1;
222                }
223            }
224        } else {
225            $databases = array();
226            foreach ($GLOBALS['dbs_to_test'] as $db) {
227                $query = "SHOW DATABASES LIKE '" . $db . "'";
228                $handle = $GLOBALS['dbi']->tryQuery($query);
229                if ($handle === false) {
230                    continue;
231                }
232                while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
233                    $databases[] = $arr[0];
234                }
235            }
236            sort($databases);
237            foreach ($databases as $database) {
238                if (strcasecmp($database, $GLOBALS['db']) >= 0) {
239                    break;
240                }
241
242                $prefix = strstr(
243                    $database,
244                    $GLOBALS['cfg']['NavigationTreeDbSeparator'],
245                    true
246                );
247                if ($prefix === false) {
248                    $prefix = $database;
249                }
250                $prefixMap[$prefix] = 1;
251            }
252        }
253
254        $navItems = (int)$GLOBALS['cfg']['FirstLevelNavigationItems'];
255        $retval = floor((count($prefixMap) / $navItems)) * $navItems;
256
257        return $retval;
258    }
259
260    /**
261     * Converts an encoded path to a node in string format to an array
262     *
263     * @param string $string The path to parse
264     *
265     * @return array
266     */
267    private function _parsePath($string)
268    {
269        $path = explode('.', $string);
270        foreach ($path as $key => $value) {
271            $path[$key] = base64_decode($value);
272        }
273
274        return $path;
275    }
276
277    /**
278     * Generates the tree structure so that it can be rendered later
279     *
280     * @return Node|false The active node or false in case of failure
281     */
282    private function _buildPath()
283    {
284        $retval = $this->_tree;
285
286        // Add all databases unconditionally
287        $data = $this->_tree->getData(
288            'databases',
289            $this->_pos,
290            $this->_searchClause
291        );
292        $hiddenCounts = $this->_tree->getNavigationHidingData();
293        foreach ($data as $db) {
294            $node = NodeFactory::getInstance('NodeDatabase', $db);
295            if (isset($hiddenCounts[$db])) {
296                $node->setHiddenCount($hiddenCounts[$db]);
297            }
298            $this->_tree->addChild($node);
299        }
300
301        // Whether build other parts of the tree depends
302        // on whether we have any paths in $this->_aPath
303        foreach ($this->_aPath as $key => $path) {
304            $retval = $this->_buildPathPart(
305                $path,
306                $this->_pos2_name[$key],
307                $this->_pos2_value[$key],
308                isset($this->_pos3_name[$key]) ? $this->_pos3_name[$key] : '',
309                isset($this->_pos3_value[$key]) ? $this->_pos3_value[$key] : ''
310            );
311        }
312
313        return $retval;
314    }
315
316    /**
317     * Builds a branch of the tree
318     *
319     * @param array  $path  A paths pointing to the branch
320     *                      of the tree that needs to be built
321     * @param string $type2 The type of item being paginated on
322     *                      the second level of the tree
323     * @param int    $pos2  The position for the pagination of
324     *                      the branch at the second level of the tree
325     * @param string $type3 The type of item being paginated on
326     *                      the third level of the tree
327     * @param int    $pos3  The position for the pagination of
328     *                      the branch at the third level of the tree
329     *
330     * @return Node|false The active node or false in case of failure
331     */
332    private function _buildPathPart(array $path, $type2, $pos2, $type3, $pos3)
333    {
334        if (empty($pos2)) {
335            $pos2 = 0;
336        }
337        if (empty($pos3)) {
338            $pos3 = 0;
339        }
340
341        $retval = true;
342        if (count($path) <= 1) {
343            return $retval;
344        }
345
346        array_shift($path); // remove 'root'
347        /* @var $db NodeDatabase */
348        $db = $this->_tree->getChild($path[0]);
349        $retval = $db;
350
351        if ($db === false) {
352            return false;
353        }
354
355        $containers = $this->_addDbContainers($db, $type2, $pos2);
356
357        array_shift($path); // remove db
358
359        if ((count($path) <= 0 || !array_key_exists($path[0], $containers))
360            && count($containers) != 1
361        ) {
362            return $retval;
363        }
364
365        if (count($containers) == 1) {
366            $container = array_shift($containers);
367        } else {
368            $container = $db->getChild($path[0], true);
369            if ($container === false) {
370                return false;
371            }
372        }
373        $retval = $container;
374
375        if (count($container->children) <= 1) {
376            $dbData = $db->getData(
377                $container->real_name,
378                $pos2,
379                $this->_searchClause2
380            );
381            foreach ($dbData as $item) {
382                switch ($container->real_name) {
383                case 'events':
384                    $node = NodeFactory::getInstance(
385                        'NodeEvent',
386                        $item
387                    );
388                    break;
389                case 'functions':
390                    $node = NodeFactory::getInstance(
391                        'NodeFunction',
392                        $item
393                    );
394                    break;
395                case 'procedures':
396                    $node = NodeFactory::getInstance(
397                        'NodeProcedure',
398                        $item
399                    );
400                    break;
401                case 'tables':
402                    $node = NodeFactory::getInstance(
403                        'NodeTable',
404                        $item
405                    );
406                    break;
407                case 'views':
408                    $node = NodeFactory::getInstance(
409                        'NodeView',
410                        $item
411                    );
412                    break;
413                default:
414                    break;
415                }
416                if (isset($node)) {
417                    if ($type2 == $container->real_name) {
418                        $node->pos2 = $pos2;
419                    }
420                    $container->addChild($node);
421                }
422            }
423        }
424        if (count($path) > 1 && $path[0] != 'tables') {
425            $retval = false;
426
427            return $retval;
428        }
429
430        array_shift($path); // remove container
431        if (count($path) <= 0) {
432            return $retval;
433        }
434
435        /* @var $table NodeTable */
436        $table = $container->getChild($path[0], true);
437        if ($table === false) {
438            if (!$db->getPresence('tables', $path[0])) {
439                return false;
440            }
441
442            $node = NodeFactory::getInstance(
443                'NodeTable',
444                $path[0]
445            );
446            if ($type2 == $container->real_name) {
447                $node->pos2 = $pos2;
448            }
449            $container->addChild($node);
450            $table = $container->getChild($path[0], true);
451        }
452        $retval = $table;
453        $containers = $this->_addTableContainers(
454            $table,
455            $pos2,
456            $type3,
457            $pos3
458        );
459        array_shift($path); // remove table
460        if (count($path) <= 0
461            || !array_key_exists($path[0], $containers)
462        ) {
463            return $retval;
464        }
465
466        $container = $table->getChild($path[0], true);
467        $retval = $container;
468        $tableData = $table->getData(
469            $container->real_name,
470            $pos3
471        );
472        foreach ($tableData as $item) {
473            switch ($container->real_name) {
474            case 'indexes':
475                $node = NodeFactory::getInstance(
476                    'NodeIndex',
477                    $item
478                );
479                break;
480            case 'columns':
481                $node = NodeFactory::getInstance(
482                    'NodeColumn',
483                    $item
484                );
485                break;
486            case 'triggers':
487                $node = NodeFactory::getInstance(
488                    'NodeTrigger',
489                    $item
490                );
491                break;
492            default:
493                break;
494            }
495            if (isset($node)) {
496                $node->pos2 = $container->parent->pos2;
497                if ($type3 == $container->real_name) {
498                    $node->pos3 = $pos3;
499                }
500                $container->addChild($node);
501            }
502        }
503
504        return $retval;
505    }
506
507    /**
508     * Adds containers to a node that is a table
509     *
510     * References to existing children are returned
511     * if this function is called twice on the same node
512     *
513     * @param NodeTable $table The table node, new containers will be
514     *                         attached to this node
515     * @param int       $pos2  The position for the pagination of
516     *                         the branch at the second level of the tree
517     * @param string    $type3 The type of item being paginated on
518     *                         the third level of the tree
519     * @param int       $pos3  The position for the pagination of
520     *                         the branch at the third level of the tree
521     *
522     * @return array An array of new nodes
523     */
524    private function _addTableContainers($table, $pos2, $type3, $pos3)
525    {
526        $retval = array();
527        if ($table->hasChildren(true) == 0) {
528            if ($table->getPresence('columns')) {
529                $retval['columns'] = NodeFactory::getInstance(
530                    'NodeColumnContainer'
531                );
532            }
533            if ($table->getPresence('indexes')) {
534                $retval['indexes'] = NodeFactory::getInstance(
535                    'NodeIndexContainer'
536                );
537            }
538            if ($table->getPresence('triggers')) {
539                $retval['triggers'] = NodeFactory::getInstance(
540                    'NodeTriggerContainer'
541                );
542            }
543            // Add all new Nodes to the tree
544            foreach ($retval as $node) {
545                $node->pos2 = $pos2;
546                if ($type3 == $node->real_name) {
547                    $node->pos3 = $pos3;
548                }
549                $table->addChild($node);
550            }
551        } else {
552            foreach ($table->children as $node) {
553                if ($type3 == $node->real_name) {
554                    $node->pos3 = $pos3;
555                }
556                $retval[$node->real_name] = $node;
557            }
558        }
559
560        return $retval;
561    }
562
563    /**
564     * Adds containers to a node that is a database
565     *
566     * References to existing children are returned
567     * if this function is called twice on the same node
568     *
569     * @param NodeDatabase $db   The database node, new containers will be
570     *                           attached to this node
571     * @param string       $type The type of item being paginated on
572     *                           the second level of the tree
573     * @param int          $pos2 The position for the pagination of
574     *                           the branch at the second level of the tree
575     *
576     * @return array An array of new nodes
577     */
578    private function _addDbContainers($db, $type, $pos2)
579    {
580        // Get items to hide
581        $hidden = $db->getHiddenItems('group');
582        if (!$GLOBALS['cfg']['NavigationTreeShowTables']
583            && !in_array('tables', $hidden)
584        ) {
585            $hidden[] = 'tables';
586        }
587        if (!$GLOBALS['cfg']['NavigationTreeShowViews']
588            && !in_array('views', $hidden)
589        ) {
590            $hidden[] = 'views';
591        }
592        if (!$GLOBALS['cfg']['NavigationTreeShowFunctions']
593            && !in_array('functions', $hidden)
594        ) {
595            $hidden[] = 'functions';
596        }
597        if (!$GLOBALS['cfg']['NavigationTreeShowProcedures']
598            && !in_array('procedures', $hidden)
599        ) {
600            $hidden[] = 'procedures';
601        }
602        if (!$GLOBALS['cfg']['NavigationTreeShowEvents']
603            && !in_array('events', $hidden)
604        ) {
605            $hidden[] = 'events';
606        }
607
608        $retval = array();
609        if ($db->hasChildren(true) == 0) {
610            if (!in_array('tables', $hidden) && $db->getPresence('tables')) {
611                $retval['tables'] = NodeFactory::getInstance(
612                    'NodeTableContainer'
613                );
614            }
615            if (!in_array('views', $hidden) && $db->getPresence('views')) {
616                $retval['views'] = NodeFactory::getInstance(
617                    'NodeViewContainer'
618                );
619            }
620            if (!in_array('functions', $hidden) && $db->getPresence('functions')) {
621                $retval['functions'] = NodeFactory::getInstance(
622                    'NodeFunctionContainer'
623                );
624            }
625            if (!in_array('procedures', $hidden) && $db->getPresence('procedures')) {
626                $retval['procedures'] = NodeFactory::getInstance(
627                    'NodeProcedureContainer'
628                );
629            }
630            if (!in_array('events', $hidden) && $db->getPresence('events')) {
631                $retval['events'] = NodeFactory::getInstance(
632                    'NodeEventContainer'
633                );
634            }
635            // Add all new Nodes to the tree
636            foreach ($retval as $node) {
637                if ($type == $node->real_name) {
638                    $node->pos2 = $pos2;
639                }
640                $db->addChild($node);
641            }
642        } else {
643            foreach ($db->children as $node) {
644                if ($type == $node->real_name) {
645                    $node->pos2 = $pos2;
646                }
647                $retval[$node->real_name] = $node;
648            }
649        }
650
651        return $retval;
652    }
653
654    /**
655     * Recursively groups tree nodes given a separator
656     *
657     * @param mixed $node The node to group or null
658     *                    to group the whole tree. If
659     *                    passed as an argument, $node
660     *                    must be of type CONTAINER
661     *
662     * @return void
663     */
664    public function groupTree($node = null)
665    {
666        if (!isset($node)) {
667            $node = $this->_tree;
668        }
669        $this->groupNode($node);
670        foreach ($node->children as $child) {
671            $this->groupTree($child);
672        }
673    }
674
675    /**
676     * Recursively groups tree nodes given a separator
677     *
678     * @param Node $node The node to group
679     *
680     * @return void
681     */
682    public function groupNode($node)
683    {
684        if ($node->type != Node::CONTAINER
685            || !$GLOBALS['cfg']['NavigationTreeEnableExpansion']
686        ) {
687            return;
688        }
689
690        $separators = array();
691        if (is_array($node->separator)) {
692            $separators = $node->separator;
693        } else {
694            if (strlen($node->separator)) {
695                $separators[] = $node->separator;
696            }
697        }
698        $prefixes = array();
699        if ($node->separator_depth > 0) {
700            foreach ($node->children as $child) {
701                $prefix_pos = false;
702                foreach ($separators as $separator) {
703                    $sep_pos = mb_strpos($child->name, $separator);
704                    if ($sep_pos != false
705                        && $sep_pos != mb_strlen($child->name)
706                        && $sep_pos != 0
707                        && ($prefix_pos == false || $sep_pos < $prefix_pos)
708                    ) {
709                        $prefix_pos = $sep_pos;
710                    }
711                }
712                if ($prefix_pos !== false) {
713                    $prefix = mb_substr($child->name, 0, $prefix_pos);
714                    if (!isset($prefixes[$prefix])) {
715                        $prefixes[$prefix] = 1;
716                    } else {
717                        $prefixes[$prefix]++;
718                    }
719                }
720                //Bug #4375: Check if prefix is the name of a DB, to create a group.
721                foreach ($node->children as $otherChild) {
722                    if (array_key_exists($otherChild->name, $prefixes)) {
723                        $prefixes[$otherChild->name]++;
724                    }
725                }
726            }
727            //Check if prefix is the name of a DB, to create a group.
728            foreach ($node->children as $child) {
729                if (array_key_exists($child->name, $prefixes)) {
730                    $prefixes[$child->name]++;
731                }
732            }
733        }
734        // It is not a group if it has only one item
735        foreach ($prefixes as $key => $value) {
736            if ($value == 1) {
737                unset($prefixes[$key]);
738            }
739        }
740        // rfe #1634 Don't group if there's only one group and no other items
741        if (count($prefixes) == 1) {
742            $keys = array_keys($prefixes);
743            $key = $keys[0];
744            if ($prefixes[$key] == count($node->children) - 1) {
745                unset($prefixes[$key]);
746            }
747        }
748        if (count($prefixes)) {
749            /** @var Node[] $groups */
750            $groups = array();
751            foreach ($prefixes as $key => $value) {
752
753                // warn about large groups
754                if ($value > 500 && !$this->_largeGroupWarning) {
755                    trigger_error(
756                        __(
757                            'There are large item groups in navigation panel which '
758                            . 'may affect the performance. Consider disabling item '
759                            . 'grouping in the navigation panel.'
760                        ),
761                        E_USER_WARNING
762                    );
763                    $this->_largeGroupWarning = true;
764                }
765
766                $groups[$key] = new Node(
767                    htmlspecialchars((string) $key),
768                    Node::CONTAINER,
769                    true
770                );
771                $groups[$key]->separator = $node->separator;
772                $groups[$key]->separator_depth = $node->separator_depth - 1;
773                $groups[$key]->icon = Util::getImage(
774                    'b_group'
775                );
776                $groups[$key]->pos2 = $node->pos2;
777                $groups[$key]->pos3 = $node->pos3;
778                if ($node instanceof NodeTableContainer
779                    || $node instanceof NodeViewContainer
780                ) {
781                    $tblGroup = '&amp;tbl_group=' . urlencode($key);
782                    $groups[$key]->links = array(
783                        'text' => $node->links['text'] . $tblGroup,
784                        'icon' => $node->links['icon'] . $tblGroup,
785                    );
786                }
787                $node->addChild($groups[$key]);
788                foreach ($separators as $separator) {
789                    $separatorLength = strlen($separator);
790                    // FIXME: this could be more efficient
791                    foreach ($node->children as $child) {
792                        $keySeparatorLength = mb_strlen($key) + $separatorLength;
793                        $name_substring = mb_substr(
794                            $child->name,
795                            0,
796                            $keySeparatorLength
797                        );
798                        if (($name_substring != $key . $separator
799                            && $child->name != $key)
800                            || $child->type != Node::OBJECT
801                        ) {
802                            continue;
803                        }
804                        $class = get_class($child);
805                        $className = substr($class, strrpos($class, '\\') + 1);
806                        unset($class);
807                        $new_child = NodeFactory::getInstance(
808                            $className,
809                            mb_substr(
810                                $child->name,
811                                $keySeparatorLength
812                            )
813                        );
814
815                        if ($new_child instanceof NodeDatabase
816                            && $child->getHiddenCount() > 0
817                        ) {
818                            $new_child->setHiddenCount($child->getHiddenCount());
819                        }
820
821                        $new_child->real_name = $child->real_name;
822                        $new_child->icon = $child->icon;
823                        $new_child->links = $child->links;
824                        $new_child->pos2 = $child->pos2;
825                        $new_child->pos3 = $child->pos3;
826                        $groups[$key]->addChild($new_child);
827                        foreach ($child->children as $elm) {
828                            $new_child->addChild($elm);
829                        }
830                        $node->removeChild($child->name);
831                    }
832                }
833            }
834            foreach ($prefixes as $key => $value) {
835                $this->groupNode($groups[$key]);
836                $groups[$key]->classes = "navGroup";
837            }
838        }
839    }
840
841    /**
842     * Renders a state of the tree, used in light mode when
843     * either JavaScript and/or Ajax are disabled
844     *
845     * @return string HTML code for the navigation tree
846     */
847    public function renderState()
848    {
849        $this->_buildPath();
850        $retval = $this->_quickWarp();
851        $retval .= '<div class="clearfloat"></div>';
852        $retval .= '<ul>';
853        $retval .= $this->_fastFilterHtml($this->_tree);
854        if ($GLOBALS['cfg']['NavigationTreeEnableExpansion']
855        ) {
856            $retval .= $this->_controls();
857        }
858        $retval .= '</ul>';
859        $retval .= $this->_getPageSelector($this->_tree);
860        $this->groupTree();
861        $retval .= "<div id='pma_navigation_tree_content'><ul>";
862        $children = $this->_tree->children;
863        usort(
864            $children,
865            array('PhpMyAdmin\\Navigation\\NavigationTree', 'sortNode')
866        );
867        $this->_setVisibility();
868        for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) {
869            if ($i == 0) {
870                $retval .= $this->_renderNode($children[0], true, 'first');
871            } else {
872                if ($i + 1 != $nbChildren) {
873                    $retval .= $this->_renderNode($children[$i], true);
874                } else {
875                    $retval .= $this->_renderNode($children[$i], true, 'last');
876                }
877            }
878        }
879        $retval .= "</ul></div>";
880
881        return $retval;
882    }
883
884    /**
885     * Renders a part of the tree, used for Ajax
886     * requests in light mode
887     *
888     * @return string HTML code for the navigation tree
889     */
890    public function renderPath()
891    {
892        $node = $this->_buildPath();
893        if ($node === false) {
894            $retval = false;
895        } else {
896            $this->groupTree();
897            $retval = "<div class='list_container hide'>";
898            if (!empty($this->_searchClause) || !empty($this->_searchClause2)) {
899                $retval .= "<ul class='search_results'>";
900            } else {
901                $retval .= "<ul>";
902            }
903            $listContent = $this->_fastFilterHtml($node);
904            $listContent .= $this->_getPageSelector($node);
905            $children = $node->children;
906            usort(
907                $children,
908                array('PhpMyAdmin\\Navigation\\NavigationTree', 'sortNode')
909            );
910            for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) {
911                if ($i + 1 != $nbChildren) {
912                    $listContent .= $this->_renderNode($children[$i], true);
913                } else {
914                    $listContent .= $this->_renderNode($children[$i], true, 'last');
915                }
916            }
917            $retval .= $listContent;
918            $retval .= "</ul>";
919            if (!$GLOBALS['cfg']['ShowDatabasesNavigationAsTree']) {
920                $retval .= "<span class='hide loaded_db'>";
921                $parents = $node->parents(true);
922                $retval .= urlencode($parents[0]->real_name);
923                $retval .= "</span>";
924                if (empty($listContent)) {
925                    $retval .= "<div style='margin:0.75em'>";
926                    $retval .= __('No tables found in database.');
927                    $retval .= "</div>";
928                }
929            }
930            $retval .= "</div>";
931        }
932
933        if (!empty($this->_searchClause) || !empty($this->_searchClause2)) {
934            $results = 0;
935            if (!empty($this->_searchClause2)) {
936                if (is_object($node->realParent())) {
937                    $results = $node->realParent()
938                        ->getPresence(
939                            $node->real_name,
940                            $this->_searchClause2
941                        );
942                }
943            } else {
944                $results = $this->_tree->getPresence(
945                    'databases',
946                    $this->_searchClause
947                );
948            }
949            $results = sprintf(
950                _ngettext(
951                    '%s result found',
952                    '%s results found',
953                    $results
954                ),
955                $results
956            );
957            Response::getInstance()
958                ->addJSON(
959                    'results',
960                    $results
961                );
962        }
963
964        return $retval;
965    }
966
967    /**
968     * Renders the parameters that are required on the client
969     * side to know which page(s) we will be requesting data from
970     *
971     * @param Node $node The node to create the pagination parameters for
972     *
973     * @return string
974     */
975    private function _getPaginationParamsHtml($node)
976    {
977        $retval = '';
978        $paths = $node->getPaths();
979        if (isset($paths['aPath_clean'][2])) {
980            $retval .= "<span class='hide pos2_name'>";
981            $retval .= $paths['aPath_clean'][2];
982            $retval .= "</span>";
983            $retval .= "<span class='hide pos2_value'>";
984            $retval .= htmlspecialchars($node->pos2);
985            $retval .= "</span>";
986        }
987        if (isset($paths['aPath_clean'][4])) {
988            $retval .= "<span class='hide pos3_name'>";
989            $retval .= $paths['aPath_clean'][4];
990            $retval .= "</span>";
991            $retval .= "<span class='hide pos3_value'>";
992            $retval .= htmlspecialchars($node->pos3);
993            $retval .= "</span>";
994        }
995
996        return $retval;
997    }
998
999    /**
1000     * Finds whether given tree matches this tree.
1001     *
1002     * @param array $tree  Tree to check
1003     * @param array $paths Paths to check
1004     *
1005     * @return boolean
1006     */
1007    private function _findTreeMatch(array $tree, array $paths)
1008    {
1009        $match = false;
1010        foreach ($tree as $path) {
1011            $match = true;
1012            foreach ($paths as $key => $part) {
1013                if (!isset($path[$key]) || $part != $path[$key]) {
1014                    $match = false;
1015                    break;
1016                }
1017            }
1018            if ($match) {
1019                break;
1020            }
1021        }
1022
1023        return $match;
1024    }
1025
1026    /**
1027     * Renders a single node or a branch of the tree
1028     *
1029     * @param Node   $node      The node to render
1030     * @param bool   $recursive Bool: Whether to render a single node or a branch
1031     * @param string $class     An additional class for the list item
1032     *
1033     * @return string HTML code for the tree node or branch
1034     */
1035    private function _renderNode($node, $recursive, $class = '')
1036    {
1037        $retval = '';
1038        $paths = $node->getPaths();
1039        if ($node->hasSiblings()
1040            || $node->realParent() === false
1041        ) {
1042            $response = Response::getInstance();
1043            if ($node->type == Node::CONTAINER
1044                && count($node->children) == 0
1045                && ! $response->isAjax()
1046            ) {
1047                return '';
1048            }
1049            $retval .= '<li class="' . trim($class . ' ' . $node->classes) . '">';
1050            $sterile = array(
1051                'events',
1052                'triggers',
1053                'functions',
1054                'procedures',
1055                'views',
1056                'columns',
1057                'indexes',
1058            );
1059            $parentName = '';
1060            $parents = $node->parents(false, true);
1061            if (count($parents)) {
1062                $parentName = $parents[0]->real_name;
1063            }
1064            // if node name itself is in sterile, then allow
1065            if ($node->is_group
1066                || (!in_array($parentName, $sterile) && !$node->isNew)
1067                || (in_array($node->real_name, $sterile))
1068            ) {
1069                $retval .= "<div class='block'>";
1070                $iClass = '';
1071                if ($class == 'first') {
1072                    $iClass = " class='first'";
1073                }
1074                $retval .= "<i$iClass></i>";
1075                if (strpos($class, 'last') === false) {
1076                    $retval .= "<b></b>";
1077                }
1078
1079                $match = $this->_findTreeMatch(
1080                    $this->_vPath,
1081                    $paths['vPath_clean']
1082                );
1083
1084                $retval .= '<a class="' . $node->getCssClasses($match) . '"';
1085                $retval .= " href='#'>";
1086                $retval .= "<span class='hide aPath'>";
1087                $retval .= $paths['aPath'];
1088                $retval .= "</span>";
1089                $retval .= "<span class='hide vPath'>";
1090                $retval .= $paths['vPath'];
1091                $retval .= "</span>";
1092                $retval .= "<span class='hide pos'>";
1093                $retval .= $this->_pos;
1094                $retval .= "</span>";
1095                $retval .= $this->_getPaginationParamsHtml($node);
1096                if ($GLOBALS['cfg']['ShowDatabasesNavigationAsTree']
1097                    || $parentName != 'root'
1098                ) {
1099                    $retval .= $node->getIcon($match);
1100                }
1101
1102                $retval .= "</a>";
1103                $retval .= "</div>";
1104            } else {
1105                $retval .= "<div class='block'>";
1106                $iClass = '';
1107                if ($class == 'first') {
1108                    $iClass = " class='first'";
1109                }
1110                $retval .= "<i$iClass></i>";
1111                $retval .= $this->_getPaginationParamsHtml($node);
1112                $retval .= "</div>";
1113            }
1114
1115            $linkClass = '';
1116            $haveAjax = array(
1117                'functions',
1118                'procedures',
1119                'events',
1120                'triggers',
1121                'indexes',
1122            );
1123            $parent = $node->parents(false, true);
1124            $isNewView = $parent[0]->real_name == 'views' && $node->isNew === true;
1125            if ($parent[0]->type == Node::CONTAINER
1126                && (in_array($parent[0]->real_name, $haveAjax) || $isNewView)
1127            ) {
1128                $linkClass = ' ajax';
1129            }
1130
1131            if ($node->type == Node::CONTAINER) {
1132                $retval .= "<i>";
1133            }
1134
1135            $divClass = '';
1136
1137            if (isset($node->links['icon']) && !empty($node->links['icon'])) {
1138                $iconLinks = $node->links['icon'];
1139                $icons = $node->icon;
1140                if (!is_array($iconLinks)) {
1141                    $iconLinks = array($iconLinks);
1142                    $icons = array($icons);
1143                }
1144
1145                if (count($icons) > 1) {
1146                    $divClass = 'double';
1147                }
1148            }
1149
1150            $retval .= "<div class='block " . $divClass . "'>";
1151
1152            if (isset($node->links['icon']) && !empty($node->links['icon'])) {
1153                $args = array();
1154                foreach ($node->parents(true) as $parent) {
1155                    $args[] = urlencode($parent->real_name);
1156                }
1157
1158                foreach ($icons as $key => $icon) {
1159                    $link = vsprintf($iconLinks[$key], $args);
1160                    if ($linkClass != '') {
1161                        $retval .= "<a class='$linkClass' href='$link'>";
1162                        $retval .= "{$icon}</a>";
1163                    } else {
1164                        $retval .= "<a href='$link'>{$icon}</a>";
1165                    }
1166                }
1167            } else {
1168                $retval .= "<u>{$node->icon}</u>";
1169            }
1170            $retval .= "</div>";
1171
1172            if (isset($node->links['text'])) {
1173                $args = array();
1174                foreach ($node->parents(true) as $parent) {;
1175                    $args[] = urlencode($parent->real_name);
1176                }
1177                $link = vsprintf($node->links['text'], $args);
1178                $title = isset($node->links['title']) ? $node->links['title'] : '';
1179                if ($node->type == Node::CONTAINER) {
1180                    $retval .= "&nbsp;<a class='hover_show_full' href='$link'>";
1181                    $retval .= htmlspecialchars($node->name);
1182                    $retval .= "</a>";
1183                } else {
1184                    $retval .= "<a class='hover_show_full$linkClass' href='$link'";
1185                    $retval .= " title='$title'>";
1186                    $retval .= htmlspecialchars($node->real_name);
1187                    $retval .= "</a>";
1188                }
1189            } else {
1190                $retval .= "&nbsp;{$node->name}";
1191            }
1192            $retval .= $node->getHtmlForControlButtons();
1193            if ($node->type == Node::CONTAINER) {
1194                $retval .= "</i>";
1195            }
1196            $retval .= '<div class="clearfloat"></div>';
1197            $wrap = true;
1198        } else {
1199            $node->visible = true;
1200            $wrap = false;
1201            $retval .= $this->_getPaginationParamsHtml($node);
1202        }
1203
1204        if ($recursive) {
1205            $hide = '';
1206            if (!$node->visible) {
1207                $hide = " style='display: none;'";
1208            }
1209            $children = $node->children;
1210            usort(
1211                $children,
1212                array('PhpMyAdmin\\Navigation\\NavigationTree', 'sortNode')
1213            );
1214            $buffer = '';
1215            $extra_class = '';
1216            for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) {
1217                if ($i + 1 == $nbChildren) {
1218                    $extra_class = ' last';
1219                }
1220                $buffer .= $this->_renderNode(
1221                    $children[$i],
1222                    true,
1223                    $children[$i]->classes . $extra_class
1224                );
1225            }
1226            if (!empty($buffer)) {
1227                if ($wrap) {
1228                    $retval .= "<div$hide class='list_container'><ul>";
1229                }
1230                $retval .= $this->_fastFilterHtml($node);
1231                $retval .= $this->_getPageSelector($node);
1232                $retval .= $buffer;
1233                if ($wrap) {
1234                    $retval .= "</ul></div>";
1235                }
1236            }
1237        }
1238        if ($node->hasSiblings()) {
1239            $retval .= "</li>";
1240        }
1241
1242        return $retval;
1243    }
1244
1245    /**
1246     * Renders a database select box like the pre-4.0 navigation panel
1247     *
1248     * @return string HTML code
1249     */
1250    public function renderDbSelect()
1251    {
1252        $this->_buildPath();
1253        $retval = $this->_quickWarp();
1254        $this->_tree->is_group = false;
1255        $retval .= '<div id="pma_navigation_select_database">';
1256        // Provide for pagination in database select
1257        $retval .= Util::getListNavigator(
1258            $this->_tree->getPresence('databases', ''),
1259            $this->_pos,
1260            array('server' => $GLOBALS['server']),
1261            'navigation.php',
1262            'frame_navigation',
1263            $GLOBALS['cfg']['FirstLevelNavigationItems'],
1264            'pos',
1265            array('dbselector')
1266        );
1267        $children = $this->_tree->children;
1268        $url_params = array(
1269            'server' => $GLOBALS['server'],
1270        );
1271        $retval .= '<div id="pma_navigation_db_select">';
1272        $retval .= '<form action="index.php">';
1273        $retval .= Url::getHiddenFields($url_params);
1274        $retval .= '<select name="db" class="hide" id="navi_db_select">'
1275            . '<option value="" dir="' . $GLOBALS['text_dir'] . '">'
1276            . '(' . __('Databases') . ') ...</option>' . "\n";
1277        $selected = $GLOBALS['db'];
1278        foreach ($children as $node) {
1279            if ($node->isNew) {
1280                continue;
1281            }
1282            $paths = $node->getPaths();
1283            if (isset($node->links['text'])) {
1284                $title = isset($node->links['title']) ? '' : $node->links['title'];
1285                $retval .= '<option value="'
1286                    . htmlspecialchars($node->real_name) . '"'
1287                    . ' title="' . htmlspecialchars($title) . '"'
1288                    . ' apath="' . $paths['aPath'] . '"'
1289                    . ' vpath="' . $paths['vPath'] . '"'
1290                    . ' pos="' . $this->_pos . '"';
1291                if ($node->real_name == $selected) {
1292                    $retval .= ' selected="selected"';
1293                }
1294                $retval .= '>' . htmlspecialchars($node->real_name);
1295                $retval .= '</option>';
1296            }
1297        }
1298        $retval .= '</select></form>';
1299        $retval .= '</div></div>';
1300        $retval .= '<div id="pma_navigation_tree_content"><ul>';
1301        $children = $this->_tree->children;
1302        usort(
1303            $children,
1304            array('PhpMyAdmin\\Navigation\\NavigationTree', 'sortNode')
1305        );
1306        $this->_setVisibility();
1307        for ($i = 0, $nbChildren = count($children); $i < $nbChildren; $i++) {
1308            if ($i == 0) {
1309                $retval .= $this->_renderNode($children[0], true, 'first');
1310            } else {
1311                if ($i + 1 != $nbChildren) {
1312                    $retval .= $this->_renderNode($children[$i], true);
1313                } else {
1314                    $retval .= $this->_renderNode($children[$i], true, 'last');
1315                }
1316            }
1317        }
1318        $retval .= '</ul></div>';
1319
1320        return $retval;
1321    }
1322
1323    /**
1324     * Makes some nodes visible based on the which node is active
1325     *
1326     * @return void
1327     */
1328    private function _setVisibility()
1329    {
1330        foreach ($this->_vPath as $path) {
1331            $node = $this->_tree;
1332            foreach ($path as $value) {
1333                $child = $node->getChild($value);
1334                if ($child !== false) {
1335                    $child->visible = true;
1336                    $node = $child;
1337                }
1338            }
1339        }
1340    }
1341
1342    /**
1343     * Generates the HTML code for displaying the fast filter for tables
1344     *
1345     * @param Node $node The node for which to generate the fast filter html
1346     *
1347     * @return string LI element used for the fast filter
1348     */
1349    private function _fastFilterHtml($node)
1350    {
1351        $retval = '';
1352        $filter_db_min
1353            = (int)$GLOBALS['cfg']['NavigationTreeDisplayDbFilterMinimum'];
1354        $filter_item_min
1355            = (int)$GLOBALS['cfg']['NavigationTreeDisplayItemFilterMinimum'];
1356        if ($node === $this->_tree
1357            && $this->_tree->getPresence() >= $filter_db_min
1358        ) {
1359            $url_params = array(
1360                'pos' => 0,
1361            );
1362            $retval .= '<li class="fast_filter db_fast_filter">';
1363            $retval .= '<form class="ajax fast_filter">';
1364            $retval .= Url::getHiddenInputs($url_params);
1365            $retval .= '<input class="searchClause" type="text"';
1366            $retval .= ' name="searchClause" accesskey="q"';
1367            $retval .= " placeholder='"
1368                . __("Type to filter these, Enter to search all");
1369            $retval .= "' />";
1370            $retval .= '<span title="' . __('Clear fast filter') . '">X</span>';
1371            $retval .= "</form>";
1372            $retval .= "</li>";
1373
1374            return $retval;
1375        }
1376
1377        if (($node->type == Node::CONTAINER
1378            && ($node->real_name == 'tables'
1379            || $node->real_name == 'views'
1380            || $node->real_name == 'functions'
1381            || $node->real_name == 'procedures'
1382            || $node->real_name == 'events'))
1383            && method_exists($node->realParent(), 'getPresence')
1384            && $node->realParent()->getPresence($node->real_name) >= $filter_item_min
1385        ) {
1386            $paths = $node->getPaths();
1387            $url_params = array(
1388                'pos'        => $this->_pos,
1389                'aPath'      => $paths['aPath'],
1390                'vPath'      => $paths['vPath'],
1391                'pos2_name'  => $node->real_name,
1392                'pos2_value' => 0,
1393            );
1394            $retval .= "<li class='fast_filter'>";
1395            $retval .= "<form class='ajax fast_filter'>";
1396            $retval .= Url::getHiddenFields($url_params);
1397            $retval .= "<input class='searchClause' type='text'";
1398            $retval .= " name='searchClause2'";
1399            $retval .= " placeholder='"
1400                . __("Type to filter these, Enter to search all") . "' />";
1401            $retval .= "<span title='" . __('Clear fast filter') . "'>X</span>";
1402            $retval .= "</form>";
1403            $retval .= "</li>";
1404        }
1405
1406        return $retval;
1407    }
1408
1409    /**
1410     * Creates the code for displaying the controls
1411     * at the top of the navigation tree
1412     *
1413     * @return string HTML code for the controls
1414     */
1415    private function _controls()
1416    {
1417        // always iconic
1418        $showIcon = true;
1419        $showText = false;
1420
1421        $retval = '<!-- CONTROLS START -->';
1422        $retval .= '<li id="navigation_controls_outer">';
1423        $retval .= '<div id="navigation_controls">';
1424        $retval .= Util::getNavigationLink(
1425            '#',
1426            $showText,
1427            __('Collapse all'),
1428            $showIcon,
1429            's_collapseall',
1430            'pma_navigation_collapse'
1431        );
1432        $syncImage = 's_unlink';
1433        $title = __('Link with main panel');
1434        if ($GLOBALS['cfg']['NavigationLinkWithMainPanel']) {
1435            $syncImage = 's_link';
1436            $title = __('Unlink from main panel');
1437        }
1438        $retval .= Util::getNavigationLink(
1439            '#',
1440            $showText,
1441            $title,
1442            $showIcon,
1443            $syncImage,
1444            'pma_navigation_sync'
1445        );
1446        $retval .= '</div>';
1447        $retval .= '</li>';
1448        $retval .= '<!-- CONTROLS ENDS -->';
1449
1450        return $retval;
1451    }
1452
1453    /**
1454     * Generates the HTML code for displaying the list pagination
1455     *
1456     * @param Node $node The node for whose children the page
1457     *                   selector will be created
1458     *
1459     * @return string
1460     */
1461    private function _getPageSelector($node)
1462    {
1463        $retval = '';
1464        if ($node === $this->_tree) {
1465            $retval .= Util::getListNavigator(
1466                $this->_tree->getPresence('databases', $this->_searchClause),
1467                $this->_pos,
1468                array('server' => $GLOBALS['server']),
1469                'navigation.php',
1470                'frame_navigation',
1471                $GLOBALS['cfg']['FirstLevelNavigationItems'],
1472                'pos',
1473                array('dbselector')
1474            );
1475        } else {
1476            if ($node->type == Node::CONTAINER && !$node->is_group) {
1477                $paths = $node->getPaths();
1478
1479                $level = isset($paths['aPath_clean'][4]) ? 3 : 2;
1480                $_url_params = array(
1481                    'aPath'     => $paths['aPath'],
1482                    'vPath'     => $paths['vPath'],
1483                    'pos'       => $this->_pos,
1484                    'server'    => $GLOBALS['server'],
1485                    'pos2_name' => $paths['aPath_clean'][2],
1486                );
1487                if ($level == 3) {
1488                    $pos = $node->pos3;
1489                    $_url_params['pos2_value'] = $node->pos2;
1490                    $_url_params['pos3_name'] = $paths['aPath_clean'][4];
1491                } else {
1492                    $pos = $node->pos2;
1493                }
1494                $num = $node->realParent()
1495                    ->getPresence(
1496                        $node->real_name,
1497                        $this->_searchClause2
1498                    );
1499                $retval .= Util::getListNavigator(
1500                    $num,
1501                    $pos,
1502                    $_url_params,
1503                    'navigation.php',
1504                    'frame_navigation',
1505                    $GLOBALS['cfg']['MaxNavigationItems'],
1506                    'pos' . $level . '_value'
1507                );
1508            }
1509        }
1510
1511        return $retval;
1512    }
1513
1514    /**
1515     * Called by usort() for sorting the nodes in a container
1516     *
1517     * @param Node $a The first element used in the comparison
1518     * @param Node $b The second element used in the comparison
1519     *
1520     * @return int See strnatcmp() and strcmp()
1521     */
1522    static public function sortNode($a, $b)
1523    {
1524        if ($a->isNew) {
1525            return -1;
1526        }
1527
1528        if ($b->isNew) {
1529            return 1;
1530        }
1531
1532        if ($GLOBALS['cfg']['NaturalOrder']) {
1533            return strnatcasecmp($a->name, $b->name);
1534        }
1535
1536        return strcasecmp($a->name, $b->name);
1537    }
1538
1539    /**
1540     * Display quick warp links, contain Recents and Favorites
1541     *
1542     * @return string HTML code
1543     */
1544    private function _quickWarp()
1545    {
1546        $retval = '<div class="pma_quick_warp">';
1547        if ($GLOBALS['cfg']['NumRecentTables'] > 0) {
1548            $retval .= RecentFavoriteTable::getInstance('recent')
1549                ->getHtml();
1550        }
1551        if ($GLOBALS['cfg']['NumFavoriteTables'] > 0) {
1552            $retval .= RecentFavoriteTable::getInstance('favorite')
1553                ->getHtml();
1554        }
1555        $retval .= '<div class="clearfloat"></div>';
1556        $retval .= '</div>';
1557
1558        return $retval;
1559    }
1560}
1561