* @category Horde
* @license http://opensource.org/licenses/lgpl-2.1.php LGPL
* @package Core
*/
class Horde_Core_Auth_Application extends Horde_Auth_Base
{
/**
* Authentication failure reasons (additions to Horde_Auth:: reasons):
* - REASON_BROWSER: A browser change was detected
* - REASON_SESSIONIP: Logout due to change of IP address during session
* - REASON_SESSIONMAXTIME: Logout due to the session exceeding the
* maximum allowed length.
*/
const REASON_BROWSER = 100;
const REASON_SESSIONIP = 101;
const REASON_SESSIONMAXTIME = 102;
/**
* Application for authentication.
*
* @var string
*/
protected $_app = 'horde';
/**
* The list of application capabilities.
*
* @var array
*/
protected $_appCapabilities;
/**
* The base auth driver, used for Horde authentication.
*
* @var Horde_Auth_Base
*/
protected $_base;
/**
* The view mode.
*
* @var string
*/
protected $_view = 'auto';
/**
* Available capabilities.
*
* @var array
*/
protected $_capabilities = array(
'add' => true,
'authenticate' => true,
'exists' => true,
'list' => true,
'remove' => true,
'resetpassword' => true,
'transparent' => true,
'update' => true,
'validate' => true
);
/**
* Constructor.
*
* @param array $params Required parameters:
* - app: (string) The application which is providing authentication.
* - base: (Horde_Auth_Base) The base Horde_Auth driver. Only needed if
* 'app' is 'horde'.
*
* @throws InvalidArgumentException
*/
public function __construct(array $params = array())
{
if (!isset($params['app'])) {
throw new InvalidArgumentException('Missing app parameter.');
}
$this->_app = $params['app'];
unset($params['app']);
if ($this->_app == 'horde') {
if (!isset($params['base'])) {
throw new InvalidArgumentException('Missing base parameter.');
}
$this->_base = $params['base'];
unset($params['base']);
}
parent::__construct($params);
}
/**
* Finds out if a set of login credentials are valid, and if requested,
* mark the user as logged in in the current session.
*
* @param string $userId The user ID to check.
* @param array $credentials The credentials to check.
* @param 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.
*
* @return boolean Whether or not the credentials are valid.
*/
public function authenticate($userId, $credentials, $login = true)
{
if (!strlen($credentials['password'])) {
return false;
}
try {
list($userId, $credentials) = $this->runHook(trim($userId), $credentials, 'preauthenticate', 'authenticate');
} catch (Horde_Auth_Exception $e) {
return false;
}
if ($this->_base) {
if (!$this->_base->authenticate($userId, $credentials, $login)) {
return false;
}
} elseif (!parent::authenticate($userId, $credentials, $login)) {
return false;
}
/* Remember the user's mode choice, if applicable. */
if (!empty($credentials['mode'])) {
$this->_view = $credentials['mode'];
}
return $this->_setAuth();
}
/**
* Find out if a set of login credentials are valid.
*
* @param string $userId The user ID to check.
* @param array $credentials The credentials to use. This object will
* always be available in the 'auth_ob' key.
*
* @throws Horde_Auth_Exception
*/
protected function _authenticate($userId, $credentials)
{
if (!$this->hasCapability('authenticate')) {
throw new Horde_Auth_Exception($this->_app . ' does not provide an authenticate() method.');
}
$credentials['auth_ob'] = $this;
$GLOBALS['registry']->callAppMethod($this->_app, 'authAuthenticate', array('args' => array($userId, $credentials), 'noperms' => true));
}
/**
* Checks for triggers that may invalidate the current auth.
* These triggers are independent of the credentials.
*
* @return boolean True if the results of authenticate() are still valid.
*/
public function validateAuth()
{
if ($this->_base) {
return $this->_base->validateAuth();
}
try {
return $this->hasCapability('validate')
? $GLOBALS['registry']->callAppMethod($this->_app, 'authValidate', array('noperms' => true))
: parent::validateAuth();
} catch (Horde_Exception_AuthenticationFailure $e) {
return false;
}
}
/**
* Add a set of authentication credentials.
*
* @param string $userId The user ID to add.
* @param array $credentials The credentials to use.
*
* @throws Horde_Auth_Exception
*/
public function addUser($userId, $credentials)
{
if ($this->_base) {
$this->_base->addUser($userId, $credentials);
return;
}
if ($this->hasCapability('add')) {
$GLOBALS['registry']->callAppMethod($this->_app, 'authAddUser', array('args' => array($userId, $credentials)));
} else {
parent::addUser($userId, $credentials);
}
}
/**
* Locks a user indefinitely or for a specified time
*
* @param string $userId The userId to lock.
* @param integer $time The duration in seconds, 0 = permanent
*
* @throws Horde_Auth_Exception
*/
public function lockUser($userId, $time = 0)
{
if ($this->_base) {
$this->_base->lockUser($userId, $time);
return;
}
if ($this->hasCapability('lock')) {
parent::lockUser($userId, $time);
}
}
/**
* Unlocks a user and optionally resets bad login count
*
* @param string $userId The userId to unlock.
* @param boolean $resetBadLogins Reset bad login counter, default no.
*
* @throws Horde_Auth_Exception
*/
public function unlockUser($userId, $resetBadLogins = false)
{
if ($this->_base) {
$this->_base->unlockUser($userId, $resetBadLogins);
return;
}
if ($this->hasCapability('lock')) {
parent::unlockUser($userId, $resetBadLogins);
}
}
/**
* Checks if $userId is currently locked.
*
* @param string $userId The userId to check.
* @param boolean $show_details Toggle array format with timeout.
*
* @throws Horde_Auth_Exception
*/
public function isLocked($userId, $show_details = false)
{
if ($this->_base) {
return $this->_base->isLocked($userId, $show_details);
}
if ($this->hasCapability('lock')) {
return parent::isLocked($userId, $show_details);
}
}
/**
* Update a set of authentication credentials.
*
* @param string $oldID The old user ID.
* @param string $newID The new user ID.
* @param array $credentials The new credentials
*
* @throws Horde_Auth_Exception
*/
public function updateUser($oldID, $newID, $credentials)
{
if ($this->_base) {
$this->_base->updateUser($oldID, $newID, $credentials);
return;
}
if ($this->hasCapability('update')) {
$GLOBALS['registry']->callAppMethod($this->_app, 'authUpdateUser', array('args' => array($oldID, $newID, $credentials)));
} else {
parent::updateUser($oldID, $newID, $credentials);
}
}
/**
* Delete a set of authentication credentials.
*
* @param string $userId The user ID to delete.
*
* @throws Horde_Auth_Exception
*/
public function removeUser($userId)
{
if ($this->_base) {
$this->_base->removeUser($userId);
} else {
if ($this->hasCapability('remove')) {
$GLOBALS['registry']->callAppMethod($this->_app, 'authRemoveUser', array('args' => array($userId)));
} else {
parent::removeUser($userId);
}
}
}
/**
* List all users in the system.
*
* @return array The array of user IDs.
* @throws Horde_Auth_Exception
*/
public function listUsers($sort = false)
{
if ($this->_base) {
return $this->_base->listUsers($sort);
}
return $this->hasCapability('list')
? $GLOBALS['registry']->callAppMethod($this->_app, 'authUserList')
: parent::listUsers($sort);
}
/**
* List all users in the system with their real names.
*
* @since Horde_Core 2.23.0
*
* @return array The array of user IDs as keys and names as values.
* @throws Horde_Auth_Exception
*/
public function listNames()
{
$factory = $GLOBALS['injector']
->getInstance('Horde_Core_Factory_Identity');
$names = array();
foreach ($this->listUsers() as $user) {
$names[$user] = $factory->create($user)->getName();
}
asort($names);
return $names;
}
/**
* Checks if a user ID exists in the system.
*
* @param string $userId User ID to check.
*
* @return boolean Whether or not the user ID already exists.
*/
public function exists($userId)
{
if ($this->_base) {
return $this->_base->exists($userId);
}
return $this->hasCapability('exists')
? $GLOBALS['registry']->callAppMethod($this->_app, 'authUserExists', array('args' => array($userId)))
: parent::exists($userId);
}
/**
* Automatic authentication.
*
* @return boolean Whether or not the client is allowed.
* @throws Horde_Auth_Exception
*/
public function transparent()
{
global $registry;
if (!($userId = $this->getCredential('userId'))) {
$userId = $registry->getAuth();
}
if (!($credentials = $this->getCredential('credentials'))) {
$credentials = $registry->getAuthCredential();
}
list($userId, $credentials) = $this->runHook($userId, $credentials, 'preauthenticate', 'transparent');
$this->setCredential('userId', $userId);
$this->setCredential('credentials', $credentials);
if ($this->_base) {
$result = $this->_base->transparent();
} elseif ($this->hasCapability('transparent')) {
$result = $registry->callAppMethod($this->_app, 'authTransparent', array('args' => array($this), 'noperms' => true));
} else {
/* If this application contains neither transparent nor
* authenticate capabilities, it does not require any
* authentication if already authenticated to Horde. */
$result = ($registry->getAuth() && !$this->hasCapability('authenticate'));
}
return $result && $this->_setAuth();
}
/**
* Reset a user's password. Used for example when the user does not
* remember the existing password.
*
* @param string $userId The user ID for which to reset the password.
*
* @return string The new password on success.
* @throws Horde_Auth_Exception
*/
public function resetPassword($userId)
{
if ($this->_base) {
return $this->_base->resetPassword($userId);
}
return $this->hasCapability('resetpassword')
? $GLOBALS['registry']->callAppMethod($this->_app, 'authResetPassword', array('args' => array($userId)))
: parent::resetPassword();
}
/**
* Queries the current driver to find out if it supports the given
* capability.
*
* @param string $capability The capability to test for.
*
* @return boolean Whether or not the capability is supported.
*/
public function hasCapability($capability)
{
if ($this->_base) {
return $this->_base->hasCapability($capability);
}
// The follow capabilities are not determined by the Application,
// but by 'Horde'.
if (in_array(Horde_String::lower($capability), array('badlogincount', 'lock'))) {
return parent::hasCapability($capability);
} elseif (!isset($this->_appCapabilities)) {
$this->_appCapabilities = $GLOBALS['registry']->getApiInstance($this->_app, 'application')->auth;
}
return in_array(Horde_String::lower($capability), $this->_appCapabilities);
}
/**
* Returns the named parameter for the current auth driver.
*
* @param string $param The parameter to fetch.
*
* @return string The parameter's value, or null if it doesn't exist.
*/
public function getParam($param)
{
return $this->_base
? $this->_base->getParam($param)
: parent::getParam($param);
}
/**
* Retrieve internal credential value(s).
*
* @param mixed $name The credential value to get. If null, will return
* the entire credential list. Valid names:
* - change: (boolean) Do credentials need to be changed?
* - credentials: (array) The credentials needed to authenticate.
* - expire: (integer) UNIX timestamp of the credential expiration date.
* - userId: (string) The user ID.
*
* @return mixed Return the credential information, or null if the
* credential doesn't exist.
*/
public function getCredential($name = null)
{
return $this->_base
? $this->_base->getCredential($name)
: parent::getCredential($name);
}
/**
* Set internal credential value.
*
* @param string $name The credential name to set.
* @param mixed $value The credential value to set. See getCredential()
* for the list of valid credentials/types.
*/
public function setCredential($name, $value)
{
if ($this->_base) {
$this->_base->setCredential($name, $value);
} else {
parent::setCredential($name, $value);
}
}
/**
* Sets the error message for an invalid authentication.
*
* @param string $type The type of error (Horde_Auth::REASON_* constant).
* @param string $msg The error message/reason for invalid
* authentication.
*/
public function setError($type, $msg = null)
{
if ($this->_base) {
$this->_base->setError($type, $msg);
} else {
parent::setError($type, $msg);
}
}
/**
* Returns the error type or message for an invalid authentication.
*
* @param boolean $msg If true, returns the message string (if set).
*
* @return mixed Error type, error message (if $msg is true) or false
* if entry doesn't exist.
*/
public function getError($msg = false)
{
return $this->_base
? $this->_base->getError($msg)
: parent::getError($msg);
}
/**
* Returns information on what login parameters to display on the login
* screen.
*
* @return array An array with the following keys:
*
* 'js_code' - (array) A list of javascript statements to be included.
* 'js_files' - (array) A list of javascript files to be included.
* 'params' - (array) A list of parameters to display on the login screen.
* Each entry is an array with the following entries:
* 'label' - (string) The label of the entry.
* 'type' - (string) 'select', 'text', or 'password'.
* 'value' - (mixed) If type is 'text' or 'password', the
* text to insert into the field by default. If type
* is 'select', an array with they keys as the
* option values and an array with the following keys:
* 'hidden' - (boolean) If true, the option will be
* hidden.
* 'name' - (string) The option label.
* 'selected' - (boolean) If true, will be selected
* by default.
*
*
* @throws Horde_Exception
*/
public function getLoginParams()
{
return ($this->_base && method_exists($this->_base, 'getLoginParams'))
? $this->_base->getLoginParams()
: $GLOBALS['registry']->callAppMethod($this->_app, 'authLoginParams', array('noperms' => true));
}
/**
* Indicate whether the application requires authentication.
*
* @return boolean True if application requires authentication.
*/
public function requireAuth()
{
return !$this->_base &&
($this->hasCapability('authenticate') ||
$this->hasCapability('transparent'));
}
/**
* Runs the pre/post-authenticate hook and parses the result.
*
* @param string $userId The userId who has been authorized.
* @param array $credentials The credentials of the user.
* @param string $type Either 'preauthenticate' or
* 'postauthenticate'.
* @param string $method The triggering method (preauthenticate only).
* Either 'authenticate' or 'transparent'.
*
* @return array Two element array, $userId and $credentials.
* @throws Horde_Auth_Exception
*/
public function runHook($userId, $credentials, $type, $method = null)
{
if (!is_array($credentials)) {
$credentials = empty($credentials)
? array()
: array($credentials);
}
$ret_array = array($userId, $credentials);
if ($type == 'preauthenticate') {
$credentials['authMethod'] = $method;
}
try {
$result = $GLOBALS['injector']->getInstance('Horde_Core_Hooks')
->callHook($type, $this->_app, array($userId, $credentials));
} catch (Horde_Exception_HookNotSet $e) {
return $ret_array;
} catch (Horde_Exception $e) {
throw new Horde_Auth_Exception($e);
}
unset($credentials['authMethod']);
if ($result === false) {
if ($this->getError() != Horde_Auth::REASON_MESSAGE) {
$this->setError(Horde_Auth::REASON_FAILED);
}
throw new Horde_Auth_Exception($type . ' hook failed');
}
if (is_array($result)) {
if ($type == 'postauthenticate') {
$ret_array[1] = $result;
} else {
if (isset($result['userId'])) {
$ret_array[0] = $result['userId'];
}
if (isset($result['credentials'])) {
$ret_array[1] = $result['credentials'];
}
}
}
return $ret_array;
}
/**
* Set authentication credentials in the Horde session.
*
* @return boolean True on success, false on failure.
*/
protected function _setAuth()
{
global $registry;
if ($registry->isAuthenticated(array('app' => $this->_app, 'notransparent' => true))) {
return true;
}
/* Grab the current language before we destroy the session. */
$language = $registry->preferredLang();
/* Destroy any existing session on login and make sure to use a
* new session ID, to avoid session fixation issues. */
if (($userId = $registry->getAuth()) === false) {
$GLOBALS['session']->clean();
$userId = $this->getCredential('userId');
}
$credentials = $this->getCredential('credentials');
try {
list(,$credentials) = $this->runHook($userId, $credentials, 'postauthenticate');
} catch (Horde_Auth_Exception $e) {
return false;
}
$registry->setAuth($userId, $credentials, array(
'app' => $this->_app,
'change' => $this->getCredential('change'),
'language' => $language
));
/* Only set the view mode on initial authentication */
if (!$GLOBALS['session']->exists('horde', 'view')) {
$this->_setView();
}
if ($this->_base &&
isset($GLOBALS['notification']) &&
($expire = $this->_base->getCredential('expire'))) {
$toexpire = ($expire - time()) / 86400;
$GLOBALS['notification']->push(sprintf(Horde_Core_Translation::ngettext("%d day until your password expires.", "%d days until your password expires.", $toexpire), $toexpire), 'horde.warning');
}
return true;
}
/**
* Sets the default global view mode in the horde session. This can be
* checked by applications, and overridden if desired. Also sets a cookie
* to remember the last view selection if applicable.
*/
protected function _setView()
{
global $conf, $browser, $notification, $registry;
$mode = $this->_view;
if (empty($conf['user']['force_view'])) {
if (empty($conf['user']['select_view'])) {
$mode = 'auto';
} else {
/* 'auto' is default, so don't store in cookie. */
setcookie(
'default_horde_view',
($mode == 'auto') ? '' : $mode,
time() + (($mode == 'auto') ? -3600 : (30 * 86400)),
$conf['cookie']['path'],
$conf['cookie']['domain']
);
}
} else {
// Forcing mode as per config.
$mode = $conf['user']['force_view'];
}
/* $mode now contains the user's preference for view based on the
* login screen parameters and configuration. */
switch ($mode) {
case 'auto':
if ($browser->hasFeature('ajax')) {
$mode = $browser->isMobile()
? 'smartmobile'
: 'dynamic';
} else {
$mode = $browser->isMobile()
? 'mobile'
: 'basic';
}
break;
case 'basic':
if (!$browser->hasFeature('javascript')) {
$notification->push(Horde_Core_Translation::t("Your browser does not support javascript. Using minimal view instead."), 'horde.warning');
$mode = 'mobile';
}
break;
case 'dynamic':
if (!$browser->hasFeature('ajax')) {
if ($browser->hasFeature('javascript')) {
$notification->push(Horde_Core_Translation::t("Your browser does not support the dynamic view. Using basic view instead."), 'horde.warning');
$mode = 'basic';
} else {
$notification->push(Horde_Core_Translation::t("Your browser does not support the dynamic view. Using minimal view instead."), 'horde.warning');
$mode = 'mobile';
}
}
break;
case 'smartmobile':
if (!$browser->hasFeature('ajax')) {
$notification->push(Horde_Core_Translation::t("Your browser does not support the dynamic view. Using minimal view instead."), 'horde.warning');
$mode = 'mobile';
}
break;
case 'mobile':
default:
$mode = 'mobile';
break;
}
if (($browser->getBrowser() == 'msie') &&
($browser->getMajor() < 8) &&
($mode != 'mobile')) {
$notification->push(Horde_Core_Translation::t("You are using an old, unsupported version of Internet Explorer. You need at least Internet Explorer 8. If you already run IE8 or higher, disable the Compatibility View. Minimal view will be used until you upgrade your browser."));
$mode = 'mobile';
}
$registry_map = array(
'basic' => Horde_Registry::VIEW_BASIC,
'dynamic' => Horde_Registry::VIEW_DYNAMIC,
'mobile' => Horde_Registry::VIEW_MINIMAL,
'smartmobile' => Horde_Registry::VIEW_SMARTMOBILE
);
$this->_view = $mode;
$registry->setView($registry_map[$mode]);
}
}