<?php
/******************************************************************************
 *  SiteBar 3 - The Bookmark Server for Personal and Team Use.                *
 *  Copyright (C) 2003-2006  Ondrej Brablc <http://brablc.com/mailto?o>       *
 *                                                                            *
 *  This program is free software; you can redistribute it and/or modify      *
 *  it under the terms of the GNU General Public License as published by      *
 *  the Free Software Foundation; either version 2 of the License, or         *
 *  (at your option) any later version.                                       *
 *                                                                            *
 *  This program is distributed in the hope that it will be useful,           *
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of            *
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             *
 *  GNU General Public License for more details.                              *
 *                                                                            *
 *  You should have received a copy of the GNU General Public License         *
 *  along with this program; if not, write to the Free Software               *
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA *
 ******************************************************************************/

require_once('./inc/database.inc.php');
require_once('./inc/errorhandler.inc.php');
require_once('./inc/usermanager.inc.php');

define ('SB_TREE_UNKNOWN', -1);
define ('SB_TREE_OTHERS',   0);
define ('SB_TREE_OWN',      1);

define ('SB_ALL_LINKS_FOR_ID', null);

function SB_trOrderCmp(&$a, &$b)
{
    if ($a->order == $b->order)
    {
        return strcmp($a->name, $b->name);
    }
    return ($a->order > $b->order) ? 1 : -1;
}

class SB_Tree_Link extends SB_ErrorHandler
{
    var $id;
    var $id_parent;
    var $url;
    var $name = '';

    var $private = false;
    var $comment = '';
    var $favicon = '';
    var $sort_info = null;
    var $added = null;
    var $changed = null;
    var $visited = null;
    var $hits = null;
    var $tested = null;
    var $is_dead = null;
    var $is_feed = null;
    var $is_sidebar = null;
    var $validate = true;
    var $target = '';
    var $type = '';

    var $order = 1000;
    var $type_flag = 'l';

    function SB_Tree_Link($rlink)
    {
        // Map DB fields to class member variables
        static $map = array
        (
            'lid'=>'id',
            'nid'=>'id_parent',
            'url'=>'url',
            'name'=>'name',
            'target'=>'target',
            'private'=>'private',
            'added'=>'added',
            'changed'=>'changed',
            'visited'=>'visited',
            'hits'=>'hits',
            'tested'=>'tested',
            'is_dead'=>'is_dead',
            'is_feed'=>'is_feed',
            'is_sidebar'=>'is_sidebar',
            'comment'=>'comment',
            'favicon'=>'favicon',
            'validate'=>'validate',
            'sort_info'=>'sort_info',
            'order'=>'order',
            'type'=>'type',
        );

        foreach ($rlink as $col => $value)
        {
            if (isset($map[$col]))
            {
                $member = $map[$col];
                $this->$member = $value;
            }
        }
    }

    function getUrl()
    {
        static $reader = null;

        if ($this->is_feed)
        {
            if ($reader===null)
            {
                $um =& SB_UserManager::staticInstance();
                $reader = $um->getParamB64('user','feed_reader_url');
            }

            if ($reader!=='')
            {
                return sprintf($reader,urlencode($this->url));
            }
        }

        return $this->url;
    }
}

class SB_Tree_Node extends SB_ErrorHandler
{
    var $_nodes = array();
    var $_links = array();

    var $id;
    var $id_parent;
    var $name = '';

    var $comment = null;
    var $deleted_by = null;
    var $sort_mode = 'user';
    var $custom_order = null;
    var $type = null;

    var $order = 1;
    var $type_flag = 'n';

    var $parent = null;
    var $level = 0;
    var $myTree = SB_TREE_UNKNOWN;
    var $isRoot = false; // Set when calling loadRoots
    var $acl = null;
    var $aclstr = null;
    var $hasACL = false;

    var $added = null;    // Min from links
    var $changed = null;  // Max from links
    var $visited = null;  // Max from links

    var $db;
    var $um;
    var $tree;

    function SB_Tree_Node($rnode=null)
    {
        $this->db =& SB_Database::staticInstance();
        $this->um =& SB_UserManager::staticInstance();
        $this->tree =& SB_Tree::staticInstance();

        if ($rnode)
        {
            // Map DB fields to class member variables
            static $map = array
            (
                'nid'=>'id',
                'nid_parent'=>'id_parent',
                'name'=>'name',
                'comment'=>'comment',
                'deleted_by'=>'deleted_by',
                'custom_order'=>'custom_order',
                'order'=>'order',
                'sort_mode'=>'sort_mode',
                'type'=>'type',
            );

            foreach ($rnode as $col => $value)
            {
                if (isset($map[$col]))
                {
                    $member = $map[$col];
                    $this->$member = $value;
                }
            }
        }
    }

    function setParent(&$parent)
    {
        $this->parent =& $parent;
        $this->level = $parent->level+1;
        $this->myTree = $parent->myTree;
    }

    function updateDates(&$obj)
    {
        if (!$this->added   || $obj->added  <$this->added)   $this->added   = $obj->added;
        if (!$this->changed || $obj->changed>$this->changed) $this->changed = $obj->changed;
        if (!$this->visited || $obj->visited>$this->visited) $this->visited = $obj->visited;
    }

    function addLink($link)
    {
        $this->updateDates($link);
        $this->_links[] = $link;
    }

    function addNode(&$node)
    {
        $this->updateDates($node);
        $this->_nodes[] = $node;
    }

    function getLinks()
    {
        return $this->_links;
    }

    function getLinksSlice($count)
    {
        return array_slice($this->_links,0,$count);
    }

    function getNodes()
    {
        return $this->_nodes;
    }

    function getChildren()
    {
        $children = array();

        if ($this->um->getParam('user','mix_mode') == 'links')
        {
            foreach ($this->_links as $link)
            {
                $children[] = $link;
            }
        }

        foreach ($this->_nodes as $node)
        {
            $children[] = $node;
        }

        if ($this->um->getParam('user','mix_mode') != 'links')
        {
            foreach ($this->_links as $link)
            {
                $children[] = $link;
            }
        }

        if ($this->sort_mode == 'custom' && strlen($this->custom_order))
        {
            $order = array();
            $pairs = explode(':',$this->custom_order);
            foreach ($pairs as $pair)
            {
                list($id, $orderNo) = explode('~',$pair);
                $order[$id] = $orderNo;
            }

            $count = count($children);
            while ($count)
            {
                $count--;
                $key = $children[$count]->type_flag.$children[$count]->id;
                if (isset($order[$key]))
                {
                    $children[$count]->order = $order[$key];
                }
            }

            usort($children, 'SB_trOrderCmp');
        }

        return $children;
    }

    function nodeCount()
    {
        return count($this->_nodes);
    }

    function linkCount()
    {
        return count($this->_links);
    }

    function childrenCount()
    {
        return $this->linkCount() + $this->nodeCount();
    }

    function isVisible()
    {
        return array_key_exists($this->id, $this->tree->getVisibleNodes());
    }

    function hasACL()
    {
        return array_key_exists($this->id, $this->tree->getACLNodes());
    }

    function hasRight($right='select')
    {
        // Populate $acl
        $this->getACL();
        return ($this->myTree==SB_TREE_OWN) || $this->acl['allow_'.$right];
    }

    function parentHasRight($right='select')
    {
        if ($this->id_parent)
        {
            $parent = $this->tree->getNode($this->id_parent);
            $acl =& $parent->getACL();

            if ($acl && !$acl['allow_'.$right])
            {
                return false;
            }
            else
            {
                return true;
            }
        }

        return false;
    }

    function & getACL()
    {
        // Caching, cannot change between calls
        if ($this->acl !== null)
        {
            return $this->acl;
        }

        static $groups = null;
        $this->acl = array();
        $this->_setit($this->acl, 0);

        if ($this->myTree==SB_TREE_UNKNOWN)
        {
            // Check if it is not our own tree.
            // Yes suboptimal! When called from deep child folder rather
            // then loaded from root, it travels to root several times.
            $root = $this->tree->getRootNode($this->id);
            if ($this->um->uid == $this->tree->getRootOwner($root->id))
            {
                $this->myTree = SB_TREE_OWN;
            }
            else
            {
                $this->myTree = SB_TREE_OTHERS;
            }
        }

        // When we have all rights, do not go further
        if ($this->myTree==SB_TREE_OWN)
        {
            $this->_setit($this->acl, 1);

            // We must continue to see ACL for other groups
            if (!$this->um->getParam('user','show_acl'))
            {
                return $this->acl;
            }
        }

        // Get user groups - valid for the whole execution.
        if ($groups===null)
        {
            $groups = array_keys($this->um->getUserGroups());
        }

        // We have no membership - no right.
        if ($this->myTree==SB_TREE_OTHERS && !count($groups))
        {
            return $this->acl;
        }

        if ($this->hasACL())
        {
            // We have delayed this to be able to decorate own tree.
            if ($this->myTree==SB_TREE_OWN)
            {
                return $this->acl;
            }

            // If group member
            if (count($groups))
            {
                // Black magic, select maximum value out of all groups
                $rset = $this->db->select(
                    array_values(array_map(array($this,'_maxit'), $this->tree->rights)),
                    'sitebar_acl',
                    array('nid'=>$this->id,
                          '^1'=>'AND gid IN ('.implode(',',$groups).')'));
                $this->acl = $this->db->fetchRecord($rset);
            }
        }
        else // We must take parent's ACL - we do not have own
        {
            // If the node has parent but not loaded, load it
            if ($this->id_parent && !$this->parent)
            {
                $parent = $this->tree->getNode($this->id_parent);
                $this->setParent($parent);
            }

            if ($this->parent)
            {
                // Recursive, take parent ACL if it has any.
                // Yes suboptimal! When called from deep child folder rather
                // then loaded from root, it travels to root several times.
                $this->acl = $this->parent->getACL();
                $this->myTree = $this->parent->myTree;
            }
        }

        return $this->acl;
    }

    function getGroupACL($gid)
    {
        $rset = $this->db->select(null, 'sitebar_acl',
            array( 'gid'=> $gid, '^1'=>'AND', 'nid'=>$this->id));
        return $this->db->fetchRecord($rset);
    }

    function getParentACL($gid)
    {
        $acl = null;
        $parent = null;

        if ($this->id_parent)
        {
            $parent = $this->tree->getNode($this->id_parent);
            $acl = $parent->getGroupACL($gid);
        }

        return $acl||!$parent?$acl:$parent->getParentACL($gid);
    }

    function removeACL($gid=null)
    {
        $this->db->purgeCache('acl_nodes');
        $this->db->purgeCache('vis_nodes');

        $this->tree->contentUpdated();

        $where = array('nid'=>$this->id);

        if ($gid!==null)
        {
            $where['^1'] = 'AND';
            $where['gid'] = $gid;
        }

        $rset = $this->db->delete('sitebar_acl', $where);
    }

    function updateACL($gid, $acl)
    {
        $this->removeACL($gid);

        $data = array( 'gid'=> $gid, 'nid'=>$this->id);
        foreach ($acl as $column => $value)
        {
            if (strstr($column, 'allow_'))
            {
                $data[$column] = $value;
            }
        }

        $this->db->insert('sitebar_acl', $data, array(1062));
    }

    function _maxit($right)
    {
        return "max(allow_$right) as allow_$right";
    }

    function _setit(&$rights, $flag, $exception=array())
    {
        foreach ($this->tree->rights as $right)
        {
            if (in_array($right, $exception)) continue;
            $rights['allow_'.$right] = $flag;
        }
    }

    function isPublishedByParent()
    {
        if ($this->id_parent)
        {
            $parent = $this->tree->getNode($this->id_parent);
            $acl = $parent->getGroupACL($this->um->config['gid_everyone']);

            // We have acl, is the folder published?
            if ($acl)
            {
                return $acl['allow_select'];
            }

            // Yep recursive, try to find first parent node with ACL
            return $parent->isPublishedByParent();
        }
        else
        {
            return false;
        }
    }

    function publishFolder($publish)
    {
        $gid = $this->um->config['gid_everyone'];
        $acl = $this->getGroupACL($gid);

        // Remove sharing
        if ($acl && !$publish)
        {
            // Shared directly, the user might be
            // surprised that the folder will be
            // still published via its parent.
            $this->removeACL($gid);
        }
        else if (!$acl && $publish) // Share it
        {
            $acl = array();
            $this->_setit($acl, 0);
            $acl['allow_select'] = 1;
            $this->updateACL($gid, $acl);
        }
    }

}

class SB_Tree extends SB_ErrorHandler
{
    var $db;
    var $um;
    var $rights = array('select','insert','update','delete','purge','grant');

    // Modifies default behavior of loadLinks()
    var $loadLinkFilter = '';
    var $sortMode = null;
    var $userSortMode = null;
    var $sortModeLabel = null;

    // Modifies default behavior of loadNodes()
    var $expandedNodes = null;
    var $maxLevel = -1;
    var $skipPrivate = false;
    var $syncMode = false;
    var $syncColumns = array();

    function SB_Tree()
    {
        $this->db =& SB_Database::staticInstance();
        $this->um =& SB_UserManager::staticInstance();

        $this->userSortMode = $this->um->getParam('user','link_sort_mode');
        $this->sortModeLabel = array
        (
            'user'    => 'User Default',
            'custom'  => 'Custom Order',
            'abc'     => 'Alphabetically',
            'added'   => 'Recently Added',
            'changed' => 'Recently Modified',
            'visited' => 'Recently Visited',
            'hits'    => 'Most Popular',
            'waiting' => 'Waiting for Visit',
        );
    }

    function & staticInstance()
    {
        static $tree;

        if (!$tree)
        {
            $tree = new SB_Tree();
        }

        return $tree;
    }

    function statistics(&$data)
    {
        $rset = $this->db->select('count(*) count', 'sitebar_root');
        $rec = $this->db->fetchRecord($rset);
        $data['roots_total'] = $rec['count'];
        $rset = $this->db->select('count(*) count', 'sitebar_link');
        $rec = $this->db->fetchRecord($rset);
        $data['links_total'] = $rec['count'];
        $rset = $this->db->select('count(*) count', 'sitebar_node');
        $rec = $this->db->fetchRecord($rset);
        $data['nodes_total'] = $rec['count'];
    }

/* Load existing tree */

    function loadRoots($includeHidden=false, $showAllTreesIfAdmin=false)
    {
        $uid = $this->um->uid;

        $order = array();
        foreach (explode(':',$this->um->getParam('user','root_order')) as $pair)
        {
            if ($pair)
            {
                list($id,$rank) = explode('~',$pair);
                $order[$id] = $rank;
            }
        }

        $roots = array();
        $select = 'n.*, n.nid';
        $from = 'sitebar_node n natural join sitebar_root r';
        $where = null;

        if (SB_ALL_LINKS_FOR_ID != $uid)
        {
            $where = array('uid'=>$uid);
        }

        $rset = $this->db->select( $select, $from, $where);

        // Load all own roots (small number)
        while (($root = $this->db->fetchRecord($rset)))
        {
            $root = new SB_Tree_Node($root);
            $root->myTree = SB_TREE_OWN;
            $root->isRoot = true;
            $root->order = isset($order[$root->id])?$order[$root->id]:1;
            $root->hidden = isset($this->um->hiddenFolders[$root->id]);

            if ($includeHidden || !$root->hidden)
            {
                $roots[] = $root;
            }
        }

        if (SB_ALL_LINKS_FOR_ID != $uid)
        {
            $where = array('^1'=>'uid <> '.$uid, '^2'=>'AND', 'deleted_by'=>null);

            // Ignore deleted roots (of other owners)
            $rset = $this->db->select( $select, $from, $where);

            // Check all roots - can be slow with many users
            while (($root=$this->db->fetchRecord($rset)))
            {
                $root = new SB_Tree_Node($root);
                $root->myTree = SB_TREE_OTHERS;
                $root->isRoot = true;
                $root->order = isset($order[$root->id])?$order[$root->id]:100;
                $root->hidden = isset($this->um->hiddenFolders[$root->id]);

                if ( (  ($showAllTreesIfAdmin && $this->um->isAdmin())
                     || ($root->hasRight() || $root->isVisible()) )
                &&   ($includeHidden || !$root->hidden))
                {
                    $roots[] = $root;
                }
            }
        }

        usort($roots, 'SB_trOrderCmp');
        return $roots;
    }

    function loadNodes(&$parent, $loadLinks=true, $right='select', $includeHidden=false)
    {
        // If we are deleted then do not load child nodes
        if ($parent->deleted_by)
        {
            return;
        }

        $rset = $this->db->select( null, 'sitebar_node',
            array('nid_parent'=>$parent->id,
                '^1'=>'AND', 'deleted_by'=>null), 'name'); // COLLATE utf8_general_ci

        while (($rnode = $this->db->fetchRecord($rset)))
        {
            $node = new SB_Tree_Node($rnode);
            if ( $node->deleted_by) continue;
            $node->setParent($parent);

            if (($this->expandedNodes==null || SB_safeVal($this->expandedNodes,$node->id)=='Y')
            &&  ($this->maxLevel == -1 || $parent->level < $this->maxLevel)
            ||  !$node->hasRight($right))
            {
                // Must be twice inside this function: occurence 1
                // - here it limits the depth
                $this->loadNodes($node, $loadLinks, $right, $includeHidden);
            }

            // If we have direct right or visible children
            if (($node->hasRight($right) || $node->childrenCount())
            &&  ($includeHidden || !isset($this->um->hiddenFolders[$node->id])))
            {
                // Must be twice inside this function: occurence 2
                // - here it ensures it is properly stored for frontend
                $node->setParent($parent);
                $parent->addNode($node);
            }
        }

        if ($loadLinks)
        {
            $this->loadLinks($parent);
        }
    }

    function _sortUsingSortInfo($a, $b)
    {
        $as = $a['sort_info'];
        $bs = $b['sort_info'];

        if ($as==='-' && $bs !== '-')
        {
            return -1;
        }
        if ($as!=='-' && $bs === '-')
        {
            return 1;
        }
        return intval($as) - intval($bs);
    }

    function loadLinks(&$parent)
    {
        if (!$parent->hasRight() || $parent->deleted_by)
        {
            return;
        }

        $sortMode = $this->userSortMode;

        if ($parent->sort_mode != 'user')
        {
            $sortMode = $parent->sort_mode;
        }

        // If the sort mode is overridden then take this
        if ($this->sortMode)
        {
            $sortMode = $this->sortMode;
        }

        $where = array('nid'=>$parent->id, '^1'=>'AND', 'deleted_by'=>null);

        if (strlen($this->loadLinkFilter))
        {
            $where['^2'] = 'AND (' . $this->loadLinkFilter . ')';
        }

        if ( $parent->myTree!=SB_TREE_OWN || $this->skipPrivate)
        {
            $where['^3'] = 'AND';
            $where['private'] = '0';
        }

        $select  = null;
        $from    = 'sitebar_link';
        $orderBy = 'name'; // COLLATE utf8_general_ci
        $isdate = false;
        $datefmt = '%Y-%m-%d';
        $timefmt = '%H:%i:%s';
        $selectfmt  = "*, DATE_FORMAT(%s,'%s') date_info, DATE_FORMAT(%s,'%s') time_info";

        switch ($sortMode)
        {
            case 'added':
                $isdate = true;
                $select = sprintf($selectfmt, 'added', $datefmt, 'added', $timefmt);
                $orderBy = 'added DESC, name ASC';
                break;

            case 'changed':
                $isdate = true;
                $select = sprintf($selectfmt, 'changed', $datefmt, 'changed', $timefmt);
                $orderBy = 'changed DESC, name ASC';
                break;

            case 'visited':
                $isdate = true;
                $select = sprintf($selectfmt, 'visited', $datefmt, 'visited', $timefmt);
                $orderBy = 'visited DESC, name ASC';
                break;

            case 'hits':
                $select  = '*, hits sort_info';
                $orderBy = 'hits DESC, name ASC';
                break;

            case 'waiting':
                break;
        }

        $today = date('Y-m-d');

        $rset = $this->db->select( $select, $from, $where, $orderBy);
        $records = $this->db->fetchRecords($rset);

        // Man is this complicated, we need to check when it was
        // used last time by this user ...
        if ($sortMode == 'waiting')
        {
            $sortInfoMap = array();

            $localWhere = $where;
            $localWhere['^4'] = 'AND l.lid=v.lid AND uid='.$this->um->uid;

            // ... so we read only user's visits ...
            $rset = $this->db->select
            (
                'l.lid, TO_DAYS(v.visited) - TO_DAYS(now()) sort_info',
                'sitebar_link l, sitebar_visit v',
                $localWhere
            );

            // ... and store this information (number of days from last visit) ...
            while (($rec=$this->db->fetchRecord($rset)))
            {
                $sortInfoMap[$rec['lid']] = $rec['sort_info'];
            }

            // ... now walk through the links and add the sort_info ...
            $newRecords = array();
            foreach ($records as $rlink)
            {
                $lid = $rlink['lid'];
                $rlink['sort_info'] = isset($sortInfoMap[$lid])?$sortInfoMap[$lid]:'-';
                $newRecords[] = $rlink;
            }
            $records = $newRecords;

            // ... and now sort using the sort info
            usort($records, array($this, "_sortUsingSortInfo"));
        }

        foreach ($records as $rlink)
        {
            if ($isdate)
            {
                if ($rlink['date_info'] == $today)
                {
                    $rlink['sort_info'] = $rlink['time_info'];
                }
                else
                {
                    if ($rlink['date_info']!='0000-00-00')
                    {
                        $rlink['sort_info'] = $rlink['date_info'];
                    }
                }
            }
            else
            {
                if (isset($rlink['sort_info']) && $rlink['sort_info']==='0')
                {
                    $rlink['sort_info'] = '';
                }
            }
            $parent->addLink(new SB_Tree_Link($rlink));
        }

        unset($records);
    }

    function importTree($nid_parent, $node, $renameDuplicate=false, $linkCallBack=null, $nodeCallBack=null)
    {
        $order = array();

        foreach ($node->getLinks() as $link)
        {
            if ($linkCallBack)
            {
                $link = $linkCallBack($link);
            }

            $lid = $this->addLink($nid_parent, $link, $renameDuplicate);

            $order[] = 'l'.$lid.'~'.intval($link->order);
        }

        foreach ($node->getNodes() as $childnode)
        {
            if ($nodeCallBack)
            {
                $childnode = $nodeCallBack($childnode);
            }

            $namedNode = $this->getNodeByName($nid_parent, $childnode->name);

            $nid = null;

            if (!$namedNode) // If we do not have the folder - create it!
            {
                $nid = $this->addNode($nid_parent, $childnode->name, $childnode->comment, $childnode->sort_mode);
            }
            else
            {
                $nid = $namedNode->id;

                // The folder exists, but has been deleted before
                if ($namedNode->deleted_by!="")
                {
                    // Recovery of deleted notes should not see any error messages
                    $this->ignoreWarnings(true);

                    // We want to use this again
                    $this->undeleteNode($nid);
                    // Delete its content so that deleted content it does not get visible
                    $this->removeNode($nid, true);

                    $this->ignoreWarnings(false);
                }
            }

            $order[] = 'n'.$nid.'~'.intval($childnode->order);

            $this->importTree($nid, $childnode, $renameDuplicate, $linkCallBack, $nodeCallBack);
        }

        $node = $this->getNode($nid_parent);

        // If we have custom order save it
        if ($node->sort_mode=='custom')
        {
            $columns = array
            (
                'custom_order' => implode(':',$order),
                'sort_mode' => 'custom',
            );

            $this->updateNode($nid_parent, $columns);
        }
    }

    function & getACLNodes()
    {
        static $aclNodes = null;

        if ($aclNodes !== null)
        {
            return $aclNodes;
        }

        $aclNodes = array();

        // Read cached value
        $cached = $this->db->getCache('acl_nodes',$this->um->uid);

        if (is_array($cached))
        {
            if (strlen($cached['cvalue'])>0)
            {
                $nodes = explode(',',$cached['cvalue']);
                foreach ($nodes as $nid)
                {
                    $aclNodes[$nid] = true;
                }
            }
            return $aclNodes;
        }

        static $gids = null;
        if ($gids === null)
        {
            $gids = array_keys($this->um->getUserGroups());
        }

        if (!count($gids))
        {
            return $aclNodes;
        }

        $rset = $this->db->select('distinct nid', 'sitebar_acl',
                    array( '^1'=> 'gid in ('.implode(',',$gids).')'));

        $aclNodes = array();
        while (($rec=$this->db->fetchRecord($rset)))
        {
            $nid = $rec['nid'];
            $aclNodes[$nid] = true;
        }

        $this->db->setCache('acl_nodes',$this->um->uid, implode(',',array_keys($aclNodes)));
        return $aclNodes;
    }

    function & getVisibleNodes()
    {
        static $visibleNodes = null;

        if ($visibleNodes !== null)
        {
            return $visibleNodes;
        }

        $aclNodes = $this->getACLNodes();
        $visibleNodes = $aclNodes;

        // Read cached value
        $cached = $this->db->getCache('vis_nodes',$this->um->uid);

        if (is_array($cached))
        {
            if (strlen($cached['cvalue'])>0)
            {
                $nodes = explode(',',$cached['cvalue']);
                foreach ($nodes as $nid)
                {
                    $visibleNodes[$nid] = true;
                }
            }
            return $visibleNodes;
        }

        foreach ($aclNodes as $nid => $dummy)
        {
            $parents = $this->getParentNodes($nid);

            if ($parents===null)
            {
                $this->fatal('Node number %s has ACL record but does not exist!', array($nid));
            }

            foreach ($parents as $nid)
            {
                $visibleNodes[$nid] = true;
            }
        }

        $this->db->setCache('vis_nodes',$this->um->uid, implode(',',array_keys($visibleNodes)));
        return $visibleNodes;
    }

    function getNode($nid)
    {
        $rset = $this->db->select( null, 'sitebar_node', array('nid'=>$nid));
        $rnode = $this->db->fetchRecord($rset);

        if (!$rnode)
        {
            $this->error('Folder with id %s does not exist!', array($nid));
            return null;
        }

        return new SB_Tree_Node($rnode);
    }

    function getNodeByName($nid_parent, $name)
    {
        $rset = $this->db->select( null, 'sitebar_node',
            array('nid_parent'=>$nid_parent, '^1'=>'AND', 'name'=>$name));
        $rnode = $this->db->fetchRecord($rset);

        if (!$rnode)
        {
            return null;
        }

        return new SB_Tree_Node($rnode);
    }

    function getRootOwner($nid)
    {
        static $owners = array();

        if (isset($owners[$nid]))
        {
            return $owners[$nid];
        }

        $rset = $this->db->select( null, 'sitebar_root', array('nid'=>$nid));
        $rtree = $this->db->fetchRecord($rset);

        if (!$rtree)
        {
            $this->error('Tree has already been deleted!');
            return null;
        }

        $owners[$nid] = $rtree['uid'];

        return $owners[$nid];
    }

    function getLinkCount($uid, $nid=null)
    {
        if ($nid===null)
        {
            $count = 0;

            $rset = $this->db->select( 'nid', 'sitebar_root', array('uid'=>$uid));
            while (($root = $this->db->fetchRecord($rset)))
            {
                $count += $this->getLinkCount($uid, $root['nid']);
            }
            return $count;
        }

        $rset = $this->db->select( 'count(*) lnkcnt', 'sitebar_link', array
        (
            'nid'=>$nid,
            '^1'=>'AND',
            'deleted_by'=>null
        ));
        $rec = $this->db->fetchRecord($rset);
        $count = $rec['lnkcnt'];

        $rset = $this->db->select( 'nid', 'sitebar_node', array
        (
            'nid_parent'=>$nid,
            '^1'=>'AND',
            'deleted_by'=>null
        ));

        while (($rec = $this->db->fetchRecord($rset)))
        {
            $count += $this->getLinkCount($uid, $rec['nid']);
        }

        return $count;
    }

    function deleteUsersTrees($uid)
    {
        $rset = $this->db->select( null, 'sitebar_root', array('uid'=>$uid));
        $roots = array();

        while (($rtree = $this->db->fetchRecord($rset)))
        {
            $nid = $rtree['nid'];
            $this->removeNode($nid, false);
            $this->purgeNode($nid);
        }

        $this->db->delete( 'sitebar_root', array('uid'=>$uid));
    }

    function getUserRoots($uid)
    {
        $rset = $this->db->select( null, 'sitebar_root', array('uid'=>$uid));
        $roots = array();

        while (($rtree = $this->db->fetchRecord($rset)))
        {
            $roots[] = $rtree['nid'];
        }

        return $roots;
    }

    function changeOwner($olduid, $newuid, $email=null)
    {
        $roots = $this->getUserRoots($olduid);
        foreach ($roots as $nid)
        {
            if ($email)
            {
                $node = $this->getNode($nid);

                // Prevent duplicates
                $newname = SB_T("Deleted root %s of %s at %s",
                    array($node->name, $email, date("Y-m-d H:i:s")));

                $this->updateNode($nid, array('name'=>$newname));
            }

            if (!$this->updateNodeOwner($nid, $newuid))
            {
                return false;
            }
        }

        return true;
    }

    function getRootNode($nid)
    {
        $node = $this->getNode($nid);
        $stack = array();

        while ($node->id_parent)
        {
            $child = $node;
            $node = $this->getNode($node->id_parent);
            $node->child = $child;
        }

        return $node;
    }

    function getParentNodes($nid)
    {
        $parents = array();

        $node = $this->getNode($nid);

        if (!$node)
        {
            return null;
        }

        while ($node && $node->id_parent)
        {
            $parents[] = $node->id_parent;
            $node = $this->getNode($node->id_parent);
        }

        return $parents;
    }

    function getOwner($nid)
    {
        $node = $this->getRootNode($nid);

        if (!$node)
        {
            return;
        }

        $rset = $this->db->select('uid', 'sitebar_root',
            array( 'nid' => $node->id));

        $rec = $this->db->fetchRecord($rset);

        if (!$rec)
        {
            $this->error('Tree has already been deleted!');
            return false;
        }

        // Always greater then zero
        return $rec['uid'];
    }

    function inMyTree($nid)
    {
        $root = $this->getRootNode($nid);
        $uid = $this->getRootOwner($root->id);
        return $uid == $this->um->uid;
    }

    function renameDeletedNode($nid_parent, $name)
    {
        $this->db->update( 'sitebar_node',
            array('name' => '_'.$name),
            array('nid_parent' => $nid_parent,
                  '^1'=>'AND deleted_by IS NOT NULL AND',
                  'name'=>$name));

        return ($this->db->getAffectedRows()>=1);
    }

    function addNode($nid_parent, $name, $comment=null, $sortMode='user')
    {
        $this->contentUpdated();

        $rset = $this->db->insert( 'sitebar_node',
            array( 'nid_parent' => $nid_parent,
                   'name'       => $name,
                   'comment'    => $comment,
                   'sort_mode'  => $sortMode,
            ),
            array(1062));

        // If we have duplicate
        if ($this->db->getErrorCode()==1062)
        {
            // Rename deleted folder to prevent collision
            if ($this->renameDeletedNode($nid_parent, $name))
            {
                return $this->addNode($nid_parent, $name, $comment);
            }
            else
            {
                $this->error('Duplicate folder name "%s"!', array($name));
                return 0;
            }
        }

        return $this->db->getLastId();
    }

    function addRoot($uid, $name, $comment=null)
    {
        $uniqName = $name;

        // Check wheter this name is not used for any other root
        for ($i=1; ;$i++)
        {
            $rset = $this->db->select( null, 'sitebar_node',
                array('name'=>$uniqName, '^1'=>'AND', 'nid_parent'=>0));
            $rnode = $this->db->fetchRecord($rset);

            // If not exists then we can use it
            if (!$rnode)
            {
                break;
            }

            $uniqName = $name . ' ' . $i;
        }

        $this->addNode(0, $uniqName, $comment);

        $rset = $this->db->insert( 'sitebar_root',
            array( 'uid' => $uid,
                   'nid' => $this->db->getLastId()));

        return $rset;
    }

    function removeNode($nid, $contentOnly)
    {
        $node = $this->getNode($nid);
        $where = array();
        $affected = 0;

        // If root node then content must be explicitly deleted
        if ($contentOnly || !$node->id_parent)
        {
            $this->db->update( 'sitebar_link',
                array( 'deleted_by'=>$this->um->uid,
                       'changed'=> array('now'=>'')),
                array( 'nid'=>$nid, '^1'=> 'AND deleted_by IS NULL'));

            $affected += $this->db->getAffectedRows();
        }

        if ($contentOnly)
        {
            $where['nid_parent'] = $nid;
        }
        else
        {
            $where['nid'] = $nid;
        }

        $where['^1'] = 'AND deleted_by IS NULL';

        $rset = $this->db->update( 'sitebar_node',
            array('deleted_by'=>$this->um->uid), $where);

        $affected += $this->db->getAffectedRows();

        if ($affected==0)
        {
            if ($contentOnly)
            {
                $this->warn('There is no content to be deleted!');
            }
            else
            {
                if (!$node->id_parent)
                {
                    $this->warn('Purge folder to remove it permanently!');
                }
                else
                {
                    $this->warn('Folder has already been deleted!');
                }
            }
        }

        return $rset;
    }

    function purgeNode($nid, $root_deleted_by=null)
    {
        $this->db->purgeCache('acl_nodes');
        $this->db->purgeCache('vis_nodes');

        $node = $this->getNode($nid);

        $onlydeleted = '';

        // If the folder is not deleted then purge only deleted links/folders
        if (!$root_deleted_by && !$node->deleted_by)
        {
            $onlydeleted = 'AND deleted_by IS NOT NULL';
        }

        $this->db->delete( 'sitebar_link',
            array('nid'=>$nid, '^1'=>$onlydeleted));

        // Select all deleted folders and purge them as well
        $rset = $this->db->select( 'nid, name', 'sitebar_node',
            array('nid_parent'=>$nid, '^1'=>$onlydeleted));

        foreach ($this->db->fetchRecords($rset) as $rnode)
        {
            $this->purgeNode($rnode['nid'],
                $root_deleted_by||$node->deleted_by);
        }

        // If we currently have deleted folder, them delete ACL and itself
        if ($root_deleted_by || $node->deleted_by)
        {
            $this->db->delete( 'sitebar_acl', array( 'nid' => $nid ));
            $this->db->delete( 'sitebar_node', array( 'nid' => $nid ));

            if ($node->id_parent==0)
            {
                $this->db->delete( 'sitebar_root', array( 'nid' => $nid ));
            }
        }
    }

    function undeleteNode($nid)
    {
        $node = $this->getNode($nid);
        $affected = 0;

        $this->db->update( 'sitebar_link',
            array( 'deleted_by'=>null,
                   'changed'=> array('now'=>'')),
            array( 'nid'=>$nid));
        $affected += $this->db->getAffectedRows();

        // Undelete child folders
        $rset = $this->db->update( 'sitebar_node',
        array('deleted_by'=>null), array('nid_parent'=>$nid));
        $affected += $this->db->getAffectedRows();

        // Undelete current node - can happen to root only
        $rset = $this->db->update( 'sitebar_node',
        array('deleted_by'=>null), array('nid'=>$nid));
        $affected += $this->db->getAffectedRows();

        if ($affected==0)
        {
            $this->warn('There is nothing to be undeleted!');
        }
        return $rset;
    }

    function updateNode($nid, $columns)
    {
        $this->contentUpdated();

        $rset = $this->db->update( 'sitebar_node', $columns, array( 'nid'  => $nid), array(1062));

        if ($this->db->getErrorCode()==1062)
        {
            $node = $this->getNode($nid);

            if ($this->renameDeletedNode($node->id_parent, $columns['name']))
            {
                return $this->updateNode($nid, $columns);
            }
            else
            {
                $this->error('Duplicate folder name "%s"!', array($columns['name']));
                return 0;
            }
        }

        return $rset;
    }

    function updateNodeOwner($nid, $uid)
    {
        $rset = $this->db->update( 'sitebar_root',
            array( 'uid' => $uid),
            array( 'nid'  => $nid));

        return $rset;
    }

    function moveNode( $nid, $nid_parent, $contentOnly=false)
    {
        if ($contentOnly)
        {
            $node = $this->getNode($nid);

            // Load source node to memory
            $this->loadNodes($node);

            foreach ($node->getNodes() as $childnode)
            {
                $this->moveNode($childnode->id, $nid_parent);
                if ($this->hasErrors())
                {
                    return 0;
                }
            }

            foreach ($node->getLinks() as $link)
            {
                $this->moveLink($link->id, $nid_parent);
                if ($this->hasErrors())
                {
                    return 0;
                }
            }
            return 1;
        }

        $node = $this->getNode($nid);

        if ($nid_parent == $node->id_parent)
        {
            $this->error('Cannot move to the same folder!');
            return 0;
        }

        if ($nid == $nid_parent)
        {
            $this->error('This operation would lead to broken database.');
            $this->error('Please note your last steps and contanct SiteBar development!');
            return 0;
        }

        // Just switch parent name
        $rset = $this->db->update( 'sitebar_node',
            array( 'nid_parent' => $nid_parent),
            array( 'nid'  => $nid),
            array(1062));

        if ($this->db->getErrorCode()==1062)
        {
            if ($this->renameDeletedNode($nid_parent, $node->name))
            {
                return $this->moveNode($nid, $nid_parent);
            }
            else
            {
                $this->error('Duplicate folder name "%s"!', array($node->name));
                return 0;
            }
        }
        elseif ($this->db->getAffectedRows()==0)
        {
            $this->error('Folder has already been deleted!');
        }

        // If root node
        if (!$this->hasErrors() && !$node->id_parent)
        {
            $this->db->delete( 'sitebar_root', array('nid' => $nid));
        }

        return $rset;
    }

    function copyNode( $nid, $nid_parent, $contentOnly=false)
    {
        $node = $this->getNode($nid);
        $parent = $this->getNode($nid_parent);
        $targetId = $nid_parent;

        // Load source node to memory
        $this->loadNodes($node);

        if (!$contentOnly)
        {
            // Create new parent folder with the same name as source
            $targetId = $this->addNode($parent->id, $node->name, $node->comment);
        }

        if (!$this->hasErrors())
        {
            // Import loaded tree to new parent
            $this->importTree($targetId, $node);
        }
    }

/* Manage tree operations with links */

    function getLink($lid)
    {
        $rset = $this->db->select( null, 'sitebar_link', array('lid'=>$lid));
        $rlink = $this->db->fetchRecord($rset);

        if (!$rlink)
        {
            $this->error('Link has already been deleted!');
            return null;
        }

        return new SB_Tree_Link($rlink);
    }


    function purgeDeletedLink($nid, $name)
    {
        $this->db->delete( 'sitebar_link',
            array('nid' => $nid,
                  '^1'=>'AND deleted_by IS NOT NULL AND ',
                  'name'=>$name,
                 ));

        return ($this->db->getAffectedRows()>=1);
    }

    function addLink($nid, $columns, $renameDuplicate=false)
    {
        if (is_object($columns))
        {
            $link = $columns;
            $columns = array
            (
                'name'=>$link->name?$link->name:'',
                'url'=>$link->url,
                'favicon'=>$link->favicon,
                'private'=>$link->private?1:0,
                'comment'=>$link->comment,
                'validate'=>$link->validate?1:0,
                'hits'=>$link->hits?$link->hits:0,
                'added'=>$link->added?$link->added:array('now' => ''),
                'is_feed'=>$link->is_feed?1:0,
                'is_sidebar'=>$link->is_sidebar?1:0,
            );

            if ($link->changed)
            {
                $columns['changed'] = $link->changed;
            }
            if ($link->tested)
            {
                $columns['tested'] = $link->tested;
            }
            if ($link->visited)
            {
                $columns['visited'] = $link->visited;
            }

        }
        else
        {
            $columns['added'] = array('now' => '');
        }

        $columns['nid'] = $nid;

        $this->contentUpdated();
        $rset = $this->db->insert( 'sitebar_link', $columns, array(1062));

        // Cannot insert because of an index
        if ($this->db->getErrorCode()==1062)
        {
            if ($this->syncMode)
            {
                // Select old link
                $rset = $this->db->select( null, 'sitebar_link',
                    array('nid'=>$nid,'^1'=>'AND','name'=>$columns['name']));
                $rlink = $this->db->fetchRecord($rset);

                $hasChanged = false;

                $selectedColumns = array();
                foreach ($this->syncColumns as $col)
                {
                    if ($rlink[$col] != $columns[$col])
                    {
                        $selectedColumns[$col] = $columns[$col];
                        $hasChanged = true;
                    }
                }

                if ($rlink['deleted_by']!="")
                {
                    $hasChanged = true;
                    $selectedColumns['deleted_by'] = null;
                }

                if ($hasChanged)
                {
                    $this->updateLink($rlink['lid'], $selectedColumns);
                }

                return $rlink['lid'];
            }


            if ($this->purgeDeletedLink($nid,$columns['name']))
            {
                return $this->addLink($nid, $columns);
            }
            elseif ($renameDuplicate)
            {
                $add = 1;

                if (preg_match("/^(.*) #(\d+)$/",$columns['name'],$reg))
                {
                    $add = intval($reg[2])+1;
                    $columns['name'] = $reg[1];
                }

                $columns['name'] = $columns['name']. ' #'. $add;

                return $this->addLink($nid, $columns, $renameDuplicate);
            }
            else // If we are here, then the item was not deleted and we signal error
            {
                $this->warn('Duplicate name "%s"!', array($columns['name']));
                return 0;
            }
        }

        return $this->db->getLastId();
    }

    function updateLink($lid, $columns, $changed=true)
    {
        $update = $columns;

        if ($changed)
        {
            $update['changed'] = array('now' => '');
            $this->contentUpdated();
        }

        $rset = $this->db->update( 'sitebar_link', $update, array( 'lid'  => $lid), array(1062));

        if ($this->db->getErrorCode()==1062)
        {
            $link = $this->getLink($lid);
            if ($this->purgeDeletedLink($link->id_parent,$columns['name']))
            {
                $this->updateLink($lid, $update);
            }
            else
            {
                $this->warn('Duplicate name "%s"!', array($columns['name']));
                return 0;
            }
        }
        elseif ($this->db->getAffectedRows()==0)
        {
            $this->error('Link has already been deleted!');
        }

        return $rset;
    }

    function contentUpdated()
    {
        static $done = 0;

        // We will update it only once per execution
        if (!$done)
        {
            $done++;
            $this->db->update( 'sitebar_config', array('changed'=>array('now' => '')));
        }
    }

    function countVisit($link)
    {
        $this->db->update( 'sitebar_link',
            array( 'hits'=> array('hits+'=>'1'), 'visited'=>array('now'=>null)),
            array( 'lid'  => $link->id));

        $this->db->update( 'sitebar_visit',
            array( 'visited'=> array('now'=>null)),
            array( 'lid'  => $link->id,
                   '^1'   => 'AND',
                   'uid'  => $this->um->uid));

        if (!$this->db->getAffectedRows())
        {
            // Ignore duplicates
            $this->db->insert( 'sitebar_visit',
                array( 'lid'  => $link->id,
                       'uid'  => $this->um->uid,
                       'visited' => array('now'=>null)),
                array(1062));
        }
    }

    function moveLink($lid, $nid)
    {
        $rset = $this->db->update( 'sitebar_link',
            array( 'nid'=> $nid),
            array( 'lid'  => $lid),
            array(1062));

        if ($this->db->getErrorCode()==1062)
        {
            $link = $this->getLink($lid);
            if ($this->purgeDeletedLink($nid,$link->name))
            {
                $this->moveLink($lid,$nid);
            }
            else
            {
                $this->warn('Duplicate name "%s" in the target folder!', array($link->name));
                return 0;
            }
        }
        elseif ($this->db->getAffectedRows()==0)
        {
            $this->error('Link has already been deleted!');
        }

        return $rset;
    }

    function copyLink($lid, $nid)
    {
        $link = $this->getLink($lid);

        if (!$link)
        {
            return;
        }

        return $this->addLink($nid, $link, true);
    }

    function removeLink($lid)
    {
        $rset = $this->db->update( 'sitebar_link',
            array( 'deleted_by'=>$this->um->uid,
                   'changed'=> array('now'=>'')),
            array( 'lid'=>$lid));

        if ($this->db->getAffectedRows()==0)
        {
            $this->error('Link has already been deleted!');
        }

        return $rset;
    }

    function purgeLink($lid)
    {
        $rset = $this->db->delete( 'sitebar_link', array( 'lid'=>$lid));
        if ($this->db->getAffectedRows()==0)
        {
            $this->error('Link has already been deleted!');
        }
        return $rset;
    }
}

?>
