Przeglądaj źródła

New feature: Forgot password -> email to reset (possibly disabled in the config file)

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2855 a333f486-631f-4898-b8df-5754b55c2be0
romainq 12 lat temu
rodzic
commit
d61e857b5d

+ 286 - 85
application/loginwebpage.class.inc.php

@@ -31,72 +31,62 @@ require_once(APPROOT."/application/nicewebpage.class.inc.php");
 
 class LoginWebPage extends NiceWebPage
 {
+	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()
-    {
-        parent::__construct("iTop Login");
-        $this->add_style(<<<EOF
-body {
-	background: #eee;
-	margin: 0;
-	padding: 0;
-}
-#login-logo {
-	margin-top: 150px;
-	width: 300px;
-	padding-left: 20px;
-	padding-right: 20px;
-	padding-top: 10px;
-	padding-bottom: 10px;
-	margin-left: auto;
-	margin-right: auto;
-	background: #f6f6f1;
-	height: 54px;
-	border-top: 1px solid #000;
-	border-left: 1px solid #000;
-	border-right: 1px solid #000;
-	border-bottom: 0;
-	text-align: center;
-}
-#login-logo img {
-	border: 0;
-}
-#login {
-	width: 300px;
-	margin-left: auto;
-	margin-right: auto;
-	padding: 20px;
-	background-color: #fff;
-	border-bottom: 1px solid #000;
-	border-left: 1px solid #000;
-	border-right: 1px solid #000;
-	border-top: 0;
-	text-align: center;
-}
-#pwd, #user,#old_pwd, #new_pwd, #retype_new_pwd {
-	width: 10em;
-}
-.center {
-	text-align: center;
-}
-
-h1 {
-	color: #1C94C4;
-	font-size: 16pt;
-}
-.v-spacer {
-	padding-top: 1em;
-}
-EOF
-);
+	public function __construct($sTitle = 'iTop Login')
+	{
+		parent::__construct($sTitle);
+		$this->SetStyleSheet();
 	}
 	
+	public function SetStyleSheet()
+	{
+		$this->add_linked_stylesheet("../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;
+		if (file_exists(MODULESROOT.'branding/'.$sBrandingLogo))
+		{
+			$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/'.$sBrandingLogo;
+		}
+		$this->add("<div id=\"login-logo\"><a href=\"".htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8')."\"><img title=\"$sVersionShort\" src=\"$sDisplayIcon\"></a></div>\n");
+	}
+
 	public function DisplayLoginForm($sLoginType, $bFailedLogin = false)
 	{
 		switch($sLoginType)
@@ -121,14 +111,7 @@ EOF
 			$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
 			$sAuthPwd = utils::ReadParam('suggest_pwd', '', true, 'raw_data');
 	
-			$sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION);
-			$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
-			$sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/itop-logo-external.png';
-			if (file_exists(MODULESROOT.'branding/login-logo.png'))
-			{
-				$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/login-logo.png';
-			}
-			$this->add("<div id=\"login-logo\"><a href=\"".htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8')."\"><img title=\"$sVersionShort\" src=\"$sDisplayIcon\"></a></div>\n");
+			$this->DisplayLoginHeader();
 			$this->add("<div id=\"login\">\n");
 			$this->add("<h1>".Dict::S('UI:Login:Welcome')."</h1>\n");
 			if ($bFailedLogin)
@@ -148,9 +131,14 @@ EOF
 			}
 			$this->add("<form method=\"post\">\n");
 			$this->add("<table width=\"100%\">\n");
+			$sForgotPwd = $this->EnableResetPassword() ? $this->ForgotPwdLink() : '';
 			$this->add("<tr><td style=\"text-align:right\"><label for=\"user\">".Dict::S('UI:Login:UserNamePrompt').":</label></td><td style=\"text-align:left\"><input id=\"user\" type=\"text\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" /></td></tr>\n");
 			$this->add("<tr><td style=\"text-align:right\"><label for=\"pwd\">".Dict::S('UI:Login:PasswordPrompt').":</label></td><td style=\"text-align:left\"><input id=\"pwd\" type=\"password\" name=\"auth_pwd\" value=\"".htmlentities($sAuthPwd, ENT_QUOTES, 'UTF-8')."\" /></td></tr>\n");
 			$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"> <input type=\"submit\" value=\"".Dict::S('UI:Button:Login')."\" /></td></tr>\n");
+			if (strlen($sForgotPwd) > 0)
+			{
+				$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\">$sForgotPwd</td></tr>\n");
+			}
 			$this->add("</table>\n");
 			$this->add("<input type=\"hidden\" name=\"loginop\" value=\"login\" />\n");
 						
@@ -180,11 +168,194 @@ EOF
 		}
 	}
 
+	/**
+	 * Return '' to disable this feature	
+	 */	
+	public function ForgotPwdLink()
+	{
+		$sUrl = '?loginop=forgot_pwd';
+		$sHtml = "<a href=\"$sUrl\" target=\"_blank\">".Dict::S('UI:Login:ForgotPwd')."</a>";
+		return $sHtml;
+	}
+
+	public function DisplayForgotPwdForm($bFailedToReset = false, $sFailureReason = null)
+	{
+		$this->DisplayLoginHeader();
+		$this->add("<div id=\"login\">\n");
+		$this->add("<h1>".Dict::S('UI:Login:ForgotPwdForm')."</h1>\n");
+		$this->add("<p>".Dict::S('UI:Login:ForgotPwdForm+')."</p>\n");
+		if ($bFailedToReset)
+		{
+			$this->add("<p class=\"hilite\">".Dict::Format('UI:Login:ResetPwdFailed', $sFailureReason)."</p>\n");
+		}
+		$sAuthUser = utils::ReadParam('auth_user', '', true, 'raw_data');
+		$this->add("<form method=\"post\">\n");
+		$this->add("<table width=\"100%\">\n");
+		$this->add("<tr><td style=\"text-align:right\"><label for=\"user\">".Dict::S('UI:Login:UserNamePrompt').":</label></td><td style=\"text-align:left\"><input id=\"user\" type=\"text\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" /></td></tr>\n");
+		$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\">  <input type=\"button\" onClick=\"window.close();\" value=\"".Dict::S('UI:Button:Cancel')."\" />&nbsp;&nbsp;<input type=\"submit\" value=\"".Dict::S('UI:Button:ResetPassword')."\" /></td></tr>\n");
+		$this->add("</table>\n");
+		$this->add("<input type=\"hidden\" name=\"loginop\" value=\"forgot_pwd_go\" />\n");
+		$this->add("</form>\n");
+		$this->add("</div>\n");
+	}
+
+	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);
+			$oEmail->SetRecipientFrom($sTo);
+			$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("<div id=\"login\">\n");
+			$this->add("<h1>".Dict::S('UI:Login:ForgotPwdForm')."</h1>\n");
+			$this->add("<p>".Dict::S('UI:ResetPwd-EmailSent')."</p>");
+			$this->add("<form method=\"post\">\n");
+			$this->add("<table width=\"100%\">\n");
+			$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><input type=\"button\" onClick=\"window.close();\" value=\"".Dict::S('UI:Button:Done')."\" /></td></tr>\n");
+			$this->add("</table>\n");
+			$this->add("</form>\n");
+			$this->add("</div\n");
+		}
+		catch(Exception $e)
+		{
+			$this->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("<div id=\"login\">\n");
+		$this->add("<h1>".Dict::S('UI:ResetPwd-Title')."</h1>\n");
+		if ($oUser == null)
+		{
+			$this->add("<p>".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."</p>\n");
+		}
+		elseif ($oUser->Get('reset_pwd_token') != $sToken)
+		{
+			$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\n");
+		}
+		else
+		{
+			$this->add("<p>".Dict::Format('UI:ResetPwd-Error-EnterPassword', $oUser->GetFriendlyName())."</p>\n");
+
+			$sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch');
+			$this->add_script(
+<<<EOF
+function DoCheckPwd()
+{
+	if ($('#new_pwd').val() != $('#retype_new_pwd').val())
+	{
+		alert('$sInconsistenPwdMsg');
+		return false;
+	}
+	return true;
+}
+EOF
+			);
+			$this->add("<form method=\"post\">\n");
+			$this->add("<table width=\"100%\">\n");
+			$this->add("<tr><td style=\"text-align:right\"><label for=\"new_pwd\">".Dict::S('UI:Login:NewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"new_pwd\" name=\"new_pwd\" value=\"\" /></td></tr>\n");
+			$this->add("<tr><td style=\"text-align:right\"><label for=\"retype_new_pwd\">".Dict::S('UI:Login:RetypeNewPasswordPrompt').":</label></td><td style=\"text-align:left\"><input type=\"password\" id=\"retype_new_pwd\" name=\"retype_new_pwd\" value=\"\" /></td></tr>\n");
+			$this->add("<tr><td colspan=\"2\" class=\"center v-spacer\"><input type=\"submit\" onClick=\"return DoCheckPwd();\" value=\"".Dict::S('UI:Button:ChangePassword')."\" /></td></tr>\n");
+			$this->add("</table>\n");
+			$this->add("<input type=\"hidden\" name=\"loginop\" value=\"do_reset_pwd\" />\n");
+			$this->add("<input type=\"hidden\" name=\"auth_user\" value=\"".htmlentities($sAuthUser, ENT_QUOTES, 'UTF-8')."\" />\n");
+			$this->add("<input type=\"hidden\" name=\"token\" value=\"".htmlentities($sToken, ENT_QUOTES, 'UTF-8')."\" />\n");
+			$this->add("</form>\n");
+			$this->add("</div\n");
+		}
+	}
+
+	public function DoResetPassword()
+	{
+		$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
+		$sToken = utils::ReadParam('token', '', false, 'raw_data');
+		$sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data');
+
+		UserRights::Login($sAuthUser); // Set the user's language
+		$oUser = UserRights::GetUserObject();
+
+		$this->DisplayLoginHeader();
+		$this->add("<div id=\"login\">\n");
+		$this->add("<h1>".Dict::S('UI:ResetPwd-Title')."</h1>\n");
+		if ($oUser == null)
+		{
+			$this->add("<p>".Dict::Format('UI:ResetPwd-Error-WrongLogin', $sAuthUser)."</p>\n");
+		}
+		elseif ($oUser->Get('reset_pwd_token') != $sToken)
+		{
+			$this->add("<p>".Dict::S('UI:ResetPwd-Error-InvalidToken')."</p>\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("<p>".Dict::S('UI:ResetPwd-Ready')."</p>");
+			$sUrl = utils::GetAbsoluteUrlAppRoot();
+			$this->add("<p><a href=\"$sUrl\">".Dict::S('UI:ResetPwd-Login')."</a></p>");
+		}
+		$this->add("</div\n");
+	}
+
 	public function DisplayChangePwdForm($bFailedLogin = false)
 	{
 		$sAuthUser = utils::ReadParam('auth_user', '', false, 'raw_data');
 
-		$sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION);
 		$sInconsistenPwdMsg = Dict::S('UI:Login:RetypePwdDoesNotMatch');
 		$this->add_script(<<<EOF
 function GoBack()
@@ -203,13 +374,7 @@ function DoCheckPwd()
 }
 EOF
 );
-		$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
-		$sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/itop-logo.png';
-		if (file_exists(MODULESROOT.'branding/main-logo.png'))
-		{
-			$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/main-logo.png';
-		}
-		$this->add("<div id=\"login-logo\"><a href=\"".htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8')."\"><img title=\"$sVersionShort\" src=\"$sDisplayIcon\"></a></div>\n");
+		$this->DisplayLoginHeader();
 		$this->add("<div id=\"login\">\n");
 		$this->add("<h1>".Dict::S('UI:Login:ChangeYourPassword')."</h1>\n");
 		if ($bFailedLogin)
@@ -353,7 +518,7 @@ EOF
 				{
 					$sLoginMode = $aAllowedLoginTypes[0]; // First in the list...
 				}
-				$oPage = new LoginWebPage();
+				$oPage = self::NewLoginWebPage();
 				$oPage->DisplayLoginForm( $sLoginMode, false /* no previous failed attempt */);
 				$oPage->output();
 				exit;
@@ -364,7 +529,7 @@ EOF
 				{
 					//echo "Check Credentials returned false for user $sAuthUser!";
 					self::ResetSession();
-					$oPage = new LoginWebPage();
+					$oPage = self::NewLoginWebPage();
 					$oPage->DisplayLoginForm( $sLoginMode, true /* failed attempt */);
 					$oPage->output();
 					exit;
@@ -391,6 +556,19 @@ EOF
 	}
 	
 	/**
+	 * Overridable: depending on the user, head toward a dedicated portal
+	 * @param bool $bIsAllowedToPortalUsers Whether or not the current page is considered as part of the portal
+	 */	 
+	protected static function ChangeLocation($bIsAllowedToPortalUsers)
+	{
+		if ( (!$bIsAllowedToPortalUsers) && (UserRights::IsPortalUser()))
+		{
+			// No rights to be here, redirect to the portal
+			header('Location: '.utils::GetAbsoluteUrlAppRoot().'portal/index.php');
+		}
+	}
+
+	/**
 	 * 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
@@ -421,16 +599,44 @@ EOF
 				}
 			}
 			self::ResetSession();
-			$oPage = new LoginWebPage();
+			$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 = new LoginWebPage();
+			$oPage = self::NewLoginWebPage();
 			$oPage->DisplayChangePwdForm();
 			$oPage->output();
 			exit;
@@ -443,7 +649,7 @@ EOF
 			$sNewPwd = utils::ReadPostedParam('new_pwd', '', false, 'raw_data');
 			if (UserRights::CanChangePassword() && ((!UserRights::CheckCredentials($sAuthUser, $sOldPwd)) || (!UserRights::ChangePassword($sOldPwd, $sNewPwd))))
 			{
-				$oPage = new LoginWebPage();
+				$oPage = self::NewLoginWebPage();
 				$oPage->DisplayChangePwdForm(true); // old pwd was wrong
 				$oPage->output();
 				exit;
@@ -462,12 +668,7 @@ EOF
 			$oP->output();
 			exit;
 		}
-		elseif ( (!$bIsAllowedToPortalUsers) && (UserRights::IsPortalUser()))
-		{
-			// No rights to be here, redirect to the portal
-			header('Location: '.utils::GetAbsoluteUrlAppRoot().'portal/index.php');
-		}
+		call_user_func(array(self::$sHandlerClass, 'ChangeLocation'), $bIsAllowedToPortalUsers);
 		return $sMessage;
 	}	
 } // End of class
-?>

+ 9 - 0
core/config.class.inc.php

@@ -525,6 +525,15 @@ class Config
 			'source_of_value' => '',
 			'show_in_conf_sample' => false,
 		),
+		'forgot_password' => array(
+			'type' => 'bool',
+			'description' => 'Enable the "Forgot password" feature',
+			// examples... not used (nor 'description')
+			'default' => true,
+			'value' => true,
+			'source_of_value' => '',
+			'show_in_conf_sample' => false,
+		),
 		'deadline_format' => array(
 			'type' => 'string',
 			'description' => 'The format used for displaying "deadline" attributes: any string with the following placeholders: $date$, $difference$',

+ 45 - 0
core/userrights.class.inc.php

@@ -140,6 +140,7 @@ abstract class User extends cmdbAbstractObject
 				return $sLastName;
 			}
 		}
+		return $this->Get('login');
 	}
 
 	/*
@@ -289,6 +290,9 @@ abstract class UserInternal extends User
 		MetaModel::Init_Params($aParams);
 		MetaModel::Init_InheritAttributes();
 
+		// When set, this token allows for password reset
+		MetaModel::Init_AddAttribute(new AttributeString("reset_pwd_token", array("allowed_values"=>null, "sql"=>"reset_pwd_token", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
+
 		// Display lists
 		MetaModel::Init_SetZListItems('details', array('contactid', 'first_name', 'email', 'login', 'language', 'profile_list', 'allowed_org_list')); // Attributes to be displayed for the complete details
 		MetaModel::Init_SetZListItems('list', array('finalclass', 'first_name', 'last_name', 'login')); // Attributes to be displayed for a list
@@ -296,6 +300,47 @@ abstract class UserInternal extends User
 		MetaModel::Init_SetZListItems('standard_search', array('login', 'contactid')); // Criteria of the std search form
 		MetaModel::Init_SetZListItems('advanced_search', array('login', 'contactid')); // Criteria of the advanced search form
 	}
+
+	/**
+	 * Use with care!
+	 */	 	
+	public function SetPassword($sNewPassword)
+	{
+	}
+
+	/**
+	 * The email recipient is the person who is allowed to regain control when the password gets lost	
+	 * Throws an exception if the feature cannot be available
+	 */	
+	public function GetResetPasswordEmail()
+	{
+		if (!MetaModel::IsValidAttCode(get_class($this), 'contactid'))
+		{
+			throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact'));
+		}
+		$iContactId = $this->Get('contactid');
+		if ($iContactId == 0)
+		{
+			throw new Exception(Dict::S('UI:ResetPwd-Error-NoContact'));
+		}
+		$oContact = MetaModel::GetObject('Contact', $iContactId);
+		// Determine the email attribute (the first one will be our choice)
+		foreach (MetaModel::ListAttributeDefs(get_class($oContact)) as $sAttCode => $oAttDef)
+		{
+			if ($oAttDef instanceof AttributeEmailAddress)
+			{
+				$sEmailAttCode = $sAttCode;
+				// we've got one, exit the loop
+				break;
+			}
+		}
+		if (!isset($sEmailAttCode))
+		{
+			throw new Exception(Dict::S('UI:ResetPwd-Error-NoEmailAtt'));
+		}
+		$sRes = trim($oContact->Get($sEmailAttCode));
+		return $sRes;
+	}
 }
 
 /**

+ 52 - 0
css/login.css

@@ -0,0 +1,52 @@
+@CHARSET "UTF-8";
+body {
+	background: #eee;
+	margin: 0;
+	padding: 0;
+}
+#login-logo {
+	margin-top: 150px;
+	width: 300px;
+	padding-left: 20px;
+	padding-right: 20px;
+	padding-top: 10px;
+	padding-bottom: 10px;
+	margin-left: auto;
+	margin-right: auto;
+	background: #f6f6f1;
+	height: 54px;
+	border-top: 1px solid #000;
+	border-left: 1px solid #000;
+	border-right: 1px solid #000;
+	border-bottom: 0;
+	text-align: center;
+}
+#login-logo img {
+	border: 0;
+}
+#login {
+	width: 300px;
+	margin-left: auto;
+	margin-right: auto;
+	padding: 20px;
+	background-color: #fff;
+	border-bottom: 1px solid #000;
+	border-left: 1px solid #000;
+	border-right: 1px solid #000;
+	border-top: 0;
+	text-align: center;
+}
+#pwd, #user,#old_pwd, #new_pwd, #retype_new_pwd {
+	width: 10em;
+}
+.center {
+	text-align: center;
+}
+
+h1 {
+	color: #1C94C4;
+	font-size: 16pt;
+}
+.v-spacer {
+	padding-top: 1em;
+}

+ 25 - 2
dictionaries/dictionary.itop.ui.php

@@ -475,6 +475,29 @@ Dict::Add('EN US', 'English', 'English', array(
 	'UI:Login:IdentifyYourself' => 'Identify yourself before continuing',
 	'UI:Login:UserNamePrompt' => 'User Name',
 	'UI:Login:PasswordPrompt' => 'Password',
+	'UI:Login:ForgotPwd' => 'Forgot your password?',
+	'UI:Login:ForgotPwdForm' => 'Forgot your password',
+	'UI:Login:ForgotPwdForm+' => 'iTop can send you an email in which you will find instructions to follow to reset your account.',
+	'UI:Button:ResetPassword' => 'Send now!',
+	'UI:Login:ResetPwdFailed' => 'Failed to send an email: %1$s',
+
+	'UI:ResetPwd-Error-WrongLogin' => '\'%1$s\' is not a valid login',
+	'UI:ResetPwd-Error-NotPossible' => 'external accounts do not allow password reset.',
+	'UI:ResetPwd-Error-FixedPwd' => 'the account does not allow password reset.',
+	'UI:ResetPwd-Error-NoContact' => 'the account is not associated to a person.',
+	'UI:ResetPwd-Error-NoEmailAtt' => 'the account is not associated to a person having an email attribute. Please Contact your administrator.',
+	'UI:ResetPwd-Error-NoEmail' => 'missing an email address. Please Contact your administrator.',
+	'UI:ResetPwd-Error-Send' => 'email transport technical issue. Please Contact your administrator.',
+	'UI:ResetPwd-EmailSent' => 'Please check your email box and follow the instructions...',
+	'UI:ResetPwd-EmailSubject' => 'Reset your iTop password',
+	'UI:ResetPwd-EmailBody' => '<body><p>You have requested to reset your iTop password.</p><p>Please follow this link (single usage) to <a href="%1$s">enter a new password</a></p>.',
+
+	'UI:ResetPwd-Title' => 'Reset password',
+	'UI:ResetPwd-Error-InvalidToken' => 'Sorry, either the password has already been reset, or you have received several emails. Please make sure that you use the link provided in the very last email received.',
+	'UI:ResetPwd-Error-EnterPassword' => 'Enter a new password for the account \'%1$s\'.',
+	'UI:ResetPwd-Ready' => 'The password has been changed.',
+	'UI:ResetPwd-Login' => 'Click here to login...',
+
 	'UI:Login:About' => '',
 	'UI:Login:ChangeYourPassword' => 'Change Your Password',
 	'UI:Login:OldPasswordPrompt' => 'Old password',
@@ -485,11 +508,11 @@ Dict::Add('EN US', 'English', 'English', array(
 	'UI:LogOff:ThankYou' => 'Thank you for using iTop',
 	'UI:LogOff:ClickHereToLoginAgain' => 'Click here to login again...',
 	'UI:ChangePwdMenu' => 'Change Password...',
-	'UI:Login:PasswordChanged' => 'Password successfully set !',
+	'UI:Login:PasswordChanged' => 'Password successfully set!',
 	'UI:AccessRO-All' => 'iTop is read-only',
 	'UI:AccessRO-Users' => 'iTop is read-only for end-users',
 	'UI:ApplicationEnvironment' => 'Application environment: %1$s',
-	'UI:Login:RetypePwdDoesNotMatch' => 'New password and retyped new password do not match !',
+	'UI:Login:RetypePwdDoesNotMatch' => 'New password and retyped new password do not match!',
 	'UI:Button:Login' => 'Enter iTop',
 	'UI:Login:Error:AccessRestricted' => 'iTop access is restricted. Please, contact an iTop administrator.',
 	'UI:Login:Error:AccessAdmin' => 'Access restricted to people having administrator privileges. Please, contact an iTop administrator.',

+ 23 - 0
dictionaries/fr.dictionary.itop.ui.php

@@ -351,6 +351,29 @@ Dict::Add('FR FR', 'French', 'Français', array(
 	'UI:Login:IdentifyYourself' => 'Merci de vous identifier',
 	'UI:Login:UserNamePrompt' => 'Identifiant',
 	'UI:Login:PasswordPrompt' => 'Mot de passe',
+	'UI:Login:ForgotPwd' => 'Mot de passe oublié ?',
+	'UI:Login:ForgotPwdForm' => 'Mot de passe oublié',
+	'UI:Login:ForgotPwdForm+' => 'Vous pouvez demander à saisir d\'un nouveau mot de passe. Vous allez recevoir un email et vous pourrez suivre les instructions.',
+	'UI:Button:ResetPassword' => 'Envoyer le message',
+	'UI:Login:ResetPwdFailed' => 'Impossible de vous faire parvenir le message: %1$s',
+
+	'UI:ResetPwd-Error-WrongLogin' => 'le compte \'%1$s\' est inconnu.',
+	'UI:ResetPwd-Error-NotPossible' => 'les comptes "externes" ne permettent pas la saisie d\'un mot de passe dans iTop.',
+	'UI:ResetPwd-Error-FixedPwd' => 'ce mode de saisie du mot de passe n\'est pas autorisé pour ce compte.',
+	'UI:ResetPwd-Error-NoContact' => 'le comte n\'est pas associé à une Personne.',
+	'UI:ResetPwd-Error-NoEmailAtt' => 'il manque un attribut de type "email" sur la Personne associée à ce compte. Veuillez contacter l\'administrateur de l\'application.',
+	'UI:ResetPwd-Error-NoEmail' => 'il manque une adresse email sur la Personne associée à ce compte. Veuillez contacter l\'administrateur de l\'application.',
+	'UI:ResetPwd-Error-Send' => 'erreur technique lors de l\'envoi de l\'email. Veuillez contacter l\'administrateur de l\'application.',
+	'UI:ResetPwd-EmailSent' => 'Veuillez vérifier votre boîte de réception. Ensuite, suivez les instructions données dans l\'email...',
+	'UI:ResetPwd-EmailSubject' => 'Changer votre mot de passe iTop',
+	'UI:ResetPwd-EmailBody' => '<body><p>Vous avez demandé à changer votre de passe iTop sans connaitre le mot de passe précédent.</p><p>Veuillez suivre le lien suivant (usage unique) afin de pouvoir <a href="%1$s">saisir un nouveau mot de passe</a></p>.',
+
+	'UI:ResetPwd-Title' => 'Nouveau mot de passe',
+	'UI:ResetPwd-Error-InvalidToken' => 'Désolé, le mot de passe a déjà été modifié avec le lien que vous avez suivi, ou bien vous avez reçu plusieurs emails. Dans ce cas, veillez à utiliser le tout dernier lien reçu.',
+	'UI:ResetPwd-Error-EnterPassword' => 'Veuillez saisir le nouveau mot de passe pour \'%1$s\'.',
+	'UI:ResetPwd-Ready' => 'Le mot de passe a bien été changé.',
+	'UI:ResetPwd-Login' => 'Cliquez ici pour vous connecter...',
+
 	'UI:Login:ChangeYourPassword' => 'Changer de mot de passe',
 	'UI:Login:OldPasswordPrompt' => 'Ancien mot de passe',
 	'UI:Login:NewPasswordPrompt' => 'Nouveau mot de passe',

+ 2 - 9
pages/logoff.php

@@ -51,16 +51,9 @@ switch($sLoginMode)
 	phpCAS::logoutWithRedirectService($sCASLogoutUrl); // Redirects to the CAS logout page
 	break;
 }
-$oPage = new LoginWebPage();
+$oPage = LoginWebPage::NewLoginWebPage();
 $oPage->no_cache();
-$sVersionShort = Dict::Format('UI:iTopVersion:Short', ITOP_VERSION);
-$sIconUrl = Utils::GetConfig()->Get('app_icon_url');
-$sDisplayIcon = utils::GetAbsoluteUrlAppRoot().'images/itop-logo-external.png';
-if (file_exists(MODULESROOT.'branding/login-logo.png'))
-{
-	$sDisplayIcon = utils::GetAbsoluteUrlModulesRoot().'branding/login-logo.png';
-}
-$oPage->add("<div id=\"login-logo\"><a href=\"".htmlentities($sIconUrl, ENT_QUOTES, 'UTF-8')."\"><img title=\"$sVersionShort\" src=\"$sDisplayIcon\"></a></div>\n");
+$oPage->DisplayLoginHeader();
 $oPage->add("<div id=\"login\">\n");
 $oPage->add("<h1>".Dict::S('UI:LogOff:ThankYou')."</h1>\n");