<?php

require_once HORDE_BASE . '/lib/Category.php';
require_once HORDE_BASE . '/lib/Perms.php';

/**
 * Horde_Share:: This class provides an interface to all shares a user
 * might have. Its methods take care of any site-specific restrictions
 * configured in prefs.php and conf.php.
 *
 * $Horde: horde/lib/Share.php,v 1.46 2003/08/18 02:00:15 jwm Exp $
 *
 * Copyright 2002-2003 Joel Vandal <jvandal@infoteck.qc.ca>
 * Copyright 2002-2003 Infoteck Internet <webmaster@infoteck.qc.ca>
 * Copyright 2002-2003 Chuck Hagenbuch <chuck@horde.org>
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Joel Vandal <jvandal@infoteck.qc.ca>
 * @author  Mike Cochrame <mike@graftonhall.co.nz>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.46 $
 * @since   Horde 3.0
 * @package horde.share
 */
class Horde_Share {

    /**
     * Pointer to a category instance to manage/store shares
     *
     * @var object Category $_shares
     */
    var $_shares;

    /**
     * Pointer to a perms instance.
     *
     * @var object Perms $_perms
     */
    var $_perms;

    /**
     * The application we're managing shares for.
     *
     * @var string $_app
     */
    var $_app;

    /**
     * Reads all the user's shares from the prefs object or builds
     * a new share from the standard values given in prefs.php.
     *
     * @param string $app   The applications that the shares
     *                      relate to.
     */
    function Horde_Share($app)
    {
        global $conf, $registry;

        if (!isset($conf['category']['driver'])) {
            Horde::fatal('You must configure a Category backend to use Shares.');
        }
        $driver = $conf['category']['driver'];
        $this->_shares = &Category::singleton($driver,
                                              array_merge(Horde::getDriverConfig('category', $driver),
                                                          array('group' => 'horde.shares.' . $app)));
        $this->_perms = &Perms::singleton();
        $this->_app = $app;
    }

    /**
     * Attempts to return a reference to a concrete Horde_Share
     * instance. It will only create a new instance if no Horde_Share
     * instance currently exists.
     *
     * This method must be invoked as: $var = &Horde_Share::singleton()
     *
     * @param string $app    The applications that the shares
     *                       relate to.
     *
     * @return object Share  The concrete Share reference, or false on an
     *                       error.
     */
    function &singleton($app)
    {
        static $shares;

        if (!isset($shares[$app])) {
            $shares[$app] = new Horde_Share($app);
        }

        return $shares[$app];
    }

    /**
     * Return a CategoryObject_Share object corresponding to the given
     * share name, with the details retrieved appropriately.
     *
     * @param string $name   The name of the share to retrieve.
     */
    function &getShare($name)
    {
        /* cache of previous retrieved shares */
        static $shareCache;

        if (!is_array($shareCache)) {
            $shareCache = array();
        }

        if (array_key_exists($name, $shareCache)) {
            return $shareCache[$name];
        }

        $shareCache[$name] = $this->_shares->getCategory($name, 'CategoryObject_Share');
        if (!is_a($shareCache[$name], 'PEAR_Error')) {
            $shareCache[$name]->setShareOb($this);
        }
        return $shareCache[$name];
    }

    /**
     * Return a CategoryObject_Share object corresponding to the given
     * unique ID, with the details retrieved appropriately.
     *
     * @param string $cid  The id of the share to retrieve.
     */
    function &getShareById($cid)
    {
        $share = $this->_shares->getCategoryById($cid, 'CategoryObject_Share');
        if (!is_a($share, 'PEAR_Error')) {
            $share->setShareOb($this);
        }
        return $share;
    }

    /**
     * Return an array of CategoryObject_Share objects corresponding
     * to the given set of unique IDs, with the details retrieved
     * appropriately.
     *
     * @param array $cids  The array of ids to retrieve.
     */
    function &getShares($cids)
    {
        $shares = &$this->_shares->getCategories($cids, 'CategoryObject_Share');
        if (is_a($shares, 'PEAR_Error')) {
            return $shares;
        }

        $keys = array_keys($shares);
        foreach ($keys as $key) {
            if (!is_a($shares[$key], 'PEAR_Error')) {
                $shares[$key]->setShareOb($this);
            }
        }

        return $shares;
    }

    /**
     * Return a new share object.
     *
     * @param string $name  The share's name.
     *
     * @return object CategoryObject_Share A new share object.
     */
    function &newShare($name)
    {
        if (empty($name)) {
            return PEAR::raiseError('Share names must be non-empty');
        }
        $share = &new CategoryObject_Share($name);
        $share->setShareOb($this);
        return $share;
    }

    /**
     * Add a share to the shares system. The share must first be
     * created with Horde_Share::newShare(), and have any initial
     * details added to it, before this function is called.
     *
     * @param object CategoryObject_Share $share The new share object.
     */
    function addShare($share)
    {
        if (!is_a($share, 'CategoryObject_Share')) {
            return PEAR::raiseError('Shares must be CategoryObject_Share objects or extend that class.');
        }

        $perm = &$this->_perms->newPermission($share->getName());

        /* Give the owner full access */
        $perm->addUserPermission($share->getOwner(), _PERMS_SHOW, false);
        $perm->addUserPermission($share->getOwner(), _PERMS_READ, false);
        $perm->addUserPermission($share->getOwner(), _PERMS_EDIT, false);
        $perm->addUserPermission($share->getOwner(), _PERMS_DELETE, false);

        $share->setPermission($perm, false);

        return $this->_shares->addCategory($share);
    }

    /**
     * Store updated data - name, etc. - of a share to the backend
     * system.
     *
     * @param object CategoryObject_Share $share   The share to update.
     */
    function updateShare($share)
    {
        if (!is_a($share, 'CategoryObject_Share')) {
            return PEAR::raiseError('Shares must be CategoryObject_Share objects or extend that class.');
        }
        return $this->_shares->updateCategoryData($share);
    }

    /**
     * Remove a share from the shares system permanently.
     *
     * @param object CategoryObject_Share $share  The share to remove.
     */
    function removeShare($share)
    {
        if (!is_a($share, 'CategoryObject_Share')) {
            return PEAR::raiseError('Shares must be CategoryObject_Share objects or extend that class.');
        }

        return $this->_shares->removeCategory($share);
    }

    function getShareId($share)
    {
        return $this->_shares->getCategoryId($share->getName());
    }

    /**
     * Utility function to be used with uasort() (do NOT use usort;
     * you'll lose key => value associations) for sorting arrays of
     * Horde_Share:: objects.
     *
     * Usage: uasort($list, array('Horde_Share', '_sortShares'));
     */
    function _sortShares($a, $b)
    {
        return strcmp($a->getShareName(), $b->getShareName());
    }

    /**
     * Check if a share exists in the system.
     *
     * @param string $share           The share to check.

     * @return boolean true if the share exists, false otherwise.
     */
    function exists($share)
    {
        return $this->_shares->exists($share);
    }

    /**
     * Return an array of all shares the given user has access to.
     *
     * @param string  $userid  The userid of the user.
     * @param integer $perm    (optional) The level of permissions required.
     * @param boolean $owner   (optional) Only return shares that $userid owns.
     *
     * @return array  The shares the user has access to (
     */
    function listShares($userid, $perm = _PERMS_SHOW, $owner = false)
    {
        static $cache;

        $key = serialize(array($this->_app, $userid, $perm, $owner));
        if (empty($cache[$key])) {
            if (!empty($userid)) {
                if ($owner) {
                    $criteria = array(
                        'AND' => array(
                            array('field' => 'name', 'op' => '=', 'test' => 'owner'),
                            array('field' => 'value', 'op' => '=', 'test' => $userid)));
                } else {
                    $criteria = array(
                        'OR' => array(
                            // (owner == $userid)
                            array(
                                'AND' => array(
                                    array('field' => 'name', 'op' => '=', 'test' => 'owner'),
                                    array('field' => 'value', 'op' => '=', 'test' => $userid))),

                            // (name == perm_users and key == $userid and val & $perm)
                            array(
                                'AND' => array(
                                    array('field' => 'name', 'op' => '=', 'test' => 'perm_users'),
                                    array('field' => 'key', 'op' => '=', 'test' => $userid),
                                    array('field' => 'value', 'op' => '&', 'test' => $perm))),

                            // (name == perm_default and val & $perm
                            array(
                                'AND' => array(
                                    array('field' => 'name', 'op' => '=', 'test' => 'perm_default'),
                                    array('field' => 'value', 'op' => '&', 'test' => $perm)))));

                    // If the user has any group memberships, check for
                    // those also.
                    require_once dirname(__FILE__) . '/Group.php';
                    $group = &Group::singleton();
                    $groups = $group->getGroupMemberships($userid);
                    if (!is_a($groups, 'PEAR_Error') && count($groups)) {
                        // (name == perm_groups and key in ($groups) and val & $perm)
                        $criteria['OR'][] = array(
                            'AND' => array(
                                array('field' => 'name', 'op' => '=', 'test' => 'perm_groups'),
                                array('field' => 'key', 'op' => 'IN', 'test' => '(' . implode(', ', array_keys($groups)) . ')'),
                                array('field' => 'value', 'op' => '&', 'test' => $perm)));
                    }
                }
            } else {
                $criteria = array(
                    'AND' => array(
                        array('field' => 'name', 'op' => '=', 'test' => 'perm_guest'),
                        array('field' => 'value', 'op' => '&', 'test' => $perm)));
            }

            $sharelist = $this->_shares->getCategoriesByAttributes($criteria);
            if (is_a($sharelist, 'PEAR_Error') || !count($sharelist)) {
                // If we got back an error or an empty array, just return
                // it.
                return $sharelist;
            }

            $cache[$key] = $this->getShares(array_keys($sharelist));
            uasort($cache[$key], array('Horde_Share', '_sortShares'));
        }

        return $cache[$key];
    }

    /**
     * List *all* shares for the current app/share category,
     * regardless of permissions. This is for admin functionality and
     * scripting tools, and shouldn't be called from user-level code
     * normally.
     *
     * @return array All shares for the current app/share category.
     */
    function listAllShares()
    {
        $shares = array();
        $sharelist = $this->_shares->get(CATEGORY_FORMAT_FLAT, '-1', true);
        unset($sharelist['-1']);

        foreach (array_values($sharelist) as $share) {
            $shares[$share] = &$this->getShare($share);
        }

        return $shares;
    }

    function getPermissions($share, $user = null)
    {
        if (!is_a($share, 'CategoryObject_share')) {
            $share = &$this->getShare($share);
        }

        $perm = &$share->getPermission();
        return $this->_perms->getPermissions($perm, $user);
    }

    /**
     * Returns the requested preferences for a particulate share
     * owner, or the current user's prefs, if this is the preferences
     * page.
     *
     * @param string $pref   The preference to fetch.
     * @param mixed  $share  (optional) The share to fetch the pref for - either
     *                       the string name, or the CategoryObject_Share object.
     *
     * @return string       The preference's value.
     */
    function getPrefByShare($pref, $share = null)
    {
        global $prefs, $registry;

        $c = parse_url($_SERVER['PHP_SELF']);
        if (preg_match('|prefs.php$|', $c['path']) || is_null($share)) {
            return $prefs->getValue($pref);
        } else {
            if (!is_a($share, 'CategoryObject_Share')) {
                $share = $this->getShare($share);
                if (is_a($share, 'PEAR_Error')) {
                    return null;
                }
            }

            $owner = $share->getOwner();
            if ($owner != Auth::getAuth()) {
                $_prefs = &Prefs::singleton($GLOBALS['conf']['prefs']['driver'],
                                            $registry->getApp(),
                                            $owner, '');

                $_prefs->setDefaults($registry->getParam('fileroot', 'horde') . '/config/prefs.php');
                $_prefs->setDefaults($registry->getParam('fileroot') . '/config/prefs.php');
                $value = $_prefs->getPref($owner, $pref, $registry->getApp());
                if (empty($value) || !$value) {
                    return $prefs->getValue($pref);
                } else {
                    return $value;
                }
            } else {
                return $prefs->getValue($pref);
            }
        }
    }

    /**
     * Returns the Identity for a particulate share owner.
     *
     * @param mixed  $share  The share to fetch the Identity for - either
     *                       the string name, or the CategoryObject_Share object.
     *
     * @return string       The preference's value.
     */
    function &getIdentityByShare($share)
    {
        global $prefs, $registry;

        if (!is_a($share, 'CategoryObject_Share')) {
            $share = $this->getShare($share);
            if (is_a($share, 'PEAR_Error')) {
                return null;
            }
        }

        require_once HORDE_BASE . '/lib/Identity.php';
        $owner = $share->getOwner();
        return new Identity($owner);
    }

}

/**
 * Extension of the CategoryObject class for storing Share information
 * in the Categories driver. If you want to store specialized Share
 * information, you should extend this class instead of extending
 * CategoryObject directly.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @version $Revision: 1.46 $
 * @since   Horde 3.0
 * @package horde.share
 */
class CategoryObject_Share extends CategoryObject {

    /**
     * The Horde_Share object which this share came from - needed for
     * updating data in the backend to make changes stick, etc.
     * @var object Horde_Share $_shareOb
     */
    var $_shareOb;

    /**
     * Pointer to a perms instance.
     *
     * @var object Perms $_perms
     */
    var $_perms;

    /**
     * The CategoryObject_Share constructor. Just makes sure to call
     * the parent constructor so that the share's is is set
     * properly.
     *
     * @param string $id The id of the share.
     */
    function CategoryObject_Share($id)
    {
        parent::CategoryObject($id);
        if (is_null($this->data)) {
            $this->data = array();
        }

        $this->_perms = &Perms::singleton();
    }

    /**
     * Associates a Share object with this share.
     *
     * @param object Share $shareOb The Share object.
     */
    function setShareOb(&$shareOb)
    {
        $this->_shareOb = &$shareOb;
    }

    function getId()
    {
        return $this->_shareOb->getShareId($this);
    }

    /**
     * Gives a user certain privileges for this share.
     *
     * @param string   $userid     The userid of the user
     * @param constant $permission A _PERMS_* constant
     */
    function addUserPermission($userid, $permission)
    {
        $perm = &$this->getPermission();
        $perm->addUserPermission($userid, $permission, false);
        $this->setPermission($perm);
    }

    /**
     * Removes a certain privileges from a user.
     *
     * @param string   $userid     The userid of the user
     * @param constant $permission A _PERMS_* constant
     */
    function removeUserPermission($userid, $permission)
    {
        $perm = &$this->getPermission();
        $perm->removeUserPermission($userid, $permission, false);
        $this->setPermission($perm);
    }

    /**
     * Gives a group certain privileges for this share.
     *
     * @param string   $group      The group to add permissions for.
     * @param constant $permission A _PERMS_* constant
     */
    function addGroupPermission($group, $permission)
    {
        $perm = &$this->getPermission();
        $perm->addGroupPermission($group, $permission, false);
        $this->setPermission($perm);
    }

    /**
     * Removes a certain privileges from a group.
     *
     * @param string   $group      The group to remove permissions from.
     * @param constant $permission A _PERMS_* constant
     */
    function removeGroupPermission($group, $permission)
    {
        $perm = &$this->getPermission();
        $perm->removeGroupPermission($group, $permission, false);
        $this->setPermission($perm);
    }

    /**
     * Checks to see if a user has a given permission.
     *
     * @param string   $userid The userid of the user
     * @param constant $priv   A _PERMS_* constant to test for
     * @param string $creator (optional) The creator of the event
     *
     * @return boolean  Whether or not $userid has $permission.
     */
    function hasPermission($userid, $permission, $creator = null)
    {
        if ($userid == $this->getOwner()) {
            return true;
        }

        if ($this->getType() == 0) {
            return false;
        }

        return $this->_perms->hasPermission($this->getPermission(), $userid, $permission, $creator);
    }

    /**
     * Remove a user from this share
     *
     * @param string $userid The userid of the user to remove
     */
    function removeUser($userid)
    {
        /* Remove all $userid's permissions. */
        $perm = &$this->getPermission();
        $perm->removeUserPermission($userid, _PERMS_SHOW, false);
        $perm->removeUserPermission($userid, _PERMS_READ, false);
        $perm->removeUserPermission($userid, _PERMS_EDIT, false);
        $perm->removeUserPermission($userid, _PERMS_DELETE, false);
    }

    /**
     * Returns an array containing all the userids of the users
     * with access to this share
     *
     * @return array The users with access to this share
     */
    function listUsers()
    {
        $perm = &$this->getPermission();
        return array_keys($perm->getUserPermissions());
    }

    /**
     * Returns an array containing all the groupids of the groups
     * with access to this share
     *
     * @return array The users with access to this share
     */
    function listGroups()
    {
        $perm = &$this->getPermission();
        return array_keys($perm->getGroupPermissions());
    }

    /**
     * Sets the name of this share
     *
     * @param $name The new name for this share
     */
    function setShareName($name, $update = true)
    {
        $this->data['name'] = $name;
        if ($update) {
            return $this->_shareOb->updateShare($this);
        }
        return true;
    }

    /**
     * Retrieves the name of this share
     *
     * @return string The name of the share
     */
    function getShareName()
    {
        return (array_key_exists('name', $this->data)) ? $this->data['name'] : '';
    }

    function getShareId()
    {
        return $this->getName();
    }

    function setType($type, $update = true)
    {
        $this->data['type'] = $type;
        if ($update) {
            return $this->_shareOb->updateShare($this);
        }
        return true;
    }

    function getType()
    {
        return (array_key_exists('type', $this->data)) ? $this->data['type'] : 0;
    }

    function setOwner($owner, $update = true)
    {
        $this->data['owner'] = $owner;
        if ($update) {
            return $this->_shareOb->updateShare($this);
        }
        return true;
    }

    function getOwner()
    {
        return (array_key_exists('owner', $this->data)) ? $this->data['owner'] : '';
    }

    function setDescription($desc, $update = true)
    {
        $this->data['desc'] = $desc;
        if ($update) {
            return $this->_shareOb->updateShare($this);
        }
        return true;
    }

    function getDescription()
    {
        return (array_key_exists('desc', $this->data)) ? $this->data['desc'] : '';
    }

    function setPermission(&$perm, $update = true)
    {
        $this->data['perm'] = $perm->getData();
        if ($update) {
            return $this->_shareOb->updateShare($this);
        }
        return true;
    }

    function &getPermission()
    {
        $perm = &new CategoryObject_Permission($this->getName());
        $perm->data = isset($this->data['perm']) ? $this->data['perm'] : array();

        return $perm;
    }

    /**
     * Save any changes to this object to the backend permanently.
     */
    function save()
    {
        return $this->_shareOb->updateShare($this);
    }

    /**
     * Map this object's attributes from the data array into a format
     * that we can store in the attributes storage backend.
     *
     * @return array  The attributes array.
     */
    function _toAttributes()
    {
        // Default to no attributes.
        $attributes = array();

        foreach ($this->data as $key => $value) {
            if ($key == 'perm') {
                foreach ($value as $type => $perms) {
                    if (is_array($perms)) {
                        foreach ($perms as $member => $perm) {
                            $attributes[] = array('name' => 'perm_' . $type,
                                                  'key' => $member,
                                                  'value' => $perm);
                        }
                    } else {
                        $attributes[] = array('name' => 'perm_' . $type,
                                              'key' => '',
                                              'value' => $perms);
                    }
                }
            } else {
                $attributes[] = array('name' => $key,
                                      'key' => '',
                                      'value' => $value);
            }
        }

        return $attributes;
    }

    /**
     * Take in a list of attributes from the backend and map it to our
     * internal data array.
     *
     * @param array $attributes  The list of attributes from the
     *                           backend (attribute name, key, and value).
     */
    function _fromAttributes($attributes)
    {
        // Initialize data array.
        $this->data['perm'] = array();

        foreach ($attributes as $attr) {
            if (substr($attr['name'], 0, 4) == 'perm') {
                if (!empty($attr['key'])) {
                    $this->data['perm'][substr($attr['name'], 5)][$attr['key']] = $attr['value'];
                } else {
                    $this->data['perm'][substr($attr['name'], 5)] = $attr['value'];
                }
            } else {
                $this->data[$attr['name']] = $attr['value'];
            }
        }
    }

}
