/**
* Class LoginWebPage
*
* @copyright Copyright (C) 2010-2017 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
require_once(APPROOT."/application/nicewebpage.class.inc.php");
require_once(APPROOT.'/application/portaldispatcher.class.inc.php');
/**
* Web page used for displaying the login form
*/
class LoginWebPage extends NiceWebPage
{
const EXIT_PROMPT = 0;
const EXIT_HTTP_401 = 1;
const EXIT_RETURN = 2;
const EXIT_CODE_OK = 0;
const EXIT_CODE_MISSINGLOGIN = 1;
const EXIT_CODE_MISSINGPASSWORD = 2;
const EXIT_CODE_WRONGCREDENTIALS = 3;
const EXIT_CODE_MUSTBEADMIN = 4;
const EXIT_CODE_PORTALUSERNOTAUTHORIZED = 5;
protected static $sHandlerClass = __class__;
public static function RegisterHandler($sClass)
{
self::$sHandlerClass = $sClass;
}
public static function NewLoginWebPage()
{
return new self::$sHandlerClass;
}
protected static $m_sLoginFailedMessage = '';
public function __construct($sTitle = 'iTop Login')
{
parent::__construct($sTitle);
$this->SetStyleSheet();
$this->add_header("Cache-control: no-cache");
}
public function SetStyleSheet()
{
$this->add_linked_stylesheet(utils::GetAbsoluteUrlAppRoot().'css/login.css');
}
public static function SetLoginFailedMessage($sMessage)
{
self::$m_sLoginFailedMessage = $sMessage;
}
public function EnableResetPassword()
{
return MetaModel::GetConfig()->Get('forgot_password');
}
public function DisplayLoginHeader($bMainAppLogo = false)
{
if ($bMainAppLogo)
{
$sLogo = 'itop-logo.png';
$sBrandingLogo = 'main-logo.png';
}
else
{
$sLogo = 'itop-logo-external.png';
$sBrandingLogo = 'login-logo.png';
}
$sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION);
$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
$sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/'.$sLogo.'?itopversion='.ITOP_VERSION;
if (file_exists(MODULESROOT.'branding/'.$sBrandingLogo))
{
$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/'.$sBrandingLogo.'?itopversion='.ITOP_VERSION;
}
$this->add("

\n");
}
public function DisplayLoginForm($sLoginType, $bFailedLogin = false)
{
switch($sLoginType)
{
case 'cas':
utils::InitCASClient();
// force CAS authentication
phpCAS::forceAuthentication(); // Will redirect the user and exit since the user is not yet authenticated
break;
case 'basic':
case 'url':
$this->add_header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION));
$this->add_header('HTTP/1.0 401 Unauthorized');
$this->add_header('Content-type: text/html; charset=iso-8859-1');
// Note: displayed when the user will click on Cancel
$this->add(''.Dict::S('UI:Login:Error:AccessRestricted').'
');
break;
case 'external':
case 'form':
default: // In case the settings get messed up...
$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
$sAuthPwd = utils::ReadParam('suggest_pwd', '', true, 'raw_data');
$this->DisplayLoginHeader();
$this->add("\n");
$this->add("
".Dict::S('UI:Login:Welcome')."
\n");
if ($bFailedLogin)
{
if (self::$m_sLoginFailedMessage != '')
{
$this->add("
".self::$m_sLoginFailedMessage."
\n");
}
else
{
$this->add("
".Dict::S('UI:Login:IncorrectLoginPassword')."
\n");
}
}
else
{
$this->add("
".Dict::S('UI:Login:IdentifyYourself')."
\n");
}
$this->add("
\n");
$this->add(Dict::S('UI:Login:About'));
$this->add("
\n");
break;
}
}
/**
* Return '' to disable this feature
*/
public function ForgotPwdLink()
{
$sUrl = '?loginop=forgot_pwd';
$sHtml = "".Dict::S('UI:Login:ForgotPwd')."";
return $sHtml;
}
public function DisplayForgotPwdForm($bFailedToReset = false, $sFailureReason = null)
{
$this->DisplayLoginHeader();
$this->add("\n");
$this->add("
".Dict::S('UI:Login:ForgotPwdForm')."
\n");
$this->add("
".Dict::S('UI:Login:ForgotPwdForm+')."
\n");
if ($bFailedToReset)
{
$this->add("
".Dict::Format('UI:Login:ResetPwdFailed', htmlentities($sFailureReason, ENT_QUOTES, 'UTF-8'))."
\n");
}
$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
$this->add("
\n");
$this->add("
\n");
$this->add_ready_script('$("#user").focus();');
}
protected function ForgotPwdGo()
{
$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
try
{
UserRights::Login($sAuthUser); // Set the user's language (if possible!)
$oUser = UserRights::GetUserObject();
if ($oUser == null)
{
throw new Exception(Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser));
}
if (!MetaModel::IsValidAttCode(get_class($oUser), 'reset_pwd_token'))
{
throw new Exception(Dict::S('UI:ResetPwd-Error-NotPossible'));
}
if (!$oUser->CanChangePassword())
{
throw new Exception(Dict::S('UI:ResetPwd-Error-FixedPwd'));
}
$sTo = $oUser->GetResetPasswordEmail(); // throws Exceptions if not allowed
if ($sTo == '')
{
throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmail'));
}
// This token allows the user to change the password without knowing the previous one
$sToken = substr(md5(APPROOT.uniqid()), 0, 16);
$oUser->Set('reset_pwd_token', $sToken);
CMDBObject::SetTrackInfo('Reset password');
$oUser->DBUpdate();
$oEmail = new Email();
$oEmail->SetRecipientTO($sTo);
$sFrom = MetaModel::GetConfig()->Get('forgot_password_from');
if ($sFrom == '')
{
$sFrom = $sTo;
}
$oEmail->SetRecipientFrom($sFrom);
$oEmail->SetSubject(Dict::S('UI:ResetPwd-EmailSubject'));
$sResetUrl = utils::GetAbsoluteUrlAppRoot().'pages/UI.php?loginop=reset_pwd&auth_user='.urlencode($oUser->Get('login')).'&token='.urlencode($sToken);
$oEmail->SetBody(Dict::Format('UI:ResetPwd-EmailBody', $sResetUrl));
$iRes = $oEmail->Send($aIssues, true /* force synchronous exec */);
switch ($iRes)
{
//case EMAIL_SEND_PENDING:
case EMAIL_SEND_OK:
break;
case EMAIL_SEND_ERROR:
default:
IssueLog::Error('Failed to send the email with the NEW password for '.$oUser->Get('friendlyname').': '.implode(', ', $aIssues));
throw new Exception(Dict::S('UI:ResetPwd-Error-Send'));
}
$this->DisplayLoginHeader();
$this->add("\n");
$this->add("
".Dict::S('UI:Login:ForgotPwdForm')."
\n");
$this->add("
".Dict::S('UI:ResetPwd-EmailSent')."
");
$this->add("
\n");
$this->add("
DisplayForgotPwdForm(true, $e->getMessage());
}
}
public function DisplayResetPwdForm()
{
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sToken = utils::ReadParam('token', '', false, 'raw_data');
UserRights::Login($sAuthUser); // Set the user's language
$oUser = UserRights::GetUserObject();
$this->DisplayLoginHeader();
$this->add("\n");
$this->add("
".Dict::S('UI:ResetPwd-Title')."
\n");
if ($oUser == null)
{
$this->add("
".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."
\n");
}
else
{
$oEncryptedToken = $oUser->Get('reset_pwd_token');
if (!$oEncryptedToken->CheckPassword($sToken))
{
$this->add("
".Dict::S('UI:ResetPwd-Error-InvalidToken')."
\n");
}
else
{
$this->add("
".Dict::Format('UI:ResetPwd-Error-EnterPassword', $oUser->GetFriendlyName())."
\n");
$sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch');
$this->add_script(
<<
add("\n");
$this->add(" DisplayLoginHeader();
$this->add("\n");
$this->add("
".Dict::S('UI:ResetPwd-Title')."
\n");
if ($oUser == null)
{
$this->add("
".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."
\n");
}
else
{
$oEncryptedPassword = $oUser->Get('reset_pwd_token');
if (!$oEncryptedPassword->CheckPassword($sToken))
{
$this->add("
".Dict::S('UI:ResetPwd-Error-InvalidToken')."
\n");
}
else
{
// Trash the token and change the password
$oUser->Set('reset_pwd_token', '');
$oUser->SetPassword($sNewPwd); // Does record the change into the DB
$this->add("
".Dict::S('UI:ResetPwd-Ready')."
");
$sUrl = utils::GetAbsoluteUrlAppRoot();
$this->add("
".Dict::S('UI:ResetPwd-Login')."
");
}
$this->add("
add_script(<<DisplayLoginHeader();
$this->add("\n");
$this->add("
".Dict::S('UI:Login:ChangeYourPassword')."
\n");
if ($bFailedLogin)
{
$this->add("
".Dict::S('UI:Login:IncorrectOldPassword')."
\n");
}
$this->add("
\n");
$this->add("
\n");
}
static function ResetSession()
{
// Unset all of the session variables.
unset($_SESSION['auth_user']);
unset($_SESSION['login_mode']);
unset($_SESSION['archive_mode']);
unset($_SESSION['archive_allowed']);
UserRights::_ResetSessionCache();
// If it's desired to kill the session, also delete the session cookie.
// Note: This will destroy the session, and not just the session data!
}
static function SecureConnectionRequired()
{
return MetaModel::GetConfig()->GetSecureConnectionRequired();
}
/**
* Guess if a string looks like an UTF-8 string based on some ranges of multi-bytes encoding
* @param string $sString
* @return bool True if the string contains some typical UTF-8 multi-byte sequences
*/
static function LooksLikeUTF8($sString)
{
return preg_match('%(?:
[\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
|\xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
|\xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
|\xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
|[\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
|\xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
)+%xs', $sString);
}
/**
* Attempt a login
*
* @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...)
* @return int One of the class constants EXIT_CODE_...
*/
protected static function Login($iOnExit)
{
if (self::SecureConnectionRequired() && !utils::IsConnectionSecure())
{
// Non secured URL... request for a secure connection
throw new Exception('Secure connection required!');
}
$aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes();
if (isset($_SESSION['auth_user']))
{
//echo "User: ".$_SESSION['auth_user']."\n";
// Already authentified
$bRet = UserRights::Login($_SESSION['auth_user']); // Login & set the user's language
if ($bRet)
{
return self::EXIT_CODE_OK;
}
// The user account is no longer valid/enabled
static::ResetSession();
}
$index = 0;
$sLoginMode = '';
$sAuthentication = 'internal';
while(($sLoginMode == '') && ($index < count($aAllowedLoginTypes)))
{
$sLoginType = $aAllowedLoginTypes[$index];
switch($sLoginType)
{
case 'cas':
utils::InitCASClient();
// check CAS authentication
if (phpCAS::isAuthenticated())
{
$sAuthUser = phpCAS::getUser();
$sAuthPwd = '';
$sLoginMode = 'cas';
$sAuthentication = 'external';
}
break;
case 'form':
// iTop standard mode: form based authentication
$sAuthUser = utils::ReadPostedParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadPostedParam('auth_pwd', null, false, 'raw_data');
if (($sAuthUser != '') && ($sAuthPwd !== null))
{
$sLoginMode = 'form';
}
break;
case 'basic':
// Standard PHP authentication method, works with Apache...
// Case 1) Apache running in CGI mode + rewrite rules in .htaccess
if (isset($_SERVER['HTTP_AUTHORIZATION']) && !empty($_SERVER['HTTP_AUTHORIZATION']))
{
list($sAuthUser, $sAuthPwd) = explode(':' , base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)));
$sLoginMode = 'basic';
}
else if (isset($_SERVER['PHP_AUTH_USER']))
{
$sAuthUser = $_SERVER['PHP_AUTH_USER'];
// Unfortunately, the RFC is not clear about the encoding...
// IE and FF supply the user and password encoded in ISO-8859-1 whereas Chrome provides them encoded in UTF-8
// So let's try to guess if it's an UTF-8 string or not... fortunately all encodings share the same ASCII base
if (!self::LooksLikeUTF8($sAuthUser))
{
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthUser = iconv('iso-8859-1', 'utf-8', $sAuthUser);
}
$sAuthPwd = $_SERVER['PHP_AUTH_PW'];
if (!self::LooksLikeUTF8($sAuthPwd))
{
// Does not look like and UTF-8 string, try to convert it from iso-8859-1 to UTF-8
// Supposed to be harmless in case of a plain ASCII string...
$sAuthPwd = iconv('iso-8859-1', 'utf-8', $sAuthPwd);
}
$sLoginMode = 'basic';
}
break;
case 'external':
// Web server supplied authentication
$bExternalAuth = false;
$sExtAuthVar = MetaModel::GetConfig()->GetExternalAuthenticationVariable(); // In which variable is the info passed ?
eval('$sAuthUser = isset('.$sExtAuthVar.') ? '.$sExtAuthVar.' : false;'); // Retrieve the value
if ($sAuthUser && (strlen($sAuthUser) > 0))
{
$sAuthPwd = ''; // No password in this case the web server already authentified the user...
$sLoginMode = 'external';
$sAuthentication = 'external';
}
break;
case 'url':
// Credentials passed directly in the url
$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
$sAuthPwd = utils::ReadParam('auth_pwd', null, false, 'raw_data');
if (($sAuthUser != '') && ($sAuthPwd !== null))
{
$sLoginMode = 'url';
}
break;
}
$index++;
}
//echo "\nsLoginMode: $sLoginMode (user: $sAuthUser / pwd: $sAuthPwd\n)";
if ($sLoginMode == '')
{
// First connection
$sDesiredLoginMode = utils::ReadParam('login_mode');
if (in_array($sDesiredLoginMode, $aAllowedLoginTypes))
{
$sLoginMode = $sDesiredLoginMode;
}
else
{
$sLoginMode = $aAllowedLoginTypes[0]; // First in the list...
}
if (array_key_exists('HTTP_X_COMBODO_AJAX', $_SERVER))
{
// X-Combodo-Ajax is a special header automatically added to all ajax requests
// Let's reply that we're currently logged-out
header('HTTP/1.0 401 Unauthorized');
exit;
}
if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic'))
{
header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION));
header('HTTP/1.0 401 Unauthorized');
header('Content-type: text/html; charset=iso-8859-1');
exit;
}
else if($iOnExit == self::EXIT_RETURN)
{
if (($sAuthUser !== '') && ($sAuthPwd === null))
{
return self::EXIT_CODE_MISSINGPASSWORD;
}
else
{
return self::EXIT_CODE_MISSINGLOGIN;
}
}
else
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */);
$oPage->output();
exit;
}
}
else
{
if (!UserRights::CheckCredentials($sAuthUser, $sAuthPwd, $sLoginMode, $sAuthentication))
{
//echo "Check Credentials returned false for user $sAuthUser!";
self::ResetSession();
if (($iOnExit == self::EXIT_HTTP_401) || ($sLoginMode == 'basic'))
{
header('WWW-Authenticate: Basic realm="'.Dict::Format('UI:iTopVersion:Short', ITOP_VERSION));
header('HTTP/1.0 401 Unauthorized');
header('Content-type: text/html; charset=iso-8859-1');
exit;
}
else if($iOnExit == self::EXIT_RETURN)
{
return self::EXIT_CODE_WRONGCREDENTIALS;
}
else
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */);
$oPage->output();
exit;
}
}
else
{
// User is Ok, let's save it in the session and proceed with normal login
UserRights::Login($sAuthUser, $sAuthentication); // Login & set the user's language
if (MetaModel::GetConfig()->Get('log_usage'))
{
$oLog = new EventLoginUsage();
$oLog->Set('userinfo', UserRights::GetUser());
$oLog->Set('user_id', UserRights::GetUserObject()->GetKey());
$oLog->Set('message', 'Successful login');
$oLog->DBInsertNoReload();
}
$_SESSION['auth_user'] = $sAuthUser;
$_SESSION['login_mode'] = $sLoginMode;
UserRights::_InitSessionCache();
}
}
return self::EXIT_CODE_OK;
}
/**
* Overridable: depending on the user, head toward a dedicated portal
* @param string|null $sRequestedPortalId
* @param int $iOnExit How to complete the call: redirect or return a code
*/
protected static function ChangeLocation($sRequestedPortalId = null, $iOnExit = self::EXIT_PROMPT)
{
$fStart = microtime(true);
$ret = call_user_func(array(self::$sHandlerClass, 'Dispatch'), $sRequestedPortalId);
if ($ret === true)
{
return self::EXIT_CODE_OK;
}
else if($ret === false)
{
throw new Exception('Nowhere to go??');
}
else
{
if ($iOnExit == self::EXIT_RETURN)
{
return self::EXIT_CODE_PORTALUSERNOTAUTHORIZED;
}
else
{
// No rights to be here, redirect to the portal
header('Location: '.$ret);
}
}
}
/**
* Check if the user is already authentified, if yes, then performs some additional validations:
* - if $bMustBeAdmin is true, then the user must be an administrator, otherwise an error is displayed
* - if $bIsAllowedToPortalUsers is false and the user has only access to the portal, then the user is redirected to the portal
* @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page
* @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal
* @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...)
*/
static function DoLogin($bMustBeAdmin = false, $bIsAllowedToPortalUsers = false, $iOnExit = self::EXIT_PROMPT)
{
$sRequestedPortalId = $bIsAllowedToPortalUsers ? 'legacy_portal' : 'backoffice';
return self::DoLoginEx($sRequestedPortalId, $bMustBeAdmin, $iOnExit);
}
/**
* Check if the user is already authentified, if yes, then performs some additional validations to redirect towards the desired "portal"
* @param string|null $sRequestedPortalId The requested "portal" interface, null for any
* @param bool $bMustBeAdmin Whether or not the user must be an admin to access the current page
* @param int iOnExit What action to take if the user is not logged on (one of the class constants EXIT_...)
*/
static function DoLoginEx($sRequestedPortalId = null, $bMustBeAdmin = false, $iOnExit = self::EXIT_PROMPT)
{
$operation = utils::ReadParam('loginop', '');
$sMessage = self::HandleOperations($operation); // May exit directly
$iRet = self::Login($iOnExit);
if ($iRet == self::EXIT_CODE_OK)
{
if ($bMustBeAdmin && !UserRights::IsAdministrator())
{
if ($iOnExit == self::EXIT_RETURN)
{
return self::EXIT_CODE_MUSTBEADMIN;
}
else
{
require_once(APPROOT.'/setup/setuppage.class.inc.php');
$oP = new SetupPage(Dict::S('UI:PageTitle:FatalError'));
$oP->add("".Dict::S('UI:Login:Error:AccessAdmin')."
\n");
$oP->p("".Dict::S('UI:LogOffMenu')."");
$oP->output();
exit;
}
}
$iRet = call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $sRequestedPortalId, $iOnExit);
}
if ($iOnExit == self::EXIT_RETURN)
{
return $iRet;
}
else
{
return $sMessage;
}
}
protected static function HandleOperations($operation)
{
$sMessage = ''; // most of the operations never return, but some can return a message to be displayed
if ($operation == 'logoff')
{
if (isset($_SESSION['login_mode']))
{
$sLoginMode = $_SESSION['login_mode'];
}
else
{
$aAllowedLoginTypes = MetaModel::GetConfig()->GetAllowedLoginTypes();
if (count($aAllowedLoginTypes) > 0)
{
$sLoginMode = $aAllowedLoginTypes[0];
}
else
{
$sLoginMode = 'form';
}
}
self::ResetSession();
$oPage = self::NewLoginWebPage();
$oPage->DisplayLoginForm( $sLoginMode, false /* not a failed attempt */);
$oPage->output();
exit;
}
else if ($operation == 'forgot_pwd')
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayForgotPwdForm();
$oPage->output();
exit;
}
else if ($operation == 'forgot_pwd_go')
{
$oPage = self::NewLoginWebPage();
$oPage->ForgotPwdGo();
$oPage->output();
exit;
}
else if ($operation == 'reset_pwd')
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayResetPwdForm();
$oPage->output();
exit;
}
else if ($operation == 'do_reset_pwd')
{
$oPage = self::NewLoginWebPage();
$oPage->DoResetPassword();
$oPage->output();
exit;
}
else if ($operation == 'change_pwd')
{
$sAuthUser = $_SESSION['auth_user'];
UserRights::Login($sAuthUser); // Set the user's language
$oPage = self::NewLoginWebPage();
$oPage->DisplayChangePwdForm();
$oPage->output();
exit;
}
if ($operation == 'do_change_pwd')
{
$sAuthUser = $_SESSION['auth_user'];
UserRights::Login($sAuthUser); // Set the user's language
$sOldPwd = utils::ReadPostedParam('old_pwd', '', false, 'raw_data');
$sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data');
if (UserRights::CanChangePassword() && ((!UserRights::CheckCredentials($sAuthUser, $sOldPwd)) || (!UserRights::ChangePassword($sOldPwd, $sNewPwd))))
{
$oPage = self::NewLoginWebPage();
$oPage->DisplayChangePwdForm(true); // old pwd was wrong
$oPage->output();
exit;
}
$sMessage = Dict::S('UI:Login:PasswordChanged');
}
return $sMessage;
}
protected static function Dispatch($sRequestedPortalId)
{
if ($sRequestedPortalId === null) return true; // allowed to any portal => return true
$aPortalsConf = PortalDispatcherData::GetData();
$aDispatchers = array();
foreach($aPortalsConf as $sPortalId => $aConf)
{
$sHandlerClass = $aConf['handler'];
$aDispatchers[$sPortalId] = new $sHandlerClass($sPortalId);
}
if (array_key_exists($sRequestedPortalId, $aDispatchers) && $aDispatchers[$sRequestedPortalId]->IsUserAllowed())
{
return true;
}
foreach($aDispatchers as $sPortalId => $oDispatcher)
{
if ($oDispatcher->IsUserAllowed()) return $oDispatcher->GetUrl();
}
return false; // nothing matched !!
}
} // End of class