<?php
/**
 * The Auth:: class provides a common abstracted interface into the
 * various backends for the Horde authentication system.
 *
 * $Horde: horde/lib/Auth.php,v 1.93 2003/07/18 16:28:28 chuck Exp $
 *
 * Copyright 1999-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  Chuck Hagenbuch <chuck@horde.org>
 * @version $Revision: 1.93 $
 * @since   Horde 1.3
 * @package Horde_Auth
 */
class Auth {

    /**
     * An array of capabilities, so that the driver can report which
     * operations it supports and which it doesn't.
     *
     * @var array $capabilities
     */
    var $capabilities = array('add'         => false,
                              'update'      => false,
                              'remove'      => false,
                              'list'        => false,
                              'groups'      => false,
                              'transparent' => false,
                              'loginscreen' => false);

    /**
     * Authentication error message.
     *
     * @var string $_authError
     */
    var $_authError = '';

   /**
    * Hash containing parameters.
    *
    * @var array $_params
    */
    var $_params = array();

    /**
     * Attempts to return a concrete Auth instance based on $driver.
     *
     * @access public
     *
     * @param mixed $driver           The type of concrete Auth subclass to
     *                                return. This is based on the storage
     *                                driver ($driver). The code is dynamically
     *                                included. If $driver is an array, then we
     *                                will look in $driver[0]/lib/Auth/ for
     *                                the subclass implementation named
     *                                $driver[1].php.
     * @param optional array $params  A hash containing any additional
     *                                configuration or connection parameters a
     *                                subclass might need.
     *
     * @return object Auth   The newly created concrete Auth instance, or false
     *                       on an error.
     */
    function &factory($driver, $params = null)
    {
        if (is_array($driver)) {
            $app = $driver[0];
            $driver = $driver[1];
        }

        $driver = basename($driver);

        if (empty($driver) || (strcmp($driver, 'none') == 0)) {
            return new Auth;
        }

        if (is_null($params)) {
            $params = Horde::getDriverConfig('auth', $driver);
        }

        if (!empty($app)) {
            require_once $GLOBALS['registry']->getParam('fileroot', $app) . '/lib/Auth/' . $driver . '.php';
        } elseif (@file_exists(dirname(__FILE__) . '/Auth/' . $driver . '.php')) {
            require_once dirname(__FILE__) . '/Auth/' . $driver . '.php';
        } else {
            @include_once 'Horde/Auth/' . $driver . '.php';
        }
        $class = 'Auth_' . $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 Auth instance
     * based on $driver. It will only create a new instance if no Auth
     * instance with the same parameters currently exists.
     *
     * This should be used if multiple authentication sources (and,
     * thus, multiple Auth instances) are required.
     *
     * This method must be invoked as: $var = &Auth::singleton()
     *
     * @access public
     *
     * @param string $driver          The type of concrete Auth subclass to
     *                                return. This is based on the storage
     *                                driver ($driver). The code is dynamically
     *                                included.
     * @param optional array $params  A hash containing any additional
     *                                configuration or connection parameters a
     *                                subclass might need.
     *
     * @return object Auth  The concrete Auth reference, or false on an error.
     */
    function &singleton($driver, $params = null)
    {
        static $instances;

        if (!isset($instances)) {
            $instances = array();
        }

        if (is_null($params)) {
            $params = Horde::getDriverConfig('auth',
                is_array($driver) ? $driver[1] : $driver);
        }

        $signature = serialize(array($driver, $params));
        if (!array_key_exists($signature, $instances)) {
            $instances[$signature] = &Auth::factory($driver, $params);
        }

        return $instances[$signature];
    }

    /**
     * Returns the name of the concrete Auth implementation.
     *
     * @return string  The Auth implementation name.
     */
    function getDriver()
    {
        return str_replace('auth_', '', get_class($this));
    }

    /**
     * Find out if a set of login credentials are valid, and if
     * requested, mark the user as logged in in the current session.
     *
     * @access public
     *
     * @param string $userID           The userID to check.
     * @param array $credentials       The credentials to check.
     * @param optional boolean $login  Whether to log the user in. If false,
     *                                 we'll only test the credentials and
     *                                 won't modify the current session.
     *                                 Defaults to true.
     * @param optional string $realm   The authentication realm to check.
     *
     * @return boolean  Whether or not the credentials are valid.
     */
    function authenticate($userID, $credentials, $login = true, $realm = null)
    {
        global $conf;

        $userID = trim($userID);

        if ($this->_authenticate($userID, $credentials)) {
            if ($login) {
                $this->clearAuth();
                $this->setAuth($userID, $credentials, $realm);
                return true;
            } elseif (empty($conf['auth']['checkip']) ||
                      ($_SESSION['__auth']['remote_addr'] == $_SERVER['REMOTE_ADDR'])) {
                return true;
            }
        }

        return false;
    }

    /**
     * Add a set of authentication credentials.
     *
     * @access public
     *
     * @param string $userID      The userID to add.
     * @param array $credentials  The credentials to use.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function addUser($userID, $credentials)
    {
        return PEAR::raiseError('unsupported');
    }

    /**
     * Update a set of authentication credentials.
     *
     * @access public
     *
     * @param string $oldID       The old userID.
     * @param string $newID       The new userID.
     * @param array $credentials  The new credentials
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function updateUser($oldID, $newID, $credentials)
    {
        return PEAR::raiseError('unsupported');
    }

    /**
     * Delete a set of authentication credentials.
     *
     * @access public
     *
     * @param string $userID  The userID to delete.
     *
     * @return mixed  True on success or a PEAR_Error object on failure.
     */
    function removeUser($userID)
    {
        return PEAR::raiseError('unsupported');
    }

    /**
     * List all users in the system.
     *
     * @access public
     *
     * @return mixed  The array of userIDs, or a PEAR_Error object on failure.
     */
    function listUsers()
    {
        return PEAR::raiseError('unsupported');
    }

    /**
     * Checks if a userID exists in the sistem.
     *
     * @access public
     *
     * @return boolean  Whether or not the userID already exists.
     */
    function exists($userID)
    {
        return PEAR::raiseError('unsupported');
    }

    /**
     * Automatic authentication: Find out if the client matches an
     * allowed IP block.
     *
     * @access public
     *
     * @return boolean  Whether or not the client is allowed.
     */
    function transparent()
    {
        return false;
    }

    /**
     * Checks if there is a session with valid auth information. for
     * the specified user. If there isn't, but the configured Auth
     * driver supports transparent authentication, then we try that.
     *
     * @access public
     *
     * @param optional string $realm   The authentication realm to check.
     *
     * @return boolean  Whether or not the user is authenticated.
     */
    function isAuthenticated($realm = null)
    {
        if (isset($_SESSION['__auth'])) {
            if (!empty($_SESSION['__auth']['authenticated']) &&
                !empty($_SESSION['__auth']['userID']) &&
                $_SESSION['__auth']['realm'] == $realm) {
                return true;
            }
        }

        // Try transparent authentication now.
        global $conf;
        $auth = &Auth::singleton($conf['auth']['driver']);
        if ($auth->hasCapability('transparent') &&
            $auth->transparent()) {
            return Auth::isAuthenticated($realm);
        }

        return false;
    }

    /**
     * Return the currently logged in user, if there is one.
     *
     * @access public
     *
     * @return mixed  The userID of the current user, or false if no user is
     *                logged in.
     */
    function getAuth($realm = null)
    {
        if (isset($_SESSION['__auth'])) {
            if (!empty($_SESSION['__auth']['authenticated']) &&
                !empty($_SESSION['__auth']['userID'])) {
                return $_SESSION['__auth']['userID'];
            }
        }

        return false;
    }

    /**
     * Return the curently logged-in user without any domain
     * information (e.g., bob@example.com would be returned as 'bob').
     *
     * @access public
     *
     * @param optional string $realm   The authentication realm to check.
     *
     * @return mixed  The userID of the current user, or false if no user is
     *                logged in.
     */
    function getBareAuth($realm = null)
    {
        $user = Auth::getAuth($realm = null);
        if ($user) {
            $pos = strpos($user, '@');
            if ($pos !== false) {
                $user = substr($user, 0, $pos);
            }
        }

        return $user;
    }

    /**
     * Return a credential of the currently logged in user, if there is one.
     *
     * @access public
     *
     * @param          string $credential  The credential to retrieve.
     *
     * @return mixed  The requested credential, or false if no user is logged
     *                in.
     */
    function getCredential($credential)
    {
        require_once HORDE_BASE . '/lib/Secret.php';

        if (!empty($_SESSION['__auth']) &&
            !empty($_SESSION['__auth']['authenticated'])) {
            $credentials = @unserialize(Secret::read(Secret::getKey('auth'), $_SESSION['__auth']['credentials']));
        } else {
            return false;
        }

        if (is_array($credentials) &&
            isset($credentials[$credential])) {
            return $credentials[$credential];
        } else {
            return false;
        }
    }

    /**
     * Set a variable in the session saying that authorization has
     * succeeded, note which userID was authorized, and note when the
     * login took place.
     *
     * If a user name hook was defined in the configuration, it gets
     * applied to the userID at this point.
     *
     * @access public
     *
     * @param          string $userID       The userID who has been authorized.
     * @param          array  $credentials  The credentials of the user.
     * @param optional string $realm        The authentication realm to use.
     */
    function setAuth($userID, $credentials, $realm = null)
    {
        global $registry;

        $userID = trim($userID);
        $userID = Auth::addHook($userID);

        require_once HORDE_BASE . '/lib/Secret.php';
        $credentials = Secret::write(Secret::getKey('auth'), serialize($credentials));

        if (!empty($realm)) {
            $userID .= '@' . $realm;
        }
        $_SESSION['__auth'] = array('authenticated' => true,
                                    'userID' => $userID,
                                    'credentials' => $credentials,
                                    'realm' => $realm,
                                    'timestamp' => time(),
                                    'remote_addr' => $_SERVER['REMOTE_ADDR']);

        // Reload preferences for the new user.
        $registry->loadPrefs();
        global $prefs;

        /* Display user's last login time if requested. */
        $old_login = @unserialize($prefs->getValue('last_login'));
        if ($prefs->getValue('show_last_login')) {
            global $notification;
            if (empty($old_login['time'])) {
                $notification->push(_("Last login: Never"), 'horde.message');
            } else {
                if (empty($old_login['host'])) {
                    $notification->push(sprintf(_("Last login: %s"), strftime('%c', $old_login['time'])), 'horde.message');
                } else {
                    $notification->push(sprintf(_("Last login: %s from %s"), strftime('%c', $old_login['time']), $old_login['host']), 'horde.message');
                }
            }
        }
        if (!empty($old_login['time'])) {
            $_SESSION['__auth']['last_login'] = $old_login['time'];
        }

        // Set the user's last_login information.
        $last_login['time'] = time();
        $last_login['host'] = @gethostbyaddr($_SERVER['REMOTE_ADDR']);
        $prefs->setValue('last_login', serialize($last_login));
    }

    /**
     * Clear any authentication tokens in the current session.
     *
     * @access public
     *
     * @param optional string $realm  The authentication realm to clear.
     */
    function clearAuth($realm = null)
    {
        if (!empty($realm) && isset($_SESSION['__auth'][$realm])) {
            $_SESSION['__auth'][$realm] = array();
            $_SESSION['__auth'][$realm]['authenticated'] = false;
        } elseif (isset($_SESSION['__auth'])) {
            $_SESSION['__auth'] = array();
            $_SESSION['__auth']['authenticated'] = false;
        }
    }

    /**
     * Authentication stub.
     *
     * @access private
     *
     * @return boolean  False.
     */
    function _authenticate()
    {
        return false;
    }

    /**
     * Is the current user an administrator?
     *
     * @access public
     *
     * @param string $permission  (optional) Allow users with this permission
     *                            admin access in the current context.
     * @param integer $permlevel  (optional) The level of permissions to check for
     *                            (_PERMS_EDIT, _PERMS_DELETE, etc). Defaults
     *                            to _PERMS_EDIT.
     *
     * @return boolean  Whether or not this is an admin user.
     *
     * @since Horde 2.2
     */
    function isAdmin($permission = null, $permlevel = null)
    {
        global $conf;

        if (@is_array($conf['auth']['admins'])) {
            if (Auth::getAuth() &&
                in_array(Auth::getAuth(), $conf['auth']['admins'])) {
                return true;
            }
        }

        if (!is_null($permission)) {
            require_once HORDE_BASE . '/lib/Perms.php';
            $perms = &Perms::singleton();
            if (is_null($permlevel)) {
                $permlevel = _PERMS_EDIT;
            }
            return $perms->hasPermission($permission, Auth::getAuth(), $permlevel);
        }

        return false;
    }

    /**
     * Applies a hook defined by the function _username_hook_frombackend() to the
     * given user name if this function exist and user hooks are enabled.
     *
     * This method should be called if a backend's user name needs to be
     * converted to a (unique) Horde user name.
     *
     * @param string userID  The original user name
     *
     * @return string  The user name with the hook applied to it.
     */
    function addHook($userID)
    {
        global $conf;

        if (!empty($conf['hooks']['username'])) {
            require_once HORDE_BASE . '/config/hooks.php';
            if (function_exists('_username_hook_frombackend')) {
                $userID = call_user_func('_username_hook_frombackend', $userID);
            }
        }

        return $userID;
    }

    /**
     * Applies a hook defined by the function _username_hook_tobackend() to the
     * given user name if this function exist and user hooks are enabled.
     *
     * This method should be called if a Horde user name needs to be converted
     * to a backend's user name or displayed to the user.
     *
     * @param string userID  The Horde user name
     *
     * @return string  The user name with the hook applied to it.
     */
    function removeHook($userID)
    {
        global $conf;

        if (!empty($conf['hooks']['username'])) {
            require_once HORDE_BASE . '/config/hooks.php';
            if (function_exists('_username_hook_tobackend')) {
                $userID = call_user_func('_username_hook_tobackend', $userID);
            }
        }

        return $userID;
    }

    /**
     * Query the current Auth object to find out if it supports the
     * given capability.
     *
     * @access public
     *
     * @param string $capability  The capability to test for.
     * @return boolean            Whether or not the capability is supported.
     */
    function hasCapability($capability)
    {
        return !empty($this->capabilities[$capability]);
    }

    /**
     * Return the URI of the login screen for this authentication
     * method.
     *
     * @access public
     *
     * @param optional string $app  The application to use.
     * @param optional string $url  The URL to redirect to after login.
     *
     * @return string  The login screen URI.
     */
    function getLoginScreen($app = 'horde', $url = '')
    {
        $login = Horde::url($GLOBALS['registry']->getParam('webroot', $app) . '/login.php', true);
        if (!empty($url)) {
            $login = Horde::addParameter($login, 'url', $url);
        }
        return $login;
    }

    /**
     * TODO
     *
     * @access public
     *
     * @param optional string $driver  TODO
     * @param optional array $params   TODO
     *
     * @return string  TODO
     */
    function getProvider($driver = null, $params = null)
    {
        global $conf;

        if (is_null($driver)) {
            $driver = $conf['auth']['driver'];
        }
        if (is_null($params)) {
            $params = Horde::getDriverConfig('auth',
                is_array($driver) ? $driver[1] : $driver);
        }

        if ($driver == 'application') {
            return array_key_exists('app', $params) ? $params['app'] : 'application';
        } elseif ($driver == 'composite') {
            if (($login_driver = Auth::_getDriverByParam('loginscreen_switch', $params)) &&
                !empty($params['drivers'][$login_driver])) {
                return Auth::getProvider($params['drivers'][$login_driver]['driver'],
                                         isset($params['drivers'][$login_driver]['params']) ? $params['drivers'][$login_driver]['params'] : null);
            }
            return 'composite';
        } else {
            return $driver;
        }
    }

    /**
     * Sets the error message for an invalid authentication.
     *
     * @access private
     *
     * @param optional string $error  The error message/reason for invalid
     *                                authentication.
     */
    function _setAuthError($error = null)
    {
        if (is_null($error)) {
            $error = (_("Username and/or password not correct."));
        }

        $this->_authError = $error;
    }

    /**
     * Gets the error message for an invalid authentication.
     *
     * @access public
     *
     * @return string  The error message/reason for invalid authentication.
     */
    function getAuthError()
    {
        return (_("Authentication failed")) . ': ' . $this->_authError;
    }

    /**
     * Return the appropriate authentication driver, if any, selecting
     * by the specified parameter.
     *
     * @access private
     *
     * @param          string $name          The parameter name.
     * @param          array  $params        The parameter list.
     * @param optional string $driverparams  A list of parameters to pass to
     *                                       the driver.
     */
    function _getDriverByParam($name, $params, $driverparams = array())
    {
        if (array_key_exists($name, $params) &&
            function_exists($params[$name])) {
            return call_user_func_array($params[$name], $driverparams);
        }

        return null;
    }

}
