<?php
/******************************************************************************
 *  SiteBar 3 - The Bookmark Server for Personal and Team Use.                *
 *  Copyright (C) 2003,2004  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');

define ('TREE_UNKNOWN', -1);
define ('TREE_OTHERS',   0);
define ('TREE_OWN',      1);

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

class Tree_Link extends ErrorHandler
{
    var $id;
    var $id_parent;
    var $url;
    var $name = '';

    var $private = false;
    var $comment = '';
    var $favicon = '';
    var $sort_info = null;
    var $tested = null;
    var $is_dead = null;
    var $validate = true;

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

    function Tree_Link($rlink)
    {
        // Map DB fields to class member variables
        static $map = array
        (
            'lid'=>'id',
            'nid'=>'id_parent',
            'url'=>'url',
            'name'=>'name',
            'private'=>'private',
            'tested'=>'tested',
            'is_dead'=>'is_dead',
            'comment'=>'comment',
            'favicon'=>'favicon',
            'validate'=>'validate',
            'sort_info'=>'sort_info',
            'order'=>'order',
        );

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

class Tree_Node extends ErrorHandler
{
    var $_nodes = array();
    var $_links = array();

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

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

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

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

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

    function Tree_Node($rnode=null)
    {
        $this->db =& Database::staticInstance();
        $this->um =& UserManager::staticInstance();
        $this->tree =& 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',
            );

            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 addLink($link)
    {
        $this->_links[] = $link;
    }

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

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

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

    function getChildren()
    {
        $children = array();
        $mix_mode = $this->um->getParam('user','mix_mode');

        if ($mix_mode == "links")
        {
            foreach ($this->_links as $link)
            {
                $children[] = $link;
            }
        }

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

        if ($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, '_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 in_array($this->id,$this->tree->getACLNodes());
    }

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

    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==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 = TREE_OWN;
            }
            else
            {
                $this->myTree = TREE_OTHERS;
            }
        }

        // When we have all right do not go further
        if ($this->myTree==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==TREE_OTHERS && !count($groups))
        {
            return $this->acl;
        }

        // Check if this node has ACL for any groups
        $rset = $this->db->select('count(*) count','sitebar_acl',array('nid'=>$this->id));
        $countrec = $this->db->fetchRecord($rset);

        // We have ACL for this node and our group, update it
        if ($countrec['count'])
        {
            $this->hasACL = true;

            // We have delayed this to be able to decorate own tree.
            if ($this->myTree==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();
            }
        }

        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)
    {
        $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 Tree extends ErrorHandler
{
    var $db;
    var $um;
    var $aclNodes = null;
    var $rights = array('select','insert','update','delete','purge','grant');

    // Modifies default behavior of loadLinks()
    var $loadLinkFilter = '';

    function Tree()
    {
        $this->db =& Database::staticInstance();
        $this->um =& UserManager::staticInstance();
    }

    function & staticInstance()
    {
        static $tree;

        if (!$tree)
        {
            $tree = new 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.*';
        $from = 'sitebar_root t natural join sitebar_node n';

        $rset = $this->db->select( $select, $from, array('uid'=>$uid));

        // Load all own roots (small number)
        foreach ($this->db->fetchRecords($rset) as $root)
        {
            $root = new Tree_Node($root);
            $root->myTree = 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;
            }
        }

        $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
        foreach ($this->db->fetchRecords($rset) as $root)
        {
            $root = new Tree_Node($root);
            $root->myTree = 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, '_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');

        foreach ($this->db->fetchRecords($rset) as $rnode)
        {
            $node = new Tree_Node($rnode);
            $node->setParent($parent);
            $this->loadNodes($node, $loadLinks, $right);

            // If we have direct right or visible children
            if ( !$node->deleted_by
            &&   ($node->hasRight($right) || $node->childrenCount())
            &&   ($includeHidden || !isset($this->um->hiddenFolders[$node->id])))
            {
                $parent->addNode($node);
            }
        }

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

    function loadLinks(&$parent, $sortMode="abc")
    {
        if (!$parent->hasRight() || $parent->deleted_by)
        {
            return;
        }

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

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

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

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

        $select  = null;
        $from    = 'sitebar_link';
        $orderBy = 'name';

        // We have to do it twice, because we miss subselect here
        if ($sortMode=='visit')
        {
            $select = 'distinct l.*, TO_DAYS(visit) - TO_DAYS(now()) sort_info';
            $from = 'sitebar_link l left join sitebar_visit v using (lid)';
            $where['^4'] = 'AND (uid IS NULL OR NOT';
            $where['uid'] = $this->um->uid;
            $where['^5'] = ')';
            $orderBy = 'sort_info, name';

            $rset = $this->db->select( $select, $from, $where, $orderBy);
            foreach ($this->db->fetchRecords($rset) as $rlink)
            {
                $parent->addLink(new Tree_Link($rlink));
            }
        }

        switch ($sortMode)
        {
            case 'changed':
                $select  = '*, DATE_FORMAT(changed,\'%Y-%m-%d\') sort_info';
                $orderBy = 'changed DESC, name ASC';
                break;

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

            case 'visit'  :
                $where['^4'] = 'AND';
                $where['^5'] = '';
                break;
        }

        $rset = $this->db->select( $select, $from, $where, $orderBy);
        foreach ($this->db->fetchRecords($rset) as $rlink)
        {
            $parent->addLink(new Tree_Link($rlink));
        }
    }

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

        foreach ($node->getLinks() as $link)
        {
            $lid = $this->addLink($nid_parent, $link, $renameDuplicate);

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

        foreach ($node->getNodes() as $childnode)
        {
            $nid = $this->getNodeIDByName($nid_parent, $childnode->name);

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

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

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

        $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()
    {
        if ($this->aclNodes !== null)
        {
            return $this->aclNodes;
        }

        static $gids = null;

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

        if (!count($gids))
        {
            $this->aclNodes = array();
            return $this->aclNodes;
        }

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

        $nodes = array();

        foreach ($this->db->fetchRecords($rset) as $rec)
        {
            $nid = $rec['nid'];
            $nodes[] = $nid;
            $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)
            {
                $nodes[] = $nid;
            }
        }

        $this->aclNodes = array_unique($nodes);
        return $this->aclNodes;
    }

    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 Tree_Node($rnode);
    }

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

        return $rnode?$rnode['nid']:0;
    }

    function getRootOwner($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;
        }

        return $rtree['uid'];
    }

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

        foreach ($this->db->fetchRecords($rset) as $rtree)
        {
            $roots[] = $rtree['nid'];
        }

        return $roots;
    }

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

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

            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")
    {
        $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)
    {
        $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)
    {
        $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);

        // 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 Tree_Link($rlink);
    }


    function purgeDeletedLink($nid, $name, $url)
    {
        $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,
                'url'=>$link->url,
                'favicon'=>$link->favicon,
                'private'=>$link->private?1:0,
                'comment'=>$link->comment,
                'validate'=>$link->validate?1:0,
            );
        }

        $columns['nid'] = $nid;
        $columns['changed'] = array('now' => '');

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

        // Cannot insert because of an index
        if ($this->db->getErrorCode()==1062)
        {
            if ($this->purgeDeletedLink($nid,$columns['name'],$columns['url']))
            {
                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' => '');
        }

        $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'],$columns['url']))
            {
                $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 countVisit($link)
    {
        $this->db->update( 'sitebar_link',
            array( 'hits'=> array('hits+'=>'1')),
            array( 'lid'  => $link->id));

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

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

    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,$link->url))
            {
                $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;
    }
}

?>
