<?php
/**
 * The Horde_Perms_Sql:: class provides a SQL driver for the Horde
 * permissions system.
 *
 * Copyright 2008-2017 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   Duck <duck@obala.net>
 * @category Horde
 * @package  Perms
 */
class Horde_Perms_Sql extends Horde_Perms_Base
{
    /**
     * Configuration parameters.
     *
     * @var array
     */
    protected $_params = array();

    /**
     * Handle for the current database connection.
     *
     * @var Horde_Db_Adapter
     */
    protected $_db;

    /**
     * Incrementing version number if cached classes change.
     *
     * @var integer
     */
    private $_cacheVersion = 2;

    /**
     * Cache of previously retrieved permissions.
     *
     * @var array
     */
    protected $_permsCache = array();

    /**
     * Constructor.
     *
     * @param array $params  Configuration parameters (in addition to base
     *                       Horde_Perms parameters):
     * <pre>
     * 'db' - (Horde_Db_Adapter) [REQUIRED] The DB instance.
     * 'table' - (string) The name of the perms table.
     *           DEFAULT: 'horde_perms'
     * </pre>
     *
     * @throws Horde_Perms_Exception
     */
    public function __construct($params = array())
    {
        if (!isset($params['db'])) {
            throw new Horde_Perms_Exception('Missing db parameter.');
        }
        $this->_db = $params['db'];
        unset($params['db']);

        $this->_params = array_merge(array(
            'table' => 'horde_perms'
        ), $this->_params, $params);

        parent::__construct($params);
    }

    /**
     * Returns a new permissions object.
     *
     * @param string $name   The permission's name.
     * @param string $type   The permission type.
     * @param array $params  The permission parameters.
     *
     * @return Horde_Perms_Permission_Sql  A new permissions object.
     */
    public function newPermission($name, $type = 'matrix', $params = null)
    {
        $ob = new Horde_Perms_Permission_Sql($name, $this->_cacheVersion, $type, $params);
        $ob->setObs($this->_cache, $this->_db);
        return $ob;
    }

    /**
     * Returns an object corresponding to the named permission, with the
     * users and other data retrieved appropriately.
     *
     * @param string $name  The name of the permission to retrieve.
     *
     * @return Horde_Perms_Permission_Sql  TODO
     * @throw Horde_Perms_Exception
     */
    public function getPermission($name)
    {
        if (isset($this->_permsCache[$name])) {
            return $this->_permsCache[$name];
        }

        $perm = $this->_cache->get('perm_sql_' . $this->_cacheVersion . $name, $GLOBALS['conf']['cache']['default_lifetime']);
        if (!empty($perm)) {
            $this->_permsCache[$name] = unserialize($perm);
        }
        if (empty($this->_permsCache[$name])) {
            $query = 'SELECT perm_id, perm_data FROM ' .
                $this->_params['table'] . ' WHERE perm_name = ?';

            try {
                $result = $this->_db->selectOne($query, array($name));
            } catch (Horde_Db_Exception $e) {
                throw new Horde_Perms_Exception($e);
            }

            if (empty($result)) {
                throw new Horde_Perms_Exception('Does not exist', Horde_Perms_Exception::NOT_EXIST);
            }

            $object = new Horde_Perms_Permission_Sql($name, $this->_cacheVersion);
            $object->setId($result['perm_id']);
            $object->setData(unserialize($result['perm_data']));

            $this->_cache->set('perm_sql_' . $this->_cacheVersion . $name, serialize($object));

            $this->_permsCache[$name] = $object;
        }

        $this->_permsCache[$name]->setObs($this->_cache, $this->_db);

        return $this->_permsCache[$name];
    }

    /**
     * Returns a permission object corresponding to the given unique ID,
     * with the users and other data retrieved appropriately.
     *
     * @param integer $id  The unique ID of the permission to retrieve.
     *
     * @return Horde_Perms_Permission_Sql  TODO
     * @throws Horde_Perms_Exception
     */
    public function getPermissionById($id)
    {
        if ($id == Horde_Perms::ROOT || empty($id)) {
            $object = $this->newPermission(Horde_Perms::ROOT);
        } else {
            $query = 'SELECT perm_name, perm_data FROM ' .
                $this->_params['table'] . ' WHERE perm_id = ?';

            try {
                $result = $this->_db->selectOne($query, array($id));
            } catch (Horde_Db_Exception $e) {
                throw new Horde_Perms_Exception($e);
            }

            if (empty($result)) {
                throw new Horde_Perms_Exception('Does not exist', Horde_Perms_Exception::NOT_EXIST);
            }

            $object = new Horde_Perms_Permission_Sql($result['perm_name'], $this->_cacheVersion);
            $object->setId($id);
            $object->setData(unserialize($result['perm_data']));
            $object->setObs($this->_cache, $this->_db);
        }

        return $object;
    }

    /**
     * Adds a permission to the permissions system. The permission must first
     * be created with newPermission(), and have any initial users added to
     * it, before this function is called.
     *
     * @param Horde_Perms_Permission_Sql $perm  The perm object.
     *
     * @return integer  Permission ID in the database.
     * @throws Horde_Perms_Exception
     */
    public function addPermission(Horde_Perms_Permission $perm)
    {
        $name = $perm->getName();
        if (empty($name)) {
            throw new Horde_Perms_Exception('Permission name must be non-empty.');
        }

        $this->_cache->expire('perm_sql_' . $this->_cacheVersion . $name);
        $this->_cache->expire('perm_sql_exists_' . $this->_cacheVersion . $name);

        // remove root from the name
        $root = Horde_Perms::ROOT . ':';
        if (substr($name, 0, strlen($root)) == ($root)) {
            $name = substr($name, strlen($root));
        }

        // build parents
        $parents = null;
        if (($pos = strrpos($name, ':')) !== false) {
            $parent_name = substr($name, 0, $pos);
            $query = 'SELECT perm_id, perm_parents FROM ' .
                $this->_params['table'] . ' WHERE perm_name = ?';
            $result = $this->_db->selectOne($query, array($parent_name));
            if (empty($result)) {
                throw new Horde_Perms_Exception(Horde_Perms_Translation::t("Trying to create sub permission of non-existent parent permission. Create parent permission(s) first."));
            }
            $parents = $result['perm_parents'] . ':' . $result['perm_id'];
        }

        $query = 'INSERT INTO ' . $this->_params['table'] .
            ' (perm_name, perm_parents) VALUES (?, ?)';

        try {
            $id = $this->_db->insert($query, array($name, $parents));
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Perms_Exception($e);
        }

        $perm->setId($id);
        $perm->save();

        return $id;
    }

    /**
     * Removes a permission from the permissions system permanently.
     *
     * @param Horde_Perms_Permission_Sql $perm  The permission to
     *                                                remove.
     * @param boolean $force                          Force to remove every
     *                                                child.
     *
     * @return boolean  True if permission was deleted.
     * @throws Horde_Perms_Exception
     */
    public function removePermission(Horde_Perms_Permission $perm,
                                     $force = false)
    {
        $name = $perm->getName();
        $this->_cache->expire('perm_sql_' . $this->_cacheVersion . $name);
        $this->_cache->expire('perm_sql_exists_' . $this->_cacheVersion . $name);

        $query = 'DELETE FROM ' . $this->_params['table'] .
            ' WHERE perm_name = ?';

        try {
            $result = $this->_db->delete($query, array($name));
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Perms_Exception($e);
        }

        if (!$force) {
            return (bool)$result;
        }

        /* Need to expire cache for all sub-permissions. */
        try {
            $sub = $this->_db->selectValues(
                'SELECT perm_name FROM ' . $this->_params['table'] . ' WHERE perm_name LIKE ?',
                array($name . ':%')
            );
            foreach ($sub as $val) {
                $this->_cache->expire('perm_sql_' . $this->_cacheVersion . $val);
                $this->_cache->expire('perm_sql_exists_' . $this->_cacheVersion . $val);
            }
        } catch (Horde_Db_Exception $e) {}

        $query = 'DELETE FROM ' . $this->_params['table'] .
            ' WHERE perm_name LIKE ?';

        try {
            return (bool)$this->_db->delete($query, array($name . ':%'));
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Perms_Exception($e);
        }
    }

    /**
     * Returns the unique identifier of this permission.
     *
     * @param Horde_Perms_Permission_Sql $perm  The permission object to
     *                                                get the ID of.
     *
     * @return integer  The unique id.
     * @throws Horde_Perms_Exception
     */
    public function getPermissionId($permission)
    {
        if ($permission->getName() == Horde_Perms::ROOT) {
            return Horde_Perms::ROOT;
        }

        $query = 'SELECT perm_id FROM ' . $this->_params['table'] .
            ' WHERE perm_name = ?';

        try {
            return $this->_db->selectValue($query, array($permission->getName()));
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Perms_Exception($e);
        }
    }

    /**
     * Checks if a permission exists in the system.
     *
     * @param string $permission  The permission to check.
     *
     * @return boolean  True if the permission exists.
     * @throws Horde_Perms_Exception
     */
    public function exists($permission)
    {
        $key = 'perm_sql_exists_' . $this->_cacheVersion . $permission;
        $exists = $this->_cache->get($key, $GLOBALS['conf']['cache']['default_lifetime']);
        if ($exists === false) {
            $query = 'SELECT COUNT(*) FROM ' . $this->_params['table'] .
                ' WHERE perm_name = ?';

            try {
                $exists = $this->_db->selectValue($query, array($permission));
            } catch (Horde_Db_Exception $e) {
                throw new Horde_Perms_Exception($e);
            }

            $this->_cache->set($key, (string)$exists);
        }

        return (bool)$exists;
    }

    /**
     * Returns a child's direct parent ID.
     *
     * @param mixed $child  The object name for which to look up the parent's
     *                      ID.
     *
     * @return integer  The unique ID of the parent.
     * @throws Horde_Perms_Exception
     */
    public function getParent($child)
    {
        $query = 'SELECT perm_parents FROM ' . $this->_params['table'] .
            ' WHERE perm_name = ?';

        try {
            $parents = $this->_db->selectValue($query, array($child));
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Perms_Exception($e);
        }

        if (empty($parents)) {
            return Horde_Perms::ROOT;
        }

        $parents = explode(':', $parents);

        return array_pop($parents);
    }

    /**
     * Returns a list of parent permissions.
     *
     * @param string $child  The name of the child to retrieve parents for.
     *
     * @return array  A hash with all parents in a tree format.
     * @throws Horde_Perms_Exception
     */
    public function getParents($child)
    {
        $query = 'SELECT perm_parents FROM ' .  $this->_params['table'] .
            ' WHERE perm_name = ?';

        try {
            $result = $this->_db->selectValue($query, array($child));
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Perms_Exception($e);
        }

        if (empty($result)) {
            throw new Horde_Perms_Exception('Does not exist', Horde_Perms_Exception::NOT_EXIST);
        }

        return $this->_getParents($result);
    }

    /**
     * TODO
     */
    protected function _getParents($parents)
    {
        if (empty($parents)) {
            return array(Horde_Perms::ROOT => true);
        }

        $pname = $parents;
        $parents = substr($parents, 0, strrpos($parents, ':'));

        return array($pname => $this->_getParents($parents));
    }

    /**
     * Returns all permissions of the system in a tree format.
     *
     * @return array  A hash with all permissions in a tree format.
     * @throws Horde_Perms_Exception
     */
    public function getTree()
    {
        $query = 'SELECT perm_id, perm_name FROM ' . $this->_params['table'] .
            ' ORDER BY perm_name ASC';

        try {
            $tree = $this->_db->selectAssoc($query);
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Perms_Exception($e);
        }

        $tree[Horde_Perms::ROOT] = Horde_Perms::ROOT;

        return $tree;
    }
}
