<?php

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

/**
 * The Group:: class provides the Horde groups system.
 *
 * $Horde: horde/lib/Group.php,v 1.46 2003/08/01 17:28:52 chuck Exp $
 *
 * Copyright 1999-2003 Stephane Huther <shuther@bigfoot.com>
 * Copyright 2001-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  Stephane Huther <shuther@bigfoot.com>
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.46 $
 * @since   Horde 2.1
 * @package horde.group
 */
class Group {

    /**
     * Pointer to a category instance to manage the different groups.
     * @var object Category $groups
     */
    var $_groups;

    /**
     * Constructor
     */
    function Group()
    {
        global $conf;

        if (!isset($conf['category']['driver'])) {
            Horde::fatal('You must configure a Category backend to use Groups.');
        }
        $driver = $conf['category']['driver'];
        $this->_groups = &Category::singleton($driver,
                                              array_merge(Horde::getDriverConfig('category', $driver),
                                                          array('group' => 'horde.groups')));
    }

    /**
     * Return a new group object.
     *
     * @param string $name The group's name.
     *
     * @return object CategoryObject_Group A new group object.
     */
    function &newGroup($name)
    {
        if (empty($name)) {
            return PEAR::raiseError(_("Group names must be non-empty"));
        }
        $group = &new CategoryObject_Group($name);
        $group->setGroupOb($this);
        return $group;
    }

    /**
     * Return a CategoryObject_Group object corresponding to the named
     * group, with the users and other data retrieved appropriately.
     *
     * @param string $name The name of the group to retrieve.
     */
    function &getGroup($name)
    {
        /* cache of previous retrieved groups */
        static $groupCache;

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

        if (!isset($groupCache[$name])) {
            $groupCache[$name] = $this->_groups->getCategory($name, 'CategoryObject_Group');
            if (!is_a($groupCache[$name], 'PEAR_Error')) {
                $groupCache[$name]->setGroupOb($this);
            }
        }

        return $groupCache[$name];
    }

    /**
     * Return a CategoryObject_Group object corresponding to the given
     * unique ID, with the users and other data retrieved
     * appropriately.
     *
     * @param string $cid  The unique ID of the group to retrieve.
     */
    function &getGroupById($cid)
    {
        $group = $this->_groups->getCategoryById($cid, 'CategoryObject_Group');
        if (!is_a($group, 'PEAR_Error')) {
            $group->setGroupOb($this);
        }
        return $group;
    }

    /**
     * Add a group to the groups system. The group must first be
     * created with Group::newGroup(), and have any initial users
     * added to it, before this function is called.
     *
     * @param object CategoryObject_Group $group The new group object.
     */
    function addGroup($group)
    {
        if (!is_a($group, 'CategoryObject_Group')) {
            return PEAR::raiseError('Groups must be CategoryObject_Group objects or extend that class.');
        }
        return $this->_groups->addCategory($group);
    }

    /**
     * Store updated data - users, etc. - of a group to the backend
     * system.
     *
     * @param object CategoryObject_Group $group   The group to update.
     */
    function updateGroup($group)
    {
        if (!is_a($group, 'CategoryObject_Group')) {
            return PEAR::raiseError('Groups must be CategoryObject_Group objects or extend that class.');
        }
        return $this->_groups->updateCategoryData($group);
    }

    /**
     * Change the name of a group without changing its contents or
     * where it is in the groups hierarchy.
     *
     * @param object CategoryObject_Group $group   The group to rename.
     * @param string                      $newName The group's new name.
     */
    function renameGroup($group, $newName)
    {
        if (!is_a($group, 'CategoryObject_Group')) {
            return PEAR::raiseError('Groups must be CategoryObject_Group objects or extend that class.');
        }
        return $this->_groups->renameCategory($group, $newName);
    }

    /**
     * Remove a group from the groups system permanently.
     *
     * @param object CategoryObject_Group $group  The group to remove.
     *
     * @param optional boolean force [default = false] Force to remove
     *                         every child
     */
    function removeGroup($group, $force = false)
    {
        if (!is_a($group, 'CategoryObject_Group')) {
            return PEAR::raiseError('Groups must be CategoryObject_Group objects or extend that class.');
        }

        return $this->_groups->removeCategory($group, $force);
    }

    /**
     * Retrieve the name of a group.
     *
     * @param integer $groupId  The id of the group to retrieve the name for..
     *
     * @return string  The group's name.
     */
    function getGroupName($groupId)
    {
        if (is_a($groupId, 'CategoryObject_Group')) {
            return $this->_groups->getCategoryName($groupId->getId());
        } else {
            return $this->_groups->getCategoryName($groupId);
        }
    }

    /**
     * Retrieve the ID of a group.
     *
     * @param string $group  The group to retrieve the ID for..
     *
     * @return integer  The group's ID.
     */
    function getGroupId($group)
    {
        if (is_a($group, 'CategoryObject_Group')) {
            return $this->_groups->getCategoryId($group->getName());
        } else {
            return $this->_groups->getCategoryId($group);
        }
    }

    /**
     * Check if a group exists in the system.
     *
     * @param string $group           The group to check.
     *
     * @return boolean true if the group exists, false otherwise.
     */
    function exists($group)
    {
        return $this->_groups->exists($group);
    }

    /**
     * Get a list of the parents of a child group.
     *
     * @param string $group The name of the child group.
     *
     * @return array
     */
    function getGroupParents($group)
    {
        return $this->_groups->getParents($group);
    }

    /**
     * Get a list of every group, in the format cid => groupname.
     *
     * @return array  CID => groupname hash.
     */
    function listGroups()
    {
        static $groups;

        if (is_null($groups)) {
            $groups = $this->_groups->get(CATEGORY_FORMAT_FLAT, '-1', true);
            unset($groups['-1']);
        }

        return $groups;
    }

    /**
     * Get a list of every user that is a part of this group ONLY.
     *
     * @param string $group The name of the group.
     *
     * @return array The user list.
     * @access public
     */
    function listUsers($group)
    {
        $groupOb = &$this->getGroup($group);
        if (!isset($groupOb->data['users']) ||
            !is_array($groupOb->data['users'])) {
            return array();
        }

        return array_keys($groupOb->data['users']);
    }

    /**
     * Get a list of every user that is part of the specified group
     * and any of its subgroups.
     *
     * @access public
     *
     * @param string $group The name of the parent group.
     *
     * @return array  The complete user list.
     */
    function listAllUsers($group)
    {
        // Get a list of every group that is a sub-group of $group.
        $groups = array($group) + array_keys($this->_groups->get(CATEGORY_FORMAT_FLAT, $group, true));
        $users = array();
        foreach ($groups as $group) {
            $users = array_merge($users, $this->listUsers($group));
        }
        return array_values(array_flip(array_flip($users)));
    }

    /**
     * Get a list of every group that $user is in.
     *
     * @param string  $user    The user to get groups for.
     * @param boolean $cached  Allow caching of the data? If false,
     *                         we'll always check the backend.
     *
     * @return array  An array of all groups the user is in.
     */
    function getGroupMemberships($user, $cached = true)
    {
        static $cache;

        if (!$cached || empty($cached[$user])) {
            $criteria = array(
                'AND' => array(
                    array('field' => 'name', 'op' => '=', 'test' => 'user'),
                    array('field' => 'key', 'op' => '=', 'test' => $user)));
            $cache[$user] = $this->_groups->getCategoriesByAttributes($criteria);
        }

        return $cache[$user];
    }

    /**
     * Say if a user is a member of a group or not.
     *
     * @param          string  $user       The name of the user.
     * @param          string  $group      The name of the group.
     * @param optional boolean $subgroups  Return true if the user is in any subgroups
     *                                     of $group, also.
     *
     * @return boolean
     * @access public
     */
    function userIsInGroup($user, $group, $subgroups = false)
    {
        if ($subgroups) {
            $group = &$this->getGroup($group);
            if (is_a($group, 'PEAR_Error')) {
                return false;
            }
            $groups = $this->getGroupMemberships($user);
            return !empty($groups[$this->getGroupId($group)]);
        } else {
            $users = $this->listUsers($group);
            return in_array($user, $users);
        }
    }

    /**
     * Attempts to return a concrete Group instance based on $driver.
     *
     * @access public
     *
     * @param mixed $driver           The type of concrete Group subclass to
     *                                return. The code is dynamically
     *                                included.
     * @param optional array $params  A hash containing any additional
     *                                configuration or connection parameters a
     *                                subclass might need.
     *
     * @return object Group   The newly created concrete Group instance, or a
     *                        PEAR_Error object on an error.
     */
    function &factory($driver = '', $params = null)
    {
        $driver = basename($driver);

        if (@file_exists(dirname(__FILE__) . '/Group/' . $driver . '.php')) {
            require_once dirname(__FILE__) . '/Group/' . $driver . '.php';
        } else {
            @include_once 'Horde/Group/' . $driver . '.php';
        }
        $class = 'Group_' . $driver;
        if (class_exists($class)) {
            return new $class($params);
        } else {
            return PEAR::raiseError('Class definition of ' . $class . ' not found.');
        }
    }

    /**
     * Attempts to return a reference to a concrete Group instance.
     * It will only create a new instance if no Group instance
     * currently exists.
     *
     * This method must be invoked as: $var = &Group::singleton()
     *
     * @return object Group  The concrete Group reference, or false on an
     *                       error.
     */
    function &singleton()
    {
        static $group;

        if (!isset($group)) {
            global $conf;

            require_once HORDE_BASE . '/lib/Auth.php';
            $auth = &Auth::singleton($conf['auth']['driver']);
            if ($auth->hasCapability('groups')) {
                $group = &Group::factory($auth->getDriver(), $auth);
            } else {
                $group = new Group();
            }
        }

        return $group;
    }

}

/**
 * Extension of the CategoryObject class for storing Group information
 * in the Categories driver. If you want to store specialized Group
 * information, you should extend this class instead of extending
 * CategoryObject directly.
 *
 * @author  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.46 $
 * @since   Horde 2.1
 * @package horde.group
 */
class CategoryObject_Group extends CategoryObject {

    /** The Group object which this group came from - needed for
        updating data in the backend to make changes stick, etc.
        @var object Group $groupOb */
    var $_groupOb;

    /**
     * The CategoryObject_Group constructor. Just makes sure to call
     * the parent constructor so that the group's name is set
     * properly.
     *
     * @param string $name The name of the group.
     */
    function CategoryObject_Group($name)
    {
        parent::CategoryObject($name);
    }

    /**
     * Associates a Group object with this group.
     *
     * @param object Group $groupOb The Group object.
     */
    function setGroupOb(&$groupOb)
    {
        $this->_groupOb = &$groupOb;
    }

    /**
     * Fetch the ID of this group
     *
     * @return string The group's ID
     */
    function getId()
    {
        return $this->_groupOb->getGroupId($this);
    }

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

    /**
     * Adds a user to this group, and makes sure that the backend is
     * updated as well.
     *
     * @param string $username The user to add.
     */
    function addUser($username, $update = true)
    {
        $this->data['users'][$username] = 1;
        if ($update && $this->_groupOb->_groups->exists($this->getName())) {
            $this->save();
        }
    }

    /**
     * Removes a user from this group, and makes sure that the backend
     * is updated as well.
     *
     * @param string $username The user to remove.
     */
    function removeUser($username, $update = true)
    {
        unset($this->data['users'][$username]);
        if ($update) {
            $this->save();
        }
    }

    /**
     * Get a list of every user that is a part of this group
     * (and only this group)
     *
     * @return array The user list
     * @access public
     */
    function listUsers()
    {
        return $this->_groupOb->listUsers($this->name);
    }

    /**
     * Get a list of every user that is a part of this group and
     * any of it's subgroups
     *
     * @return array The complete user list
     * @access public
     */
    function listAllUsers()
    {
        return $this->_groupOb->listAllUsers($this->name);
    }

    /**
     * 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();

        // Loop through all users, if any.
        if (isset($this->data['users']) && is_array($this->data['users']) && count($this->data['users'])) {
            foreach ($this->data['users'] as $user => $active) {
                $attributes[] = array('name' => 'user',
                                      'key' => $user,
                                      'value' => $active);
            }
        }
        $attributes[] = array('name' => 'email',
                              'key' => '',
                              'value' => $this->get('email'));

        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['users'] = array();

        foreach ($attributes as $attr) {
            if ($attr['name'] == 'user') {
                $this->data['users'][$attr['key']] = $attr['value'];
            } else {
                $this->data[$attr['name']] = $attr['value'];
            }
        }
    }

}
