<?php
/**
 * Copyright 2002-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   Ilya Krel <mail@krel.org>
 * @author   Jan Schneider <jan@horde.org>
 * @category Horde
 * @license  http://www.horde.org/licenses/lgpl21 LGPL-2.1
 * @package  Auth
 */

/**
 * The Horde_Auth_Cyrsql class provides a SQL implementation of the Horde
 * authentication system for the Cyrus IMAP server. Most of the functionality
 * is the same as for the SQL class; only what is different overrides the
 * parent class implementations.
 *
 * The table structure for the auth system is as follows:
 * <pre>
 * CREATE TABLE accountuser (
 *     username    VARCHAR(255) BINARY NOT NULL DEFAULT '',
 *     password    VARCHAR(32) BINARY NOT NULL DEFAULT '',
 *     prefix      VARCHAR(50) NOT NULL DEFAULT '',
 *     domain_name VARCHAR(255) NOT NULL DEFAULT '',
 *     UNIQUE KEY username (username)
 * );
 *
 * CREATE TABLE adminuser (
 *     username    VARCHAR(50) BINARY NOT NULL DEFAULT '',
 *     password    VARCHAR(50) BINARY NOT NULL DEFAULT '',
 *     type        INT(11) NOT NULL DEFAULT '0',
 *     SID         VARCHAR(255) NOT NULL DEFAULT '',
 *     home        VARCHAR(255) NOT NULL DEFAULT '',
 *     PRIMARY KEY (username)
 * );
 *
 * CREATE TABLE alias (
 *     alias       VARCHAR(255) NOT NULL DEFAULT '',
 *     dest        LONGTEXT,
 *     username    VARCHAR(50) NOT NULL DEFAULT '',
 *     status      INT(11) NOT NULL DEFAULT '1',
 *     PRIMARY KEY (alias)
 * );
 *
 * CREATE TABLE domain (
 *     domain_name VARCHAR(255) NOT NULL DEFAULT '',
 *     prefix      VARCHAR(50) NOT NULL DEFAULT '',
 *     maxaccounts INT(11) NOT NULL DEFAULT '20',
 *     quota       INT(10) NOT NULL DEFAULT '20000',
 *     transport   VARCHAR(255) NOT NULL DEFAULT 'cyrus',
 *     freenames   ENUM('YES','NO') NOT NULL DEFAULT 'NO',
 *     freeaddress ENUM('YES','NO') NOT NULL DEFAULT 'NO',
 *     PRIMARY KEY (domain_name),
 *     UNIQUE KEY prefix (prefix)
 * );
 *
 * CREATE TABLE domainadmin (
 *     domain_name VARCHAR(255) NOT NULL DEFAULT '',
 *     adminuser   VARCHAR(255) NOT NULL DEFAULT ''
 * );
 *
 * CREATE TABLE search (
 *     search_id   VARCHAR(255) NOT NULL DEFAULT '',
 *     search_sql  TEXT NOT NULL,
 *     perpage     INT(11) NOT NULL DEFAULT '0',
 *     timestamp   TIMESTAMP(14) NOT NULL,
 *     PRIMARY KEY (search_id),
 *     KEY search_id (search_id)
 * );
 *
 * CREATE TABLE virtual (
 *     alias       VARCHAR(255) NOT NULL DEFAULT '',
 *     dest        LONGTEXT,
 *     username    VARCHAR(50) NOT NULL DEFAULT '',
 *     status      INT(11) NOT NULL DEFAULT '1',
 *     KEY alias (alias)
 * );
 *
 * CREATE TABLE log (
 *     id          INT(11) NOT NULL AUTO_INCREMENT,
 *     msg         TEXT NOT NULL,
 *     user        VARCHAR(255) NOT NULL DEFAULT '',
 *     host        VARCHAR(255) NOT NULL DEFAULT '',
 *     time        DATETIME NOT NULL DEFAULT '2000-00-00 00:00:00',
 *     pid         VARCHAR(255) NOT NULL DEFAULT '',
 *     PRIMARY KEY (id)
 * );
 * </pre>
 *
 * @author    Ilya Krel <mail@krel.org>
 * @author    Jan Schneider <jan@horde.org>
 * @category  Horde
 * @copyright 2002-2017 Horde LLC
 * @license   http://www.horde.org/licenses/lgpl21 LGPL-2.1
 * @package   Auth
 */
class Horde_Auth_Cyrsql extends Horde_Auth_Sql
{
    /**
     * An array of capabilities, so that the driver can report which
     * operations it supports and which it doesn't.
     *
     * @var array
     */
    protected $_capabilities = array(
        'add'           => true,
        'list'          => true,
        'remove'        => true,
        'resetpassword' => false,
        'update'        => true,
        'authenticate'  => true,
    );

    /**
     * Horde_Imap_Client object.
     *
     * @var Horde_Imap_Client_Base
     */
    protected $_imap;

    /**
     * Constructor.
     *
     * @param array $params  Parameters:
     *   - domain_field: (string) If set to anything other than 'none' this is
     *                   used as field name where domain is stored.
     *                   DEFAULT: 'domain_name'
     *   - folders: (array) An array of folders to create under username.
     *                DEFAULT: NONE
     *   - hidden_accounts: (array) An array of system accounts to hide from
     *                      the user interface.
     *                      DEFAULT: None.
     *   - imap: (Horde_Imap_Client_Base) [REQUIRED] An IMAP client object.
     *   - quota: (integer) The quota (in kilobytes) to grant on the mailbox.
     *            DEFAULT: NONE
     *   - userhierarchy: (string) The user hierarchy prefix (UTF-8).
     *                    DEFAULT: 'user.'
     *
     * @throws InvalidArgumentException
     */
    public function __construct(array $params = array())
    {
        if (!isset($params['imap']) ||
            !($params['imap'] instanceof Horde_Imap_Client_Base)) {
            throw new InvalidArgumentException('Missing imap parameter.');
        }
        $this->_imap = $params['imap'];
        unset($params['imap']);

        $params = array_merge(array(
            'domain_field' => 'domain_name',
            'folders' => array(),
            'hidden_accounts' => array('cyrus'),
            'quota' => null,
            'userhierarchy' => 'user.'
        ), $params);

        parent::__construct($params);
    }

    /**
     * Find out if a set of login credentials are valid.
     *
     * @param string $userId      The userId to check.
     * @param array $credentials  The credentials to use.
     *
     * @throws Horde_Auth_Exception
     */
    protected function _authenticate($userId, $credentials)
    {
        if (!empty($this->_params['domain_field']) &&
            ($this->_params['domain_field'] != 'none')) {
            /* Build the SQL query with domain. */
            $query = sprintf('SELECT * FROM %s WHERE %s = ? AND %s = ?',
                             $this->_params['table'],
                             $this->_params['username_field'],
                             $this->_params['domain_field']);
            $values = explode('@', $userId);
        } else {
            /* Build the SQL query without domain. */
            $query = sprintf('SELECT * FROM %s WHERE %s = ?',
                             $this->_params['table'],
                             $this->_params['username_field']);
            $values = array($userId);
        }

        try {
            $row = $this->_db->selectOne($query, $values);
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Auth_Exception('', Horde_Auth::REASON_FAILED);
        }

        if (!$row ||
            !$this->_comparePasswords($row[$this->_params['password_field']], $credentials['password'])) {
            throw new Horde_Auth_Exception('', Horde_Auth::REASON_BADLOGIN);
        }

        $now = time();
        if (!empty($this->_params['hard_expiration_field']) &&
            !empty($row[$this->_params['hard_expiration_field']]) &&
            ($now > $row[$this->_params['hard_expiration_field']])) {
            throw new Horde_Auth_Exception('', Horde_Auth::REASON_EXPIRED);
        }

        if (!empty($this->_params['soft_expiration_field']) &&
            !empty($row[$this->_params['soft_expiration_field']]) &&
            ($now > $row[$this->_params['soft_expiration_field']])) {
            $this->setCredential('change', true);
        }
    }

    /**
     * Add a set of authentication credentials.
     *
     * @param string $userId       The userId to add.
     * @param array  $credentials  The credentials to add.
     *
     * @throw Horde_Auth_Exception
     */
    public function addUser($userId, $credentials)
    {
        if (!empty($this->_params['domain_field']) &&
            ($this->_params['domain_field'] != 'none')) {
            list($name, $domain) = explode('@', $userId);

            $query = sprintf('INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)',
                             $this->_params['table'],
                             $this->_params['username_field'],
                             $this->_params['domain_field'],
                             $this->_params['password_field']);
            $values = array(
                $name,
                $domain,
                Horde_Auth::getCryptedPassword($credentials['password'],
                                               '',
                                               $this->_params['encryption'],
                                               $this->_params['show_encryption'])
            );

            $query2 = 'INSERT INTO virtual (alias, dest, username, status) VALUES (?, ?, ?, 1)';
            $values2 = array($userId, $userId, $name);

            try {
                $this->_db->insert($query, $values);
                $this->_db->insert($query2, $values2);
            } catch (Horde_Db_Exception $e) {
                throw new Horde_Auth_Exception($e);
            }
        } else {
            parent::addUser($userId, $credentials);
        }

        $mailbox = $this->_params['userhierarchy'] . $userId;

        try {
            $this->_imap->createMailbox($mailbox);
            $this->_imap->setACL($mailbox, $this->_params['cyradmin'], array('rights' => 'lrswipcda'));
            if (isset($this->_params['quota']) &&
                ($this->_params['quota'] >= 0)) {
                $this->_imap->setQuota($mailbox, array('storage' => $this->_params['quota']));
            }
        } catch (Horde_Imap_Client_Exception $e) {
            throw new Horde_Auth_Exception($e);
        }

        foreach ($this->_params['folders'] as $val) {
            try {
                $this->_imap->createMailbox($val);
                $this->_imap->setACL($val, $this->_params['cyradmin'], array('rights' => 'lrswipcda'));
            } catch (Horde_Imap_Client_Exception $e) {}
        }
    }

    /**
     * Delete a set of authentication credentials.
     *
     * @param string $userId  The userId to delete.
     *
     * @throws Horde_Auth_Exception
     */
    public function removeUser($userId)
    {
        if (!empty($this->_params['domain_field']) &&
            ($this->_params['domain_field'] != 'none')) {
            list($name, $domain) = explode('@', $userId);

            /* Build the SQL query. */
            $query = sprintf('DELETE FROM %s WHERE %s = ? and %s = ?',
                             $this->_params['table'],
                             $this->_params['username_field'],
                             $this->_params['domain_field']);
            $values = array($name, $domain);

            $query2 = 'DELETE FROM virtual WHERE dest = ?';
            $values2 = array($userId);

            try {
                $this->_db->delete($query, $values);
                $this->_db->delete($query2, $values2);
            } catch (Horde_Db_Exception $e) {
                throw new Horde_Auth_Exception($e);
            }
        } else {
            parent::removeUser($userId);
        }

        /* Set ACL for mailbox deletion. */
        list($admin) = explode('@', $this->_params['cyradmin']);

        $mailbox = $this->_params['userhierarchy'] . $userId;

        try {
            $this->_imap->setACL($mailbox, $admin, array('rights' => 'lrswipcda'));
            $this->_imap->deleteMailbox($mailbox);
        } catch (Horde_Imap_Client_Exception $e) {
            throw new Horde_Auth_Exception($e);
        }
    }

    /**
     * List all users in the system.
     *
     * @param boolean $sort  Sort the users?
     *
     * @return mixed  The array of userIds.
     * @throws Horde_Auth_Exception
     */
    public function listUsers($sort = false)
    {
        if (!empty($this->_params['domain_field']) &&
            ($this->_params['domain_field'] != 'none')) {
            /* Build the SQL query with domain. */
            $query = sprintf('SELECT %s, %s FROM %s',
                             $this->_params['username_field'],
                             $this->_params['domain_field'],
                             $this->_params['table']);
        } else {
            /* Build the SQL query without domain. */
            $query = sprintf('SELECT %s FROM %s',
                             $this->_params['username_field'],
                             $this->_params['table']);
        }
        if ($sort) {
            $query .= sprintf(" ORDER BY %s", $this->_params['username_field']);
        }

        try {
            $result = $this->_db->select($query);
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Auth_Exception($e);
        }

        /* Loop through and build return array. */
        $users = array();
        if (!empty($this->_params['domain_field']) &&
            ($this->_params['domain_field'] != 'none')) {
            foreach ($result as $ar) {
                if (!in_array($ar[$this->_params['username_field']], $this->_params['hidden_accounts'])) {
                    $users[] = $ar[$this->_params['username_field']] . '@' . $ar[$this->_params['domain_field']];
                }
            }
        } else {
            foreach ($result as $ar) {
                if (!in_array($ar[$this->_params['username_field']], $this->_params['hidden_accounts'])) {
                    $users[] = $ar[$this->_params['username_field']];
                }
            }
        }
        return $users;
    }

    /**
     * Update a set of authentication credentials.
     *
     * @param string $oldID       The old userId.
     * @param string $newID       The new userId. [NOT SUPPORTED]
     * @param array $credentials  The new credentials
     *
     * @throws Horde_Auth_Exception
     */
    public function updateUser($oldID, $newID, $credentials)
    {
        if (!empty($this->_params['domain_field']) &&
            ($this->_params['domain_field'] != 'none')) {
            list($name, $domain) = explode('@', $oldID);
            /* Build the SQL query with domain. */
            $query = sprintf(
                'UPDATE %s SET %s = ? WHERE %s = ? and %s = ?',
                $this->_params['table'],
                $this->_params['password_field'],
                $this->_params['username_field'],
                $this->_params['domain_field']
            );
            $values = array(
                Horde_Auth::getCryptedPassword($credentials['password'], '', $this->_params['encryption'], $this->_params['show_encryption']),
                $name,
                $domain
            );
        } else {
            /* Build the SQL query. */
            $query = sprintf(
                'UPDATE %s SET %s = ? WHERE %s = ?',
                $this->_params['table'],
                $this->_params['password_field'],
                $this->_params['username_field']
            );
            $values = array(
                Horde_Auth::getCryptedPassword($credentials['password'], '', $this->_params['encryption'], $this->_params['show_encryption']),
                $oldID
            );
        }

        try {
            $this->_db->update($query, $values);
        } catch (Horde_Db_Exception $e) {
            throw new Horde_Auth_Exception($e);
        }
    }

}
