login();
}
/**
* Login form
*
* @param string $messageNoAccess Access error message
* @param bool $infoMessage
* @internal param string $currentUrl Current URL
* @return void
*/
function login($messageNoAccess = null, $infoMessage = false)
{
self::checkForceSslLogin();
$form = new FormLogin();
if ($form->validate()) {
$nonce = $form->getSubmitValue('form_nonce');
if (Nonce::verifyNonce('Login.login', $nonce)) {
$login = $form->getSubmitValue('form_login');
$password = $form->getSubmitValue('form_password');
$rememberMe = $form->getSubmitValue('form_rememberme') == '1';
$md5Password = md5($password);
try {
$this->authenticateAndRedirect($login, $md5Password, $rememberMe);
} catch (Exception $e) {
$messageNoAccess = $e->getMessage();
}
} else {
$messageNoAccess = $this->getMessageExceptionNoAccess();
}
}
$view = new View('@Login/login');
$view->AccessErrorString = $messageNoAccess;
$view->infoMessage = nl2br($infoMessage);
$view->addForm($form);
$this->configureView($view);
self::setHostValidationVariablesView($view);
echo $view->render();
}
/**
* Configure common view properties
*
* @param View $view
*/
private function configureView($view)
{
$this->setBasicVariablesView($view);
$view->linkTitle = Piwik::getRandomTitle();
$view->forceSslLogin = Config::getInstance()->General['force_ssl_login'];
// crsf token: don't trust the submitted value; generate/fetch it from session data
$view->nonce = Nonce::getNonce('Login.login');
}
/**
* Form-less login
* @see how to use it on http://piwik.org/faq/how-to/#faq_30
* @throws Exception
* @return void
*/
function logme()
{
self::checkForceSslLogin();
$password = Common::getRequestVar('password', null, 'string');
if (strlen($password) != 32) {
throw new Exception(Piwik_TranslateException('Login_ExceptionPasswordMD5HashExpected'));
}
$login = Common::getRequestVar('login', null, 'string');
if ($login == Config::getInstance()->superuser['login']) {
throw new Exception(Piwik_TranslateException('Login_ExceptionInvalidSuperUserAuthenticationMethod', array("logme")));
}
$currentUrl = 'index.php';
if (($idSite = Common::getRequestVar('idSite', false, 'int')) !== false) {
$currentUrl .= '?idSite=' . $idSite;
}
$urlToRedirect = Common::getRequestVar('url', $currentUrl, 'string');
$urlToRedirect = Common::unsanitizeInputValue($urlToRedirect);
$this->authenticateAndRedirect($login, $password, false, $urlToRedirect);
}
/**
* Authenticate user and password. Redirect if successful.
*
* @param string $login user name
* @param string $md5Password md5 hash of password
* @param bool $rememberMe Remember me?
* @param string $urlToRedirect URL to redirect to, if successfully authenticated
* @return string failure message if unable to authenticate
*/
protected function authenticateAndRedirect($login, $md5Password, $rememberMe, $urlToRedirect = 'index.php')
{
$info = array('login' => $login,
'md5Password' => $md5Password,
'rememberMe' => $rememberMe,
);
Nonce::discardNonce('Login.login');
Piwik_PostEvent('Login.initSession', array(&$info));
Url::redirectToUrl($urlToRedirect);
}
protected function getMessageExceptionNoAccess()
{
$message = Piwik_Translate('Login_InvalidNonceOrHeadersOrReferer', array('', ''));
// Should mention trusted_hosts or link to FAQ
return $message;
}
/**
* Reset password action. Stores new password as hash and sends email
* to confirm use.
*
* @param none
* @return void
*/
function resetPassword()
{
self::checkForceSslLogin();
$infoMessage = null;
$formErrors = null;
$form = new FormResetPassword();
if ($form->validate()) {
$nonce = $form->getSubmitValue('form_nonce');
if (Nonce::verifyNonce('Login.login', $nonce)) {
$formErrors = $this->resetPasswordFirstStep($form);
if (empty($formErrors)) {
$infoMessage = Piwik_Translate('Login_ConfirmationLinkSent');
}
} else {
$formErrors = array($this->getMessageExceptionNoAccess());
}
} else {
// if invalid, display error
$formData = $form->getFormData();
$formErrors = $formData['errors'];
}
$view = new View('@Login/resetPassword');
$view->infoMessage = $infoMessage;
$view->formErrors = $formErrors;
echo $view->render();
}
/**
* Saves password reset info and sends confirmation email.
*
* @param QuickForm2 $form
* @return array Error message(s) if an error occurs.
*/
private function resetPasswordFirstStep($form)
{
$loginMail = $form->getSubmitValue('form_login');
$token = $form->getSubmitValue('form_token');
$password = $form->getSubmitValue('form_password');
// check the password
try {
UsersManager::checkPassword($password);
} catch (Exception $ex) {
return array($ex->getMessage());
}
// get the user's login
if ($loginMail === 'anonymous') {
return array(Piwik_Translate('Login_InvalidUsernameEmail'));
}
$user = self::getUserInformation($loginMail);
if ($user === null) {
return array(Piwik_Translate('Login_InvalidUsernameEmail'));
}
$login = $user['login'];
// if valid, store password information in options table, then...
Login::savePasswordResetInfo($login, $password);
// ... send email with confirmation link
try {
$this->sendEmailConfirmationLink($user);
} catch (Exception $ex) {
// remove password reset info
Login::removePasswordResetInfo($login);
return array($ex->getMessage() . '
' . Piwik_Translate('Login_ContactAdmin'));
}
return null;
}
/**
* Sends email confirmation link for a password reset request.
*
* @param array $user User info for the requested password reset.
*/
private function sendEmailConfirmationLink($user)
{
$login = $user['login'];
$email = $user['email'];
// construct a password reset token from user information
$resetToken = self::generatePasswordResetToken($user);
$ip = IP::getIpFromHeader();
$url = Url::getCurrentUrlWithoutQueryString()
. "?module=Login&action=confirmResetPassword&login=" . urlencode($login)
. "&resetToken=" . urlencode($resetToken);
// send email with new password
$mail = new Mail();
$mail->addTo($email, $login);
$mail->setSubject(Piwik_Translate('Login_MailTopicPasswordChange'));
$bodyText = str_replace(
'\n',
"\n",
sprintf(Piwik_Translate('Login_MailPasswordChangeBody'), $login, $ip, $url)
) . "\n";
$mail->setBodyText($bodyText);
$fromEmailName = Config::getInstance()->General['login_password_recovery_email_name'];
$fromEmailAddress = Config::getInstance()->General['login_password_recovery_email_address'];
$mail->setFrom($fromEmailAddress, $fromEmailName);
@$mail->send();
}
/**
* Password reset confirmation action. Finishes the password reset process.
* Users visit this action from a link supplied in an email.
*/
public function confirmResetPassword()
{
$errorMessage = null;
$login = Common::getRequestVar('login', '');
$resetToken = Common::getRequestVar('resetToken', '');
try {
// get password reset info & user info
$user = self::getUserInformation($login);
if ($user === null) {
throw new Exception(Piwik_Translate('Login_InvalidUsernameEmail'));
}
// check that the reset token is valid
$resetPassword = Login::getPasswordToResetTo($login);
if ($resetPassword === false || !self::isValidToken($resetToken, $user)) {
throw new Exception(Piwik_Translate('Login_InvalidOrExpiredToken'));
}
// reset password of user
$this->setNewUserPassword($user, $resetPassword);
} catch (Exception $ex) {
$errorMessage = $ex->getMessage();
}
if (is_null($errorMessage)) // if success, show login w/ success message
{
$this->redirectToIndex('Login', 'resetPasswordSuccess');
return;
} else {
// show login page w/ error. this will keep the token in the URL
$this->login($errorMessage);
return;
}
}
/**
* Sets the password for a user.
*
* @param array $user User info.
* @param string $passwordHash The hashed password to use.
* @throws Exception
*/
private function setNewUserPassword($user, $passwordHash)
{
if (strlen($passwordHash) !== 32) // sanity check
{
throw new Exception(
"setNewUserPassword called w/ incorrect password hash. Something has gone terribly wrong.");
}
if ($user['email'] == Piwik::getSuperUserEmail()) {
if (!Config::getInstance()->isFileWritable()) {
throw new Exception(Piwik_Translate('General_ConfigFileIsNotWritable', array("(config/config.ini.php)", "
")));
}
$user['password'] = $passwordHash;
Config::getInstance()->superuser = $user;
Config::getInstance()->forceSave();
} else {
API::getInstance()->updateUser(
$user['login'], $passwordHash, $email = false, $alias = false, $isPasswordHashed = true);
}
}
/**
* The action used after a password is successfully reset. Displays the login
* screen with an extra message. A separate action is used instead of returning
* the HTML in confirmResetPassword so the resetToken won't be in the URL.
*/
public function resetPasswordSuccess()
{
$this->login($errorMessage = null, $infoMessage = Piwik_Translate('Login_PasswordChanged'));
}
/**
* Get user information
*
* @param string $loginMail user login or email address
* @return array ("login" => '...', "email" => '...', "password" => '...') or null, if user not found
*/
protected function getUserInformation($loginMail)
{
Piwik::setUserIsSuperUser();
$user = null;
if ($loginMail == Piwik::getSuperUserEmail()
|| $loginMail == Config::getInstance()->superuser['login']
) {
$user = array(
'login' => Config::getInstance()->superuser['login'],
'email' => Piwik::getSuperUserEmail(),
'password' => Config::getInstance()->superuser['password'],
);
} else if (API::getInstance()->userExists($loginMail)) {
$user = API::getInstance()->getUser($loginMail);
} else if (API::getInstance()->userEmailExists($loginMail)) {
$user = API::getInstance()->getUserByEmail($loginMail);
}
return $user;
}
/**
* Generate a password reset token. Expires in (roughly) 24 hours.
*
* @param array $user user information
* @param int $timestamp Unix timestamp
* @return string generated token
*/
protected function generatePasswordResetToken($user, $timestamp = null)
{
/*
* Piwik does not store the generated password reset token.
* This avoids a database schema change and SQL queries to store, retrieve, and purge (expired) tokens.
*/
if (!$timestamp) {
$timestamp = time() + 24 * 60 * 60; /* +24 hrs */
}
$expiry = strftime('%Y%m%d%H', $timestamp);
$token = $this->generateHash(
$expiry . $user['login'] . $user['email'],
$user['password']
);
return $token;
}
/**
* Validate token.
*
* @param string $token
* @param array $user user information
* @return bool true if valid, false otherwise
*/
protected function isValidToken($token, $user)
{
$now = time();
// token valid for 24 hrs (give or take, due to the coarse granularity in our strftime format string)
for ($i = 0; $i <= 24; $i++) {
$generatedToken = self::generatePasswordResetToken($user, $now + $i * 60 * 60);
if ($generatedToken === $token) {
return true;
}
}
// fails if token is invalid, expired, password already changed, other user information has changed, ...
return false;
}
/**
* Clear session information
*
* @param none
* @return void
*/
static public function clearSession()
{
$authCookieName = Config::getInstance()->General['login_cookie_name'];
$cookie = new Cookie($authCookieName);
$cookie->delete();
Session::expireSessionCookie();
}
/**
* Logout current user
*
* @param none
* @return void
*/
public function logout()
{
self::clearSession();
$logoutUrl = @Config::getInstance()->General['login_logout_url'];
if (empty($logoutUrl)) {
Piwik::redirectToModule('CoreHome');
} else {
Url::redirectToUrl($logoutUrl);
}
}
/**
* Check force_ssl_login and redirect if connection isn't secure and not using a reverse proxy
*
* @param none
* @return void
*/
protected function checkForceSslLogin()
{
$forceSslLogin = Config::getInstance()->General['force_ssl_login'];
if ($forceSslLogin
&& !ProxyHttp::isHttps()
) {
$url = 'https://'
. Url::getCurrentHost()
. Url::getCurrentScriptName()
. Url::getCurrentQueryString();
Url::redirectToUrl($url);
}
}
}