674 lines
24 KiB
PHP
674 lines
24 KiB
PHP
<?php
|
|
/* vim: set expandtab sw=4 ts=4 sts=4: */
|
|
/**
|
|
* Functionality for the navigation tree in the left frame
|
|
*
|
|
* @package PhpMyAdmin-Navigation
|
|
*/
|
|
/**
|
|
* Displays a collapsible of database objects in the navigation frame
|
|
*/
|
|
class PMA_NavigationTree {
|
|
/**
|
|
* @var Node Reference to the root node of the tree
|
|
*/
|
|
private $tree;
|
|
|
|
/**
|
|
* @var array The actual paths to all expanded nodes in the tree
|
|
* This does not include nodes created after the grouping
|
|
* of nodes has been performed
|
|
*/
|
|
private $a_path = array();
|
|
|
|
/**
|
|
* @var array The virtual paths to all expanded nodes in the tree
|
|
* This includes nodes created after the grouping of
|
|
* nodes has been performed
|
|
*/
|
|
private $v_path = array();
|
|
|
|
/**
|
|
* @var int Position in the list of databases,
|
|
* used for pagination
|
|
*/
|
|
private $pos;
|
|
|
|
/**
|
|
* @var object A reference to the common functions object
|
|
*/
|
|
private $_commonFunctions;
|
|
|
|
/**
|
|
* Initialises the class
|
|
*
|
|
* @param int $pos Position in the list of databases,
|
|
* used for pagination
|
|
*
|
|
* @return nothing
|
|
*/
|
|
public function __construct($pos)
|
|
{
|
|
$this->_commonFunctions = PMA_commonFunctions::getInstance();
|
|
|
|
// Save the position at which we are in the database list
|
|
$this->pos = $pos;
|
|
// Get the active node
|
|
if (isset($_REQUEST['a_path'])) {
|
|
$this->a_path[0] = $this->parsePath($_REQUEST['a_path']);
|
|
} else if (isset($_REQUEST['a_path_0'])) {
|
|
$count = 0;
|
|
while (isset($_REQUEST['a_path_' . $count])) {
|
|
$this->a_path[$count] = $this->parsePath(
|
|
$_REQUEST['a_path_' . $count]
|
|
);
|
|
$count++;
|
|
}
|
|
}
|
|
if (isset($_REQUEST['v_path'])) {
|
|
$this->v_path[0] = $this->parsePath($_REQUEST['v_path']);
|
|
} else if (isset($_REQUEST['v_path_0'])) {
|
|
$count = 0;
|
|
while (isset($_REQUEST['v_path_' . $count])) {
|
|
$this->v_path[$count] = $this->parsePath(
|
|
$_REQUEST['v_path_' . $count]
|
|
);
|
|
$count++;
|
|
}
|
|
}
|
|
// Initialise the tree by creating a root node
|
|
$node = new Node('root', Node::CONTAINER);
|
|
$this->tree = $node;
|
|
if ($GLOBALS['cfg']['LeftFrameDBTree']) {
|
|
$this->tree->separator = $GLOBALS['cfg']['LeftFrameDBSeparator'];
|
|
$this->tree->separator_depth = 10000;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Converts an encoded path to a node in string format to an array
|
|
*
|
|
* @param string $string The path to parse
|
|
*
|
|
* @return array
|
|
*/
|
|
private function parsePath($string)
|
|
{
|
|
$path = explode('.', $string);
|
|
foreach ($path as $key => $value) {
|
|
$path[$key] = base64_decode($value);
|
|
}
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Generates the tree structure so that it can be rendered later
|
|
*
|
|
* @return Node|false The active node or false in case of failure
|
|
*/
|
|
private function buildPath()
|
|
{
|
|
$retval = $this->tree;
|
|
|
|
// Add all databases unconditionally
|
|
foreach ($this->tree->getData($this->pos) as $db) {
|
|
$node = new Node_Database($db);
|
|
$this->tree->addChild($node);
|
|
}
|
|
|
|
// Whether build other parts of the tree depends
|
|
// on whether we have any paths in $this->a_path
|
|
foreach ($this->a_path as $path) {
|
|
$retval = $this->buildPathPart($path, $retval);
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Builds a branch of the tree
|
|
*
|
|
* @param array $path A paths pointing to the branch
|
|
* of the tree that needs to be built
|
|
*
|
|
* @return Node|false The active node or false in case of failure
|
|
*/
|
|
private function buildPathPart($path)
|
|
{
|
|
if (count($path) > 1) {
|
|
array_shift($path); // remove 'root'
|
|
$db = $this->tree->getChild($path[0]);
|
|
$retval = $db;
|
|
|
|
$containers = $this->addDbContainers($db);
|
|
|
|
array_shift($path); // remove db
|
|
|
|
if (count($path) > 0 && array_key_exists($path[0], $containers) || count($containers) == 1) {
|
|
if (count($containers) == 1) {
|
|
$container = array_shift($containers);
|
|
} else {
|
|
$container = $db->getChild($path[0], true);
|
|
}
|
|
$retval = $container;
|
|
|
|
if (count($container->children) <= 1) {
|
|
foreach ($db->getData($container->real_name) as $item) {
|
|
switch ($container->real_name) {
|
|
case 'events':
|
|
$node = new Node_Event($item);
|
|
break;
|
|
case 'functions':
|
|
$node = new Node_Function($item);
|
|
break;
|
|
case 'procedures':
|
|
$node = new Node_Procedure($item);
|
|
break;
|
|
case 'tables':
|
|
$node = new Node_Table($item);
|
|
break;
|
|
case 'views':
|
|
$node = new Node_View($item);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (isset($node)) {
|
|
$container->addChild($node);
|
|
}
|
|
}
|
|
}
|
|
if (count($path) > 1 && $path[0] != 'tables') {
|
|
$retval = false;
|
|
} else {
|
|
array_shift($path); // remove container
|
|
if (count($path) > 0) {
|
|
$table = $container->getChild($path[0], true);
|
|
$retval = $table;
|
|
$containers = $this->addTableContainers($table);
|
|
array_shift($path); // remove table
|
|
foreach ($containers as $container) {
|
|
foreach ($table->getData($container->real_name) as $item) {
|
|
switch ($container->real_name) {
|
|
case 'indexes':
|
|
$node = new Node_Index($item);
|
|
break;
|
|
case 'columns':
|
|
$node = new Node_Column($item);
|
|
break;
|
|
case 'triggers':
|
|
$node = new Node_Trigger($item);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (isset($node)) {
|
|
$container->addChild($node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Adds containers to a node that is a table
|
|
*
|
|
* References to existing children are returned
|
|
* if this function is called twice on the same node
|
|
*
|
|
* @param Node $table The table node, new containers will be
|
|
* attached to this node
|
|
*
|
|
* @return array An array of new nodes
|
|
*/
|
|
private function addTableContainers($table)
|
|
{
|
|
$retval = array();
|
|
if ($table->hasChildren(true) == 0) {
|
|
if ($table->getPresence('columns')) {
|
|
$retval['columns'] = new Node_Column_Container();
|
|
}
|
|
if ($table->getPresence('indexes')) {
|
|
$retval['indexes'] = new Node_Index_Container();
|
|
}
|
|
if ($table->getPresence('triggers')) {
|
|
$retval['triggers'] = new Node_Trigger_Container();
|
|
}
|
|
// Add all new Nodes to the tree
|
|
foreach ($retval as $node) {
|
|
$node->loaded = true;
|
|
$table->addChild($node);
|
|
}
|
|
} else {
|
|
foreach ($db->children as $node) {
|
|
$retval[$node->real_name] = $node;
|
|
}
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Adds containers to a node that is a database
|
|
*
|
|
* References to existing children are returned
|
|
* if this function is called twice on the same node
|
|
*
|
|
* @param Node $db The database node, new containers will be
|
|
* attached to this node
|
|
*
|
|
* @return array An array of new nodes
|
|
*/
|
|
private function addDbContainers($db)
|
|
{
|
|
$retval = array();
|
|
if ($db->hasChildren(true) == 0) {
|
|
if ($db->getPresence('tables')) {
|
|
$retval['tables'] = new Node_Table_Container();
|
|
}
|
|
if ($db->getPresence('views')) {
|
|
$retval['views'] = new Node_View_Container();
|
|
}
|
|
if ($db->getPresence('functions')) {
|
|
$retval['functions'] = new Node_Function_Container();
|
|
}
|
|
if ($db->getPresence('procedures')) {
|
|
$retval['procedures'] = new Node_Procedure_Container();
|
|
}
|
|
if ($db->getPresence('events')) {
|
|
$retval['events'] = new Node_Event_Container();
|
|
}
|
|
// Add all new Nodes to the tree
|
|
foreach ($retval as $node) {
|
|
$db->addChild($node);
|
|
}
|
|
} else {
|
|
foreach ($db->children as $node) {
|
|
$retval[$node->real_name] = $node;
|
|
}
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Recursively groups tree nodes given a separator
|
|
*
|
|
* @param null|Node $node The node to group or null
|
|
* to group the whole tree. If
|
|
* passed as an argument, $node
|
|
* must be of type CONTAINER
|
|
*
|
|
* @return nothing
|
|
*/
|
|
public function groupTree($node = null)
|
|
{
|
|
if (! isset($node)) {
|
|
$node = $this->tree;
|
|
}
|
|
$this->groupNode($node);
|
|
foreach ($node->children as $child) {
|
|
$this->groupNode($child);
|
|
$this->groupTree($child);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively groups tree nodes given a sperarator
|
|
*
|
|
* @param null|Node $node The node to group
|
|
*
|
|
* @return nothing
|
|
*/
|
|
public function groupNode($node)
|
|
{
|
|
if ($node->type == Node::CONTAINER) {
|
|
$prefixes = array();
|
|
foreach ($node->children as $child) {
|
|
if (strlen($node->separator) && $node->separator_depth > 0) {
|
|
$separator = $node->separator;
|
|
$sep_pos = strpos($child->name, $separator);
|
|
if ($sep_pos != false && $sep_pos != strlen($child->name)) {
|
|
$sep_pos++;
|
|
$prefix = substr($child->name, 0, $sep_pos);
|
|
if (! isset($prefixes[$prefix])) {
|
|
$prefixes[$prefix] = 1;
|
|
} else {
|
|
$prefixes[$prefix]++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
foreach ($prefixes as $key => $value) {
|
|
if ($value == 1) {
|
|
unset($prefixes[$key]);
|
|
}
|
|
}
|
|
if (count($prefixes)) {
|
|
$groups = array();
|
|
foreach ($prefixes as $key => $value) {
|
|
$groups[$key] = new Node($key, Node::CONTAINER, true);
|
|
$groups[$key]->separator = $node->separator;
|
|
$groups[$key]->separator_depth = $node->separator_depth - 1;
|
|
if ($GLOBALS['cfg']['NavigationBarIconic']) {
|
|
$groups[$key]->icon = $this->_commonFunctions->getImage('b_group.png');
|
|
} else {
|
|
$groups[$key]->icon = '';
|
|
}
|
|
$node->addChild($groups[$key]);
|
|
foreach ($node->children as $child) { // FIXME: this could be more efficient
|
|
if (substr($child->name, 0, strlen($key)) == $key && $child->type == Node::OBJECT) {
|
|
$new_child = new Node(substr($child->name, strlen($key)));
|
|
$new_child->real_name = $child->real_name;
|
|
$new_child->icon = $child->icon;
|
|
$new_child->links = $child->links;
|
|
$groups[$key]->addChild($new_child);
|
|
foreach ($child->children as $elm) {
|
|
$new_child->addChild($elm);
|
|
}
|
|
$node->removeChild($child->name);
|
|
}
|
|
}
|
|
}
|
|
foreach ($prefixes as $key => $value) {
|
|
$this->groupNode($groups[$key]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders a state of the tree, used in light mode when
|
|
* either JavaScript and/or Ajax are disabled
|
|
*
|
|
* @return string HTML code for the navigation tree
|
|
*/
|
|
public function renderState()
|
|
{
|
|
$node = $this->buildPath();
|
|
if ($node === false) {
|
|
$retval = false;
|
|
} else {
|
|
$this->groupTree();
|
|
$retval = $this->_commonFunctions->getImage(
|
|
'ajax_clock_small.gif',
|
|
__('Loading'),
|
|
array('style' => 'visibility: hidden;', 'class' => 'throbber')
|
|
);
|
|
$retval .= "<div><ul>\n";
|
|
$children = $this->tree->children;
|
|
usort($children, array('CollapsibleTree', 'sortNode'));
|
|
$this->setVisibility();
|
|
for ($i=0; $i<count($children); $i++) {
|
|
if ($i == 0) {
|
|
$retval .= $this->renderNode($children[0], true, '', 'first');
|
|
} else if ($i + 1 != count($children)) {
|
|
$retval .= $this->renderNode($children[$i], true, '');
|
|
} else {
|
|
$retval .= $this->renderNode($children[$i], true, '', 'last');
|
|
}
|
|
}
|
|
$retval .= "</ul></div>\n";
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Renders a part of the tree, used for Ajax
|
|
* requests in light mode
|
|
*
|
|
* @return string HTML code for the navigation tree
|
|
*/
|
|
public function renderPath()
|
|
{
|
|
$node = $this->buildPath();
|
|
if ($node === false) {
|
|
$retval = false;
|
|
} else {
|
|
$this->groupTree();
|
|
$retval = "<div class='list_container' style='display: none;'>";
|
|
$retval .= "<ul>\n";
|
|
if (($node->real_name == 'tables' || $node->real_name == 'views')
|
|
&& $node->numChildren() >= (int)$GLOBALS['cfg']['LeftDisplayTableFilterMinimum']) {
|
|
// fast filter
|
|
$retval .= $this->fastFilterHtml();
|
|
}
|
|
$children = $node->children;
|
|
usort($children, array('CollapsibleTree', 'sortNode'));
|
|
for ($i=0; $i<count($children); $i++) {
|
|
if ($i + 1 != count($children)) {
|
|
$retval .= $this->renderNode($children[$i], true, '');
|
|
} else {
|
|
$retval .= $this->renderNode($children[$i], true, '', 'last');
|
|
}
|
|
}
|
|
$retval .= "</ul>";
|
|
$retval .= "</div>\n";
|
|
}
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Renders a single node or a branch of the tree
|
|
*
|
|
* @param Node $node The node to render
|
|
* @param int|bool $recursive Bool: Whether to render a single node or a branch
|
|
* Int: How many levels deep to render
|
|
* @param string $indent String used for indentation of output
|
|
* @param string $class An additional class for the list item
|
|
*
|
|
* @return string HTML code for the tree node or branch
|
|
*/
|
|
public function renderNode($node, $recursive = -1, $indent = ' ', $class = '')
|
|
{
|
|
if (!($node->real_name == 'tables' && ! $node->hasSiblings())) {
|
|
if ( $node->type == Node::CONTAINER
|
|
&& count($node->children) == 0
|
|
&& $GLOBALS['is_ajax_request'] != true
|
|
&& $GLOBALS['cfg']['LeftFrameLight'] != true
|
|
) {
|
|
return '';
|
|
}
|
|
$retval = $indent . "<li" . ( $class || $node->classes ? " class='" . trim($class . ' ' . $node->classes) . "'" : '') . ">";
|
|
$hasChildren = $node->hasChildren(false);
|
|
$sterile = array('events', 'triggers', 'functions', 'procedures', 'views', 'columns', 'indexes');
|
|
if (($GLOBALS['is_ajax_request'] || $hasChildren || $GLOBALS['cfg']['LeftFrameLight'])
|
|
&& ! in_array($node->parent->real_name, $sterile) && ! preg_match('/^' . __('New') . '/', $node->real_name)
|
|
) {
|
|
$a_path = array();
|
|
$a_path_clean = array();
|
|
foreach ($node->parents(true, true, false) as $parent) {
|
|
$a_path[] = base64_encode($parent->real_name);
|
|
$a_path_clean[] = $parent->real_name;
|
|
}
|
|
$a_path = implode('.', array_reverse($a_path));
|
|
$a_path_clean = array_reverse($a_path_clean);
|
|
|
|
$v_path = array();
|
|
foreach ($node->parents(true, true, true) as $parent) {
|
|
$v_path[] = base64_encode($parent->name);
|
|
}
|
|
$v_path = implode('.', array_reverse($v_path));
|
|
|
|
$loaded = '';
|
|
if ($node->loaded || $node->is_group || $GLOBALS['cfg']['LeftFrameLight'] != true) {
|
|
$loaded = ' loaded';
|
|
}
|
|
$container = '';
|
|
if ($node->type == Node::CONTAINER) {
|
|
$container = ' container';
|
|
}
|
|
$retval .= "<div class='block'>";
|
|
$retval .= "<i" . ( $class == 'first' ? " class='first'" : '') . "></i>";
|
|
if (strpos($class, 'last') === false) {
|
|
$retval .= "<b></b>";
|
|
}
|
|
|
|
$icon = $this->_commonFunctions->getImage('b_plus.png');
|
|
foreach ($this->a_path as $path) {
|
|
$match = 1;
|
|
foreach ($a_path_clean as $key => $part) {
|
|
if ($part != $path[$key]) {
|
|
$match = 0;
|
|
}
|
|
}
|
|
if ($match) {
|
|
$loaded = ' loaded';
|
|
$icon = $this->_commonFunctions->getImage('b_minus.png');
|
|
break;
|
|
}
|
|
}
|
|
|
|
$retval .= "<a class='expander$loaded$container' target='_self' href='#'>";
|
|
$retval .= "<span class='hide a_path'>" . $a_path . "</span>";
|
|
$retval .= "<span class='hide v_path'>" . $v_path . "</span>";
|
|
|
|
$retval .= $icon;
|
|
|
|
$retval .= "</a>";
|
|
$retval .= "</div>";
|
|
} else {
|
|
$retval .= "<div class='block'>";
|
|
$retval .= "<i" . ( $class == 'first' ? " class='first'" : '') . "></i>";
|
|
$retval .= "</div>";
|
|
}
|
|
|
|
if ($node->type == Node::CONTAINER) {
|
|
$retval .= "<i>";
|
|
}
|
|
if ($GLOBALS['cfg']['NavigationBarIconic']) {
|
|
$retval .= "<div class='block'>";
|
|
if (isset($node->links['icon'])) {
|
|
$args = array();
|
|
foreach ($node->parents(true) as $parent) {
|
|
$args[] = urlencode($parent->real_name);
|
|
}
|
|
$link = vsprintf($node->links['icon'], $args);
|
|
$retval .= "<a href='$link'>{$node->icon}</a>";
|
|
} else {
|
|
$retval .= "<a>{$node->icon}</a>";
|
|
}
|
|
$retval .= "</div>";
|
|
}
|
|
if (isset($node->links['text'])) {
|
|
$args = array();
|
|
foreach ($node->parents(true) as $parent) {
|
|
$args[] = urlencode($parent->real_name);
|
|
}
|
|
$link = vsprintf($node->links['text'], $args);
|
|
if ($node->type == Node::CONTAINER) {
|
|
$retval .= "<a href='$link'>" . htmlspecialchars($node->name) . "</a>";
|
|
} else {
|
|
$retval .= "<a href='$link'>" . htmlspecialchars($node->real_name) . "</a>";
|
|
}
|
|
} else {
|
|
$retval .= "{$node->name}";
|
|
}
|
|
if ($node->type == Node::CONTAINER) {
|
|
$retval .= "</i>";
|
|
}
|
|
$wrap = true;
|
|
} else {
|
|
$node->visible = true;
|
|
$wrap = false;
|
|
}
|
|
|
|
if ($recursive) {
|
|
$hide = '';
|
|
if ($node->visible == false) {
|
|
$hide = " style='display: none;'";
|
|
}
|
|
$children = $node->children;
|
|
usort($children, array('CollapsibleTree', 'sortNode'));
|
|
$buffer = '';
|
|
for ($i=0; $i<count($children); $i++) {
|
|
if ($i + 1 != count($children)) {
|
|
$buffer .= $this->renderNode($children[$i], true, $indent . ' ', $this->classes);
|
|
} else {
|
|
$buffer .= $this->renderNode($children[$i], true, $indent . ' ', $this->classes . ' last');
|
|
}
|
|
}
|
|
if (! empty($buffer)) {
|
|
if ($wrap) {
|
|
$retval .= "\n" . $indent ." <div$hide class='list_container'><ul>\n";
|
|
}
|
|
if (($node->real_name == 'tables' || $node->real_name == 'views')
|
|
&& $node->numChildren() >= (int)$GLOBALS['cfg']['LeftDisplayTableFilterMinimum']
|
|
) {
|
|
$retval .= $this->fastFilterHtml();
|
|
}
|
|
$retval .= $buffer;
|
|
if ($wrap) {
|
|
$retval .= $indent . " </ul></div>\n" . $indent;
|
|
}
|
|
}
|
|
}
|
|
$retval .= "</li>\n";
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Makes some nodes visible based on the which node is active
|
|
*
|
|
* @return nothing
|
|
*/
|
|
private function setVisibility()
|
|
{
|
|
foreach ($this->v_path as $path) {
|
|
$node = $this->tree;
|
|
foreach ($path as $value) {
|
|
$child = $node->getChild($value);
|
|
if ($child !== false) {
|
|
$child->visible = true;
|
|
$node = $child;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates the HTML code for displaying the fast filter for tables
|
|
*
|
|
* @return string LI element used for the fast filter
|
|
*/
|
|
private function fastFilterHtml()
|
|
{
|
|
$retval = "<li class='fast_filter'>";
|
|
$retval .= "<input value='" . __('filter tables by name') . "' />";
|
|
$retval .= "<span title='" . __('Clear Fast Filter') . "'>X</span>";
|
|
$retval .= "</li>";
|
|
return $retval;
|
|
}
|
|
|
|
/**
|
|
* Called by usort() for sorting the nodes in a container
|
|
*
|
|
* @param Node $a The first element used in the comparison
|
|
* @param Node $b The second element used in the comparison
|
|
*
|
|
* @return int See strnatcmp() and strcmp()
|
|
*/
|
|
static public function sortNode($a, $b)
|
|
{
|
|
if ($GLOBALS['cfg']['NaturalOrder']) {
|
|
if (substr($a->classes, 0, 3) === 'new') {
|
|
return -1;
|
|
} else if (substr($b->classes, 0, 3) === 'new') {
|
|
return 1;
|
|
} else {
|
|
return strnatcmp($a->name, $b->name);
|
|
}
|
|
} else {
|
|
if (substr($a->classes, 0, 3) === 'new') {
|
|
return -1;
|
|
} else if (substr($b->classes, 0, 3) === 'new') {
|
|
return 1;
|
|
} else {
|
|
return strcmp($a->name, $b->name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
?>
|