UPDATE 4.4.0.0
[phpmyadmin.git] / libraries / navigation / Nodes / Node.class.php
blobb6327082870c53d2ba2e6b6793b595f241876357
1 <?php
2 /* vim: set expandtab sw=4 ts=4 sts=4: */
3 /**
4 * Functionality for the navigation tree in the left frame
6 * @package PhpMyAdmin-Navigation
7 */
8 if (! defined('PHPMYADMIN')) {
9 exit;
12 /**
13 * The Node is the building block for the collapsible navigation tree
15 * @package PhpMyAdmin-Navigation
17 class Node
19 /**
20 * @var int Defines a possible node type
22 const CONTAINER = 0;
24 /**
25 * @var int Defines a possible node type
27 const OBJECT = 1;
29 /**
30 * @var string A non-unique identifier for the node
31 * This may be trimmed when grouping nodes
33 public $name = "";
35 /**
36 * @var string A non-unique identifier for the node
37 * This will never change after being assigned
39 public $real_name = "";
41 /**
42 * @var int May be one of CONTAINER or OBJECT
44 public $type = Node::OBJECT;
46 /**
47 * @var bool Whether this object has been created while grouping nodes
48 * Only relevant if the node is of type CONTAINER
50 public $is_group;
52 /**
53 * @var bool Whether to add a "display: none;" CSS
54 * rule to the node when rendering it
56 public $visible = false;
58 /**
59 * @var Node A reference to the parent object of
60 * this node, NULL for the root node.
62 public $parent;
64 /**
65 * @var array An array of Node objects that are
66 * direct children of this node
68 public $children = array();
70 /**
71 * @var Mixed A string used to group nodes, or an array of strings
72 * Only relevant if the node is of type CONTAINER
74 public $separator = '';
76 /**
77 * @var int How many time to recursively apply the grouping function
78 * Only relevant if the node is of type CONTAINER
80 public $separator_depth = 1;
82 /**
83 * @var string An IMG tag, used when rendering the node
85 public $icon;
87 /**
88 * @var Array An array of A tags, used when rendering the node
89 * The indexes in the array may be 'icon' and 'text'
91 public $links;
93 /**
94 * @var string HTML title
96 public $title;
98 /**
99 * @var string Extra CSS classes for the node
101 public $classes = '';
104 * @var string Whether this node is a link for creating new objects
106 public $isNew = false;
109 * @var int The position for the pagination of
110 * the branch at the second level of the tree
112 public $pos2 = 0;
115 * @var int The position for the pagination of
116 * the branch at the third level of the tree
118 public $pos3 = 0;
121 * Initialises the class by setting the mandatory variables
123 * @param string $name An identifier for the new node
124 * @param int $type Type of node, may be one of CONTAINER or OBJECT
125 * @param bool $is_group Whether this object has been created
126 * while grouping nodes
128 * @return Node
130 public function __construct($name, $type = Node::OBJECT, $is_group = false)
132 if (! empty($name)) {
133 $this->name = $name;
134 $this->real_name = $name;
136 if ($type === Node::CONTAINER) {
137 $this->type = Node::CONTAINER;
139 $this->is_group = (bool)$is_group;
143 * Adds a child node to this node
145 * @param Node $child A child node
147 * @return void
149 public function addChild($child)
151 $this->children[] = $child;
152 $child->parent = $this;
156 * Returns a child node given it's name
158 * @param string $name The name of requested child
159 * @param bool $real_name Whether to use the "real_name"
160 * instead of "name" in comparisons
162 * @return false|Node The requested child node or false,
163 * if the requested node cannot be found
165 public function getChild($name, $real_name = false)
167 if ($real_name) {
168 foreach ($this->children as $child) {
169 if ($child->real_name == $name) {
170 return $child;
173 } else {
174 foreach ($this->children as $child) {
175 if ($child->name == $name) {
176 return $child;
180 return false;
184 * Removes a child node from this node
186 * @param string $name The name of child to be removed
188 * @return void
190 public function removeChild($name)
192 foreach ($this->children as $key => $child) {
193 if ($child->name == $name) {
194 unset($this->children[$key]);
195 break;
201 * Retrieves the parents for a node
203 * @param bool $self Whether to include the Node itself in the results
204 * @param bool $containers Whether to include nodes of type CONTAINER
205 * @param bool $groups Whether to include nodes which have $group == true
207 * @return array An array of parent Nodes
209 public function parents($self = false, $containers = false, $groups = false)
211 $parents = array();
212 if ($self
213 && ($this->type != Node::CONTAINER || $containers)
214 && ($this->is_group != true || $groups)
216 $parents[] = $this;
218 $parent = $this->parent;
219 while (isset($parent)) {
220 if ( ($parent->type != Node::CONTAINER || $containers)
221 && ($parent->is_group != true || $groups)
223 $parents[] = $parent;
225 $parent = $parent->parent;
227 return $parents;
231 * Returns the actual parent of a node. If used twice on an index or columns
232 * node, it will return the table and database nodes. The names of the returned
233 * nodes can be used in SQL queries, etc...
235 * @return Node|false
237 public function realParent()
239 $retval = $this->parents();
240 if (count($retval) <= 0) {
241 return false;
244 return $retval[0];
248 * This function checks if the node has children nodes associated with it
250 * @param bool $count_empty_containers Whether to count empty child
251 * containers as valid children
253 * @return bool Whether the node has child nodes
255 public function hasChildren($count_empty_containers = true)
257 $retval = false;
258 if ($count_empty_containers) {
259 if (count($this->children)) {
260 $retval = true;
262 } else {
263 foreach ($this->children as $child) {
264 if ($child->type == Node::OBJECT || $child->hasChildren(false)) {
265 $retval = true;
266 break;
270 return $retval;
274 * Returns true if the node has some siblings (other nodes on the same tree
275 * level, in the same branch), false otherwise.
276 * The only exception is for nodes on
277 * the third level of the tree (columns and indexes), for which the function
278 * always returns true. This is because we want to render the containers
279 * for these nodes
281 * @return bool
283 public function hasSiblings()
285 $retval = false;
286 $paths = $this->getPaths();
287 if (count($paths['aPath_clean']) > 3) {
288 $retval = true;
289 return $retval;
292 foreach ($this->parent->children as $child) {
293 if ($child !== $this
294 && ($child->type == Node::OBJECT || $child->hasChildren(false))
296 $retval = true;
297 break;
300 return $retval;
304 * Returns the number of child nodes that a node has associated with it
306 * @return int The number of children nodes
308 public function numChildren()
310 $retval = 0;
311 foreach ($this->children as $child) {
312 if ($child->type == Node::OBJECT) {
313 $retval++;
314 } else {
315 $retval += $child->numChildren();
318 return $retval;
322 * Returns the actual path and the virtual paths for a node
323 * both as clean arrays and base64 encoded strings
325 * @return array
327 public function getPaths()
329 $aPath = array();
330 $aPath_clean = array();
331 foreach ($this->parents(true, true, false) as $parent) {
332 $aPath[] = base64_encode($parent->real_name);
333 $aPath_clean[] = $parent->real_name;
335 $aPath = implode('.', array_reverse($aPath));
336 $aPath_clean = array_reverse($aPath_clean);
338 $vPath = array();
339 $vPath_clean = array();
340 foreach ($this->parents(true, true, true) as $parent) {
341 $vPath[] = base64_encode($parent->name);
342 $vPath_clean[] = $parent->name;
344 $vPath = implode('.', array_reverse($vPath));
345 $vPath_clean = array_reverse($vPath_clean);
347 return array(
348 'aPath' => $aPath,
349 'aPath_clean' => $aPath_clean,
350 'vPath' => $vPath,
351 'vPath_clean' => $vPath_clean
356 * Returns the names of children of type $type present inside this container
357 * This method is overridden by the Node_Database and Node_Table classes
359 * @param string $type The type of item we are looking for
360 * ('tables', 'views', etc)
361 * @param int $pos The offset of the list within the results
362 * @param string $searchClause A string used to filter the results of the query
364 * @return array
366 public function getData($type, $pos, $searchClause = '')
368 $maxItems = $GLOBALS['cfg']['FirstLevelNavigationItems'];
369 if (!$GLOBALS['cfg']['NavigationTreeEnableGrouping']
370 || !$GLOBALS['cfg']['ShowDatabasesNavigationAsTree']
372 if (isset($GLOBALS['cfg']['Server']['DisableIS'])
373 && ! $GLOBALS['cfg']['Server']['DisableIS']
375 $query = "SELECT `SCHEMA_NAME` ";
376 $query .= "FROM `INFORMATION_SCHEMA`.`SCHEMATA` ";
377 $query .= $this->_getWhereClause('SCHEMA_NAME', $searchClause);
378 $query .= "ORDER BY `SCHEMA_NAME` ";
379 $query .= "LIMIT $pos, $maxItems";
380 $retval = $GLOBALS['dbi']->fetchResult($query);
381 return $retval;
384 if ($GLOBALS['dbs_to_test'] === false) {
385 $retval = array();
386 $query = "SHOW DATABASES ";
387 $query .= $this->_getWhereClause('Database', $searchClause);
388 $handle = $GLOBALS['dbi']->tryQuery($query);
389 if ($handle !== false) {
390 $count = 0;
391 while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
392 if ($pos <= 0 && $count < $maxItems) {
393 $retval[] = $arr[0];
394 $count++;
396 $pos--;
399 return $retval;
402 $retval = array();
403 $count = 0;
404 foreach ($this->_getDatabasesToSearch($searchClause) as $db) {
405 $query = "SHOW DATABASES LIKE '" . $db . "'";
406 $handle = $GLOBALS['dbi']->tryQuery($query);
407 if ($handle === false) {
408 continue;
411 while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
412 if ($this->_isHideDb($arr[0])) {
413 continue;
415 if (in_array($arr[0], $retval)) {
416 continue;
419 if ($pos <= 0 && $count < $maxItems) {
420 $retval[] = $arr[0];
421 $count++;
423 $pos--;
426 sort($retval);
427 return $retval;
430 $dbSeparator = $GLOBALS['cfg']['NavigationTreeDbSeparator'];
431 if (isset($GLOBALS['cfg']['Server']['DisableIS'])
432 && ! $GLOBALS['cfg']['Server']['DisableIS']
434 $query = "SELECT `SCHEMA_NAME` ";
435 $query .= "FROM `INFORMATION_SCHEMA`.`SCHEMATA`, ";
436 $query .= "(";
437 $query .= "SELECT DB_first_level ";
438 $query .= "FROM ( ";
439 $query .= "SELECT DISTINCT SUBSTRING_INDEX(SCHEMA_NAME, ";
440 $query .= "'$dbSeparator', 1) ";
441 $query .= "DB_first_level ";
442 $query .= "FROM INFORMATION_SCHEMA.SCHEMATA ";
443 $query .= $this->_getWhereClause('SCHEMA_NAME', $searchClause);
444 $query .= ") t ";
445 $query .= "ORDER BY DB_first_level ASC ";
446 $query .= "LIMIT $pos, $maxItems";
447 $query .= ") t2 ";
448 $query .= "WHERE 1 = LOCATE(CONCAT(DB_first_level, ";
449 $query .= "'$dbSeparator'), ";
450 $query .= "CONCAT(SCHEMA_NAME, ";
451 $query .= "'$dbSeparator')) ";
452 $query .= "ORDER BY SCHEMA_NAME ASC";
453 $retval = $GLOBALS['dbi']->fetchResult($query);
454 return $retval;
457 if ($GLOBALS['dbs_to_test'] === false) {
458 $query = "SHOW DATABASES ";
459 $query .= $this->_getWhereClause('Database', $searchClause);
460 $handle = $GLOBALS['dbi']->tryQuery($query);
461 $prefixes = array();
462 if ($handle !== false) {
463 $prefixMap = array();
464 $total = $pos + $maxItems;
465 while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
466 $prefix = strstr($arr[0], $dbSeparator, true);
467 if ($prefix === false) {
468 $prefix = $arr[0];
470 $prefixMap[$prefix] = 1;
471 if (sizeof($prefixMap) == $total) {
472 break;
475 $prefixes = array_slice(array_keys($prefixMap), $pos);
478 $query = "SHOW DATABASES ";
479 $query .= $this->_getWhereClause('Database', '');
480 $query .= " AND (";
481 $subClauses = array();
482 foreach ($prefixes as $prefix) {
483 $subClauses[] = " LOCATE('"
484 . PMA_Util::sqlAddSlashes($prefix) . $dbSeparator . "', "
485 . "CONCAT(`Database`, '" . $dbSeparator . "')) = 1 ";
487 $query .= implode("OR", $subClauses) . ")";
488 $retval = $GLOBALS['dbi']->fetchResult($query);
489 return $retval;
492 $retval = array();
493 $prefixMap = array();
494 $total = $pos + $maxItems;
495 foreach ($this->_getDatabasesToSearch($searchClause) as $db) {
496 $query = "SHOW DATABASES LIKE '" . $db . "'";
497 $handle = $GLOBALS['dbi']->tryQuery($query);
498 if ($handle === false) {
499 continue;
502 while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
503 if ($this->_isHideDb($arr[0])) {
504 continue;
506 $prefix = strstr($arr[0], $dbSeparator, true);
507 if ($prefix === false) {
508 $prefix = $arr[0];
510 $prefixMap[$prefix] = 1;
511 if (sizeof($prefixMap) == $total) {
512 break 2;
516 $prefixes = array_slice(array_keys($prefixMap), $pos);
518 foreach ($this->_getDatabasesToSearch($searchClause) as $db) {
519 $query = "SHOW DATABASES LIKE '" . $db . "'";
520 $handle = $GLOBALS['dbi']->tryQuery($query);
521 if ($handle === false) {
522 continue;
525 while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
526 if ($this->_isHideDb($arr[0])) {
527 continue;
529 if (in_array($arr[0], $retval)) {
530 continue;
533 foreach ($prefixes as $prefix) {
534 $starts_with = strpos(
535 $arr[0] . $dbSeparator,
536 $prefix . $dbSeparator
537 ) === 0;
538 if ($starts_with) {
539 $retval[] = $arr[0];
540 break;
545 sort($retval);
547 return $retval;
551 * Returns the number of children of type $type present inside this container
552 * This method is overridden by the Node_Database and Node_Table classes
554 * @param string $type The type of item we are looking for
555 * ('tables', 'views', etc)
556 * @param string $searchClause A string used to filter the results of the query
558 * @return int
560 public function getPresence($type = '', $searchClause = '')
562 if (!$GLOBALS['cfg']['NavigationTreeEnableGrouping']
563 || !$GLOBALS['cfg']['ShowDatabasesNavigationAsTree']
565 if (isset($GLOBALS['cfg']['Server']['DisableIS'])
566 && ! $GLOBALS['cfg']['Server']['DisableIS']
568 $query = "SELECT COUNT(*) ";
569 $query .= "FROM INFORMATION_SCHEMA.SCHEMATA ";
570 $query .= $this->_getWhereClause('SCHEMA_NAME', $searchClause);
571 $retval = (int)$GLOBALS['dbi']->fetchValue($query);
572 return $retval;
575 if ($GLOBALS['dbs_to_test'] === false) {
576 $query = "SHOW DATABASES ";
577 $query .= $this->_getWhereClause('Database', $searchClause);
578 $retval = $GLOBALS['dbi']->numRows(
579 $GLOBALS['dbi']->tryQuery($query)
581 return $retval;
584 $retval = 0;
585 foreach ($this->_getDatabasesToSearch($searchClause) as $db) {
586 $query = "SHOW DATABASES LIKE '" . $db . "'";
587 $retval += $GLOBALS['dbi']->numRows(
588 $GLOBALS['dbi']->tryQuery($query)
591 return $retval;
594 $dbSeparator = $GLOBALS['cfg']['NavigationTreeDbSeparator'];
595 if (! $GLOBALS['cfg']['Server']['DisableIS']) {
596 $query = "SELECT COUNT(*) ";
597 $query .= "FROM ( ";
598 $query .= "SELECT DISTINCT SUBSTRING_INDEX(SCHEMA_NAME, ";
599 $query .= "'$dbSeparator', 1) ";
600 $query .= "DB_first_level ";
601 $query .= "FROM INFORMATION_SCHEMA.SCHEMATA ";
602 $query .= $this->_getWhereClause('SCHEMA_NAME', $searchClause);
603 $query .= ") t ";
604 $retval = (int)$GLOBALS['dbi']->fetchValue($query);
605 return $retval;
608 if ($GLOBALS['dbs_to_test'] !== false) {
609 $prefixMap = array();
610 foreach ($this->_getDatabasesToSearch($searchClause) as $db) {
611 $query = "SHOW DATABASES LIKE '" . $db . "'";
612 $handle = $GLOBALS['dbi']->tryQuery($query);
613 if ($handle === false) {
614 continue;
617 while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
618 if ($this->_isHideDb($arr[0])) {
619 continue;
621 $prefix = strstr($arr[0], $dbSeparator, true);
622 if ($prefix === false) {
623 $prefix = $arr[0];
625 $prefixMap[$prefix] = 1;
628 $retval = count($prefixMap);
629 return $retval;
632 $prefixMap = array();
633 $query = "SHOW DATABASES ";
634 $query .= $this->_getWhereClause('Database', $searchClause);
635 $handle = $GLOBALS['dbi']->tryQuery($query);
636 if ($handle !== false) {
637 while ($arr = $GLOBALS['dbi']->fetchArray($handle)) {
638 $prefix = strstr($arr[0], $dbSeparator, true);
639 if ($prefix === false) {
640 $prefix = $arr[0];
642 $prefixMap[$prefix] = 1;
645 $retval = count($prefixMap);
647 return $retval;
651 * Detemines whether a given database should be hidden according to 'hide_db'
653 * @param string $db database name
655 * @return boolean whether to hide
657 private function _isHideDb($db)
659 if (! empty($GLOBALS['cfg']['Server']['hide_db'])
660 && preg_match('/' . $GLOBALS['cfg']['Server']['hide_db'] . '/', $db)
662 return true;
664 return false;
668 * Get the list of databases for 'SHOW DATABASES LIKE' queries.
669 * If a search clause is set it gets the highest priority while only_db gets
670 * the next priority. In case both are empty list of databases determined by
671 * GRANTs are used
673 * @param string $searchClause search clause
675 * @return array array of databases
677 private function _getDatabasesToSearch($searchClause)
679 if (! empty($searchClause)) {
680 $databases = array(
681 "%" . PMA_Util::sqlAddSlashes($searchClause, true) . "%"
683 } elseif (! empty($GLOBALS['cfg']['Server']['only_db'])) {
684 $databases = $GLOBALS['cfg']['Server']['only_db'];
685 } elseif (! empty($GLOBALS['dbs_to_test'])) {
686 $databases = $GLOBALS['dbs_to_test'];
688 sort($databases);
689 return $databases;
693 * Returns the WHERE clause depending on the $searchClause parameter
694 * and the hide_db directive
696 * @param string $columnName Column name of the column having database names
697 * @param string $searchClause A string used to filter the results of the query
699 * @return string
701 private function _getWhereClause($columnName, $searchClause = '')
703 $whereClause = "WHERE TRUE ";
704 if (! empty($searchClause)) {
705 $whereClause .= "AND " . PMA_Util::backquote($columnName) . " LIKE '%";
706 $whereClause .= PMA_Util::sqlAddSlashes(
707 $searchClause, true
709 $whereClause .= "%' ";
712 if (! empty($GLOBALS['cfg']['Server']['hide_db'])) {
713 $whereClause .= "AND " . PMA_Util::backquote($columnName)
714 . " NOT REGEXP '" . $GLOBALS['cfg']['Server']['hide_db'] . "' ";
717 if (! empty($GLOBALS['cfg']['Server']['only_db'])) {
718 if (is_string($GLOBALS['cfg']['Server']['only_db'])) {
719 $GLOBALS['cfg']['Server']['only_db'] = array(
720 $GLOBALS['cfg']['Server']['only_db']
723 $whereClause .= "AND (";
724 $subClauses = array();
725 foreach ($GLOBALS['cfg']['Server']['only_db'] as $each_only_db) {
726 $subClauses[] = " " . PMA_Util::backquote($columnName) . " LIKE '"
727 . $each_only_db . "' ";
729 $whereClause .= implode("OR", $subClauses) . ")";
731 return $whereClause;
735 * Returns HTML for control buttons displayed infront of a node
737 * @return String HTML for control buttons
739 public function getHtmlForControlButtons()
741 return '';
745 * Returns CSS classes for a node
747 * @param boolean $match Whether the node matched loaded tree
749 * @return String with html classes.
751 public function getCssClasses($match)
753 if (! $GLOBALS['cfg']['NavigationTreeEnableExpansion']
755 return '';
758 $result = array('expander');
760 if ($this->is_group || $match) {
761 $result[] = 'loaded';
763 if ($this->type == Node::CONTAINER) {
764 $result[] = 'container';
767 return implode(' ', $result);
771 * Returns icon for the node
773 * @param boolean $match Whether the node matched loaded tree
775 * @return String with image name
777 public function getIcon($match)
779 if (! $GLOBALS['cfg']['NavigationTreeEnableExpansion']
781 return '';
782 } elseif ($match && ! $this->is_group) {
783 $this->visible = true;
784 return PMA_Util::getImage('b_minus.png');
785 } else {
786 return PMA_Util::getImage('b_plus.png', __('Expand/Collapse'));
791 * Gets the count of hidden elements for each database
793 * @return array array containing the count of hidden elements for each database
795 public function getNavigationHidingData()
797 $cfgRelation = PMA_getRelationsParam();
798 if (isset($cfgRelation['navwork']) && $cfgRelation['navwork']) {
799 $navTable = PMA_Util::backquote($cfgRelation['db'])
800 . "." . PMA_Util::backquote($cfgRelation['navigationhiding']);
801 $sqlQuery = "SELECT `db_name`, COUNT(*) AS `count` FROM " . $navTable
802 . " WHERE `username`='"
803 . PMA_Util::sqlAddSlashes($GLOBALS['cfg']['Server']['user']) . "'"
804 . " GROUP BY `db_name`";
805 $counts = $GLOBALS['dbi']->fetchResult(
806 $sqlQuery, 'db_name', 'count', $GLOBALS['controllink']
808 return $counts;
810 return null;