<?php
/**
 * Copyright 1999-2016 Horde LLC (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 *
 * @author   Jan Schneider <jan@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package  Group
 */

/**
 * Horde_Group_Base is the base class for all drivers of the Horde group
 * system.
 *
 * @author   Jan Schneider <jan@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
 * @package  Group
 */
abstract class Horde_Group_Base
{
    /** Cache prefix. */
    const CACHE_PREFIX = 'horde_group_';

    /** Cache version. */
    const CACHE_VERSION = 1;

    /**
     * Cache object.
     *
     * @since Horde_Group 2.1.0
     * @var Horde_Cache
     */
    protected $_cache;

    /**
     * Constructor.
     */
    public function __construct($params = array())
    {
        if (isset($params['cache'])) {
            $this->_cache = $params['cache'];
        } else {
            $this->_cache = new Horde_Support_Stub();
        }
    }

    /**
     * Returns whether the group backend is read-only.
     *
     * @return boolean
     */
    public function readOnly()
    {
        return true;
    }

    /**
     * Returns whether groups can be renamed.
     *
     * @return boolean
     */
    public function renameSupported()
    {
        return true;
    }

    /**
     * Sets a cache object.
     *
     * @inject
     * @since Horde_Group 2.1.0
     * @param Horde_Cache $cache  The cache object.
     */
    public function setCache(Horde_Cache $cache)
    {
        $this->_cache = $cache;
    }

    /**
     * Returns the cache object.
     *
     * @since Horde_Group 2.1.0
     * @return Horde_Cache
     */
    public function getCache()
    {
        return $this->_cache;
    }

    /**
     * Creates a new group.
     *
     * @param string $name   A group name.
     * @param string $email  The group's email address.
     *
     * @return mixed  The ID of the created group.
     * @throws Horde_Group_Exception
     */
    public function create($name, $email = null)
    {
        // Create group.
        $gid = $this->_create($name, $email);

        // Update caches.
        if (($list = $this->_getListCache()) !== null) {
            $list[$gid] = $name;
            $this->_setListCache($list);
        }
        try {
            $this->_cache->set($this->_sig('name_' . $gid), $name);
            $this->_cache->set($this->_sig('exists_' . $gid), 1);
            $this->_cache->set(
                $this->_sig('data_' . $gid),
                serialize(array('name' => $name, 'email' => $email))
            );
        } catch (Horde_Cache_Exception $e) {
        }

        return $gid;
    }

    /**
     * Creates a new group.
     *
     * @param string $name   A group name.
     * @param string $email  The group's email address.
     *
     * @return mixed  The ID of the created group.
     * @throws Horde_Group_Exception
     */
    protected function _create($name, $email = null)
    {
        throw new Horde_Group_Exception('This group backend is read-only.');
    }

    /**
     * Renames a group.
     *
     * @param mixed $gid    A group ID.
     * @param string $name  The new name.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    public function rename($gid, $name)
    {
        // Rename group.
        $this->_rename($gid, $name);

        // Update list cache, if propagated.
        if (($list = $this->_getListCache()) !== null) {
            $list[$gid] = $name;
            $this->_setListCache($list);
        }

        // Update data and name cache.
        $sig = $this->_sig('data_' . $gid);
        try {
            if ($data = $this->_cache->get($sig, 0)) {
                $data = @unserialize($data);
                $data['name'] = $name;
                $this->_cache->set($sig, serialize($data));
            }
            $this->_cache->set($this->_sig('name_' . $gid), $name);
        } catch (Horde_Cache_Exception $e) {
        }
    }

    /**
     * Renames a group.
     *
     * @param mixed $gid    A group ID.
     * @param string $name  The new name.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    protected function _rename($gid, $name)
    {
        throw new Horde_Group_Exception('This group backend is read-only.');
    }

    /**
     * Removes a group.
     *
     * @param mixed $gid  A group ID.
     *
     * @throws Horde_Group_Exception
     */
    public function remove($gid)
    {
        // Remove group.
        $this->_remove($gid);

        // Update caches.
        if (($list = $this->_getListCache()) !== null) {
            unset($list[$gid]);
            $this->_setListCache($list);
        }
        try {
            $this->_cache->expire($this->_sig('name_' . $gid));
            $this->_cache->expire($this->_sig('data_' . $gid));
            $this->_cache->set($this->_sig('exists_' . $gid), 0);
        } catch (Horde_Cache_Exception $e) {
        }
    }

    /**
     * Removes a group.
     *
     * @param mixed $gid  A group ID.
     *
     * @throws Horde_Group_Exception
     */
    protected function _remove($gid)
    {
        throw new Horde_Group_Exception('This group backend is read-only.');
    }

    /**
     * Checks if a group exists.
     *
     * @param mixed $gid  A group ID.
     *
     * @return boolean  True if the group exists.
     * @throws Horde_Group_Exception
     */
    public function exists($gid)
    {
        // Check list cache.
        if ((($list = $this->_getListCache()) !== null) &&
            isset($list[$gid])) {
            return true;
        }

        // Check "exists" cache.
        if (is_bool($exists = $this->_checkExistsCache($gid))) {
            return $exists;
        }

        // Existance check.
        $exists = $this->_exists($gid);

        // Update "exists" cache.
        try {
            $this->_cache->set($this->_sig('exists_' . $gid), (int)$exists);
        } catch (Horde_Cache_Exception $e) {
        }

        return $exists;
    }

    protected function _checkExistsCache($gid)
    {
        // Use strlen() to catch "0" and "1".
        try {
            if (strlen($exists = $this->_cache->get($this->_sig('exists_' . $gid), 0))) {
                return (bool)$exists;
            }
        } catch (Horde_Cache_Exception $e) {
        }
    }

    /**
     * Checks if a group exists.
     *
     * @param mixed $gid  A group ID.
     *
     * @return boolean  True if the group exists.
     * @throws Horde_Group_Exception
     */
    abstract protected function _exists($gid);

    /**
     * Returns a group name.
     *
     * @param mixed $gid  A group ID.
     *
     * @return string  The group's name.
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    public function getName($gid)
    {
        // Check list cache.
        if (($list = $this->_getListCache()) !== null) {
            if (!isset($list[$gid])) {
                throw new Horde_Exception_NotFound();
            }
            return $list[$gid];
        }

        // Check "exists" cache.
        if ($this->_checkExistsCache($gid) === false) {
            throw new Horde_Exception_NotFound();
        }

        // Check name cache.
        $sig = $this->_sig('name_' . $gid);
        try {
            if ($name = $this->_cache->get($sig, 0)) {
                return $name;
            }
        } catch (Horde_Cache_Exception $e) {
        }

        // Retrieve name.
        $name = $this->_getName($gid);

        // Update name cache.
        try {
            $this->_cache->set($sig, $name);
        } catch (Horde_Cache_Exception $e) {
        }

        return $name;
    }

    /**
     * Returns a group name.
     *
     * @param mixed $gid  A group ID.
     *
     * @return string  The group's name.
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    abstract protected function _getName($gid);

    /**
     * Returns all available attributes of a group.
     *
     * @param mixed $gid  A group ID.
     *
     * @return array  The group's data.
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    public function getData($gid)
    {
        // Check list cache.
        if (($list = $this->_getListCache()) !== null &&
            !isset($list[$gid])) {
            throw new Horde_Exception_NotFound();
        }

        // Check "exists" cache.
        if ($this->_checkExistsCache($gid) === false) {
            throw new Horde_Exception_NotFound();
        }

        // Check data cache.
        $sig = $this->_sig('data_' . $gid);
        try {
            if ($data = $this->_cache->get($sig, 0)) {
                return @unserialize($data);
            }
        } catch (Horde_Cache_Exception $e) {
        }

        // Retrieve data.
        $data = $this->_getData($gid);

        // Update data cache.
        try {
            $this->_cache->set($sig, serialize($data));
        } catch (Horde_Cache_Exception $e) {
        }

        return $data;
    }

    /**
     * Returns all available attributes of a group.
     *
     * @param mixed $gid  A group ID.
     *
     * @return array  The group's date.
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    abstract protected function _getData($gid);

    /**
     * Sets one or more attributes of a group.
     *
     * @param mixed $gid               A group ID.
     * @param array|string $attribute  An attribute name or a hash of
     *                                 attributes.
     * @param string $value            An attribute value if $attribute is a
     *                                 string.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    public function setData($gid, $attribute, $value = null)
    {
        // Store data.
        $this->_setData($gid, $attribute, $value);

        // Update data cache.
        $sig = $this->_sig('data_' . $gid);
        try {
            if ($data = $this->_cache->get($sig, 0)) {
                $data = @unserialize($data);
            } else {
                $data = array();
            }
        } catch (Horde_Cache_Exception $e) {
        }
        if (is_array($attribute)) {
            $data = $attribute;
        } else {
            $data[$attribute] = $value;
        }
        try {
            $this->_cache->set($sig, serialize($data));
        } catch (Horde_Cache_Exception $e) {
        }

        // Update name and list caches.
        $name = null;
        if (is_array($attribute)) {
            if (isset($attribute['name'])) {
                $name = $attribute['name'];
            }
        } elseif ($attribute == 'name') {
            $name = $value;
        }
        if ($name) {
            try {
                $this->_cache->set($this->_sig('name_' . $gid), $name);
            } catch (Horde_Cache_Exception $e) {
            }
            if (($list = $this->_getListCache()) !== null) {
                $list[$gid] = $name;
                $this->_setListCache($list);
            }
        }
    }

    /**
     * Sets one or more attributes of a group.
     *
     * @param mixed $gid               A group ID.
     * @param array|string $attribute  An attribute name or a hash of
     *                                 attributes.
     * @param string $value            An attribute value if $attribute is a
     *                                 string.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    protected function _setData($gid, $attribute, $value = null)
    {
        throw new Horde_Group_Exception('This group backend is read-only.');
    }

    /**
     * Returns a list of all groups a user may see, with IDs as keys and names
     * as values.
     *
     * @param string $member  Only return groups that this user is a member of.
     *
     * @return array  All existing groups.
     * @throws Horde_Group_Exception
     */
    public function listAll($member = null)
    {
        if (!is_null($member)) {
            return $this->listGroups($member);
        }

        // Check list cache.
        if (($list = $this->_getListCache()) !== null) {
            return $list;
        }

        // Retrieve all groups.
        $list = $this->_listAll();

        // Update list cache.
        $this->_setListCache($list);

        return $list;
    }

    /**
     * Returns a list of all groups a user may see, with IDs as keys and names
     * as values.
     *
     * @param string $member  Only return groups that this user is a member of.
     *
     * @return array  All existing groups.
     * @throws Horde_Group_Exception
     */
    abstract protected function _listAll();

    /**
     * Returns a list of users in a group.
     *
     * @param mixed $gid  A group ID.
     *
     * @return array  List of group users.
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    public function listUsers($gid)
    {
        // Check user cache.
        $sig = $this->_sig('users_' . $gid);
        try {
            if ($users = $this->_cache->get($sig, 0)) {
                return @unserialize($users);
            }
        } catch (Horde_Cache_Exception $e) {
        }

        $users = $this->_listUsers($gid);

        // Update users cache.
        try {
            $this->_cache->set($sig, serialize($users));
        } catch (Horde_Cache_Exception $e) {
        }

        return $users;
    }

    /**
     * Returns a list of users in a group.
     *
     * @param mixed $gid  A group ID.
     *
     * @return array  List of group users.
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    abstract protected function _listUsers($gid);

    /**
     * Returns a list of groups a user belongs to.
     *
     * @param string $user  A user name.
     *
     * @return array  A list of groups, with IDs as keys and names as values.
     * @throws Horde_Group_Exception
     */
    public function listGroups($user)
    {
        // Check list cache.
        if (($list = $this->_getListCache($user)) !== null) {
            return $list;
        }

        // Retrieve all groups.
        $list = $this->_listGroups($user);

        // Update list cache.
        $this->_setListCache($list, $user);

        return $list;
    }

    /**
     * Returns a list of groups a user belongs to.
     *
     * @param string $user  A user name.
     *
     * @return array  A list of groups, with IDs as keys and names as values.
     * @throws Horde_Group_Exception
     */
    abstract protected function _listGroups($user);

    /**
     * Add a user to a group.
     *
     * @param mixed $gid    A group ID.
     * @param string $user  A user name.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    public function addUser($gid, $user)
    {
        $this->_addUser($gid, $user);

        // Update users cache.
        $sig = $this->_sig('users_' . $gid);
        try {
            if ($users = $this->_cache->get($sig, 0)) {
                $users = @unserialize($users);
                $users[] = $user;
                $this->_cache->set($sig, serialize($users));
            }
        } catch (Horde_Cache_Exception $e) {
        }
    }

    /**
     * Add a user to a group.
     *
     * @param mixed $gid    A group ID.
     * @param string $user  A user name.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    protected function _addUser($gid, $user)
    {
        throw new Horde_Group_Exception('This group backend is read-only.');
    }

    /**
     * Removes a user from a group.
     *
     * @param mixed $gid    A group ID.
     * @param string $user  A user name.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    public function removeUser($gid, $user)
    {
        $this->_removeUser($gid, $user);

        // Update users cache.
        $sig = $this->_sig('users_' . $gid);
        try {
            if ($users = $this->_cache->get($sig, 0)) {
                $users = array_flip(@unserialize($users));
                unset($users[$user]);
                $this->_cache->set($sig, serialize(array_keys($users)));
            }
        } catch (Horde_Cache_Exception $e) {
        }
    }

    /**
     * Removes a user from a group.
     *
     * @param mixed $gid    A group ID.
     * @param string $user  A user name.
     *
     * @throws Horde_Group_Exception
     * @throws Horde_Exception_NotFound
     */
    protected function _removeUser($gid, $user)
    {
        throw new Horde_Group_Exception('This group backend is read-only.');
    }

    /**
     * Searches for group names.
     *
     * @param string $name  A search string.
     *
     * @return array  A list of matching groups, with IDs as keys and names as
     *                values.
     * @throws Horde_Group_Exception
     */
    public function search($name)
    {
        // Check search cache.
        $sig = $this->_sig('search_' . hash('md5', $name));
        try {
            if ($groups = $this->_cache->get($sig, 0)) {
                return @unserialize($groups);
            }
        } catch (Horde_Cache_Exception $e) {
        }

        $groups = $this->_search($name);

        // Update groups cache.
        try {
            $this->_cache->set($sig, serialize($groups));
        } catch (Horde_Cache_Exception $e) {
        }

        return $groups;
    }

    /**
     * Searches for group names.
     *
     * @param string $name  A search string.
     *
     * @return array  A list of matching groups, with IDs as keys and names as
     *                values.
     * @throws Horde_Group_Exception
     */
    abstract protected function _search($name);

    /**
     * Returns a full cache key.
     *
     * @param string $key  The internal key.
     *
     * @return string  The full key for Horde_Cache consumption.
     */
    protected function _sig($key)
    {
        return self::CACHE_PREFIX . self::CACHE_VERSION . '_' . $key;
    }

    /**
     * Returns the cached group list.
     *
     * @param string $user  A user name.
     *
     * @return array  The group list or null if not cached.
     */
    protected function _getListCache($user = null)
    {
        $sig = $this->_sig('list');
        if (!is_null($user)) {
            $sig .= '_' . $user;
        }
        try {
            if ($list = $this->_cache->get($sig, 0)) {
                return @unserialize($list);
            }
        } catch (Horde_Cache_Exception $e) {
        }
    }

    /**
     * Sets the cached group list.
     *
     * @param array $list   A group list.
     * @param string $user  A user name.
     */
    protected function _setListCache(array $list, $user = null)
    {
        $sig = $this->_sig('list');
        if (!is_null($user)) {
            $sig .= '_' . $user;
        }
        try {
            $this->_cache->set($sig, serialize($list));
        } catch (Horde_Cache_Exception $e) {
        }
    }
}
