1<?php 2/** 3 * Matomo - free/libre analytics platform 4 * 5 * @link https://matomo.org 6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later 7 * 8 */ 9namespace Piwik\Plugins\Login; 10 11use Exception; 12use Piwik\Access; 13use Piwik\Auth\Password; 14use Piwik\Common; 15use Piwik\Config; 16use Piwik\Container\StaticContainer; 17use Piwik\Date; 18use Piwik\Log; 19use Piwik\Nonce; 20use Piwik\Piwik; 21use Piwik\Plugins\Login\Security\BruteForceDetection; 22use Piwik\Plugins\UsersManager\Model AS UsersModel; 23use Piwik\Plugins\UsersManager\UserUpdater; 24use Piwik\QuickForm2; 25use Piwik\Session; 26use Piwik\Url; 27use Piwik\UrlHelper; 28use Piwik\View; 29 30/** 31 * Login controller 32 * @api 33 */ 34class Controller extends \Piwik\Plugin\ControllerAdmin 35{ 36 const NONCE_CONFIRMRESETPASSWORD = 'loginConfirmResetPassword'; 37 38 /** 39 * @var PasswordResetter 40 */ 41 protected $passwordResetter; 42 43 /** 44 * @var Auth 45 */ 46 protected $auth; 47 48 /** 49 * @var \Piwik\Session\SessionInitializer 50 */ 51 protected $sessionInitializer; 52 53 /** 54 * @var BruteForceDetection 55 */ 56 protected $bruteForceDetection; 57 58 /** 59 * @var SystemSettings 60 */ 61 protected $systemSettings; 62 63 /* 64 * @var PasswordVerifier 65 */ 66 protected $passwordVerify; 67 68 /** 69 * Constructor. 70 * 71 * @param PasswordResetter $passwordResetter 72 * @param AuthInterface $auth 73 * @param SessionInitializer $sessionInitializer 74 * @param PasswordVerifier $passwordVerify 75 * @param BruteForceDetection $bruteForceDetection 76 * @param SystemSettings $systemSettings 77 */ 78 public function __construct($passwordResetter = null, $auth = null, $sessionInitializer = null, $passwordVerify = null, $bruteForceDetection = null, $systemSettings = null) 79 { 80 parent::__construct(); 81 82 if (empty($passwordResetter)) { 83 $passwordResetter = new PasswordResetter(); 84 } 85 $this->passwordResetter = $passwordResetter; 86 87 if (empty($auth)) { 88 $auth = StaticContainer::get('Piwik\Auth'); 89 } 90 $this->auth = $auth; 91 92 if (empty($passwordVerify)) { 93 $passwordVerify = StaticContainer::get('Piwik\Plugins\Login\PasswordVerifier'); 94 } 95 $this->passwordVerify = $passwordVerify; 96 97 if (empty($sessionInitializer)) { 98 $sessionInitializer = new \Piwik\Session\SessionInitializer(); 99 } 100 $this->sessionInitializer = $sessionInitializer; 101 102 if (empty($bruteForceDetection)) { 103 $bruteForceDetection = StaticContainer::get('Piwik\Plugins\Login\Security\BruteForceDetection'); 104 } 105 $this->bruteForceDetection = $bruteForceDetection; 106 107 if (empty($systemSettings)) { 108 $systemSettings = StaticContainer::get('Piwik\Plugins\Login\SystemSettings'); 109 } 110 $this->systemSettings = $systemSettings; 111 } 112 113 /** 114 * Default action 115 * 116 * @return string 117 */ 118 function index() 119 { 120 return $this->login(); 121 } 122 123 /** 124 * Login form 125 * 126 * @param string $messageNoAccess Access error message 127 * @param bool $infoMessage 128 * @internal param string $currentUrl Current URL 129 * @return string 130 */ 131 function login($messageNoAccess = null, $infoMessage = false) 132 { 133 $form = new FormLogin(); 134 if ($form->validate()) { 135 $nonce = $form->getSubmitValue('form_nonce'); 136 $messageNoAccess = Nonce::verifyNonceWithErrorMessage('Login.login', $nonce, null); 137 138 // validate if there is error message 139 if ($messageNoAccess === "") { 140 $loginOrEmail = $form->getSubmitValue('form_login'); 141 $login = $this->getLoginFromLoginOrEmail($loginOrEmail); 142 143 $password = $form->getSubmitValue('form_password'); 144 try { 145 $this->authenticateAndRedirect($login, $password); 146 } catch (Exception $e) { 147 $messageNoAccess = $e->getMessage(); 148 } 149 } 150 } 151 152 if ($messageNoAccess) { 153 http_response_code(403); 154 } 155 156 $view = new View('@Login/login'); 157 $view->AccessErrorString = $messageNoAccess; 158 $view->infoMessage = nl2br($infoMessage); 159 $view->addForm($form); 160 $this->configureView($view); 161 self::setHostValidationVariablesView($view); 162 163 return $view->render(); 164 } 165 166 private function getLoginFromLoginOrEmail($loginOrEmail) 167 { 168 $model = new UsersModel(); 169 if (!$model->userExists($loginOrEmail)) { 170 $user = $model->getUserByEmail($loginOrEmail); 171 if (!empty($user)) { 172 return $user['login']; 173 } 174 } 175 176 return $loginOrEmail; 177 } 178 179 /** 180 * Configure common view properties 181 * 182 * @param View $view 183 */ 184 protected function configureView($view) 185 { 186 $this->setBasicVariablesNoneAdminView($view); 187 188 $view->linkTitle = Piwik::getRandomTitle(); 189 190 // crsf token: don't trust the submitted value; generate/fetch it from session data 191 $view->nonce = Nonce::getNonce('Login.login'); 192 } 193 194 public function confirmPassword() 195 { 196 Piwik::checkUserIsNotAnonymous(); 197 Piwik::checkUserHasSomeViewAccess(); 198 199 if (!$this->passwordVerify->hasPasswordVerifyBeenRequested()) { 200 throw new Exception('Not available'); 201 } 202 203 if (!Url::isValidHost()) { 204 throw new Exception("Cannot confirm password with untrusted hostname!"); 205 } 206 207 $nonceKey = 'confirmPassword'; 208 $messageNoAccess = ''; 209 210 if (!empty($_POST)) { 211 $nonce = Common::getRequestVar('nonce', null, 'string', $_POST); 212 $password = Common::getRequestVar('password', null, 'string', $_POST); 213 if ($password) { 214 $password = Common::unsanitizeInputValue($password); 215 } 216 $errorMessage = Nonce::verifyNonceWithErrorMessage($nonceKey, $nonce); 217 if ($errorMessage !== "") { 218 $messageNoAccess = $errorMessage; 219 } elseif ($this->passwordVerify->isPasswordCorrect(Piwik::getCurrentUserLogin(), $password)) { 220 $this->passwordVerify->setPasswordVerifiedCorrectly(); 221 return; 222 } else { 223 $messageNoAccess = Piwik::translate('Login_WrongPasswordEntered'); 224 } 225 } 226 227 return $this->renderTemplate('@Login/confirmPassword', array( 228 'nonce' => Nonce::getNonce($nonceKey), 229 'AccessErrorString' => $messageNoAccess, 230 'loginPlugin' => Piwik::getLoginPluginName(), 231 )); 232 } 233 234 /** 235 * Form-less login 236 * @see how to use it on http://piwik.org/faq/how-to/#faq_30 237 * @throws Exception 238 * @return void 239 */ 240 function logme() 241 { 242 if (Config::getInstance()->General['login_allow_logme'] == 0) { 243 throw new Exception('This functionality has been disabled in config'); 244 } 245 246 $password = Common::getRequestVar('password', null, 'string'); 247 248 $login = Common::getRequestVar('login', null, 'string'); 249 if (Piwik::hasTheUserSuperUserAccess($login)) { 250 throw new Exception(Piwik::translate('Login_ExceptionInvalidSuperUserAccessAuthenticationMethod', array("logme"))); 251 } 252 253 $currentUrl = 'index.php'; 254 255 if ($this->idSite) { 256 $currentUrl .= '?idSite=' . $this->idSite; 257 } 258 259 $urlToRedirect = Common::getRequestVar('url', $currentUrl, 'string'); 260 $urlToRedirect = Common::unsanitizeInputValue($urlToRedirect); 261 262 $this->authenticateAndRedirect($login, $password, $urlToRedirect, $passwordHashed = true); 263 } 264 265 public function bruteForceLog() 266 { 267 Piwik::checkUserHasSuperUserAccess(); 268 269 return $this->renderTemplate('bruteForceLog', array( 270 'blockedIps' => $this->bruteForceDetection->getCurrentlyBlockedIps(), 271 'blacklistedIps' => $this->systemSettings->blacklistedBruteForceIps->getValue() 272 )); 273 } 274 275 /** 276 * Error message shown when an AJAX request has no access 277 * 278 * @param string $errorMessage 279 * @return string 280 */ 281 public function ajaxNoAccess($errorMessage) 282 { 283 return sprintf( 284 '<div class="alert alert-danger"> 285 <p><strong>%s:</strong> %s</p> 286 <p><a href="%s">%s</a></p> 287 </div>', 288 Piwik::translate('General_Error'), 289 htmlentities($errorMessage, Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8', $doubleEncode = false), 290 'index.php?module=' . Piwik::getLoginPluginName(), 291 Piwik::translate('Login_LogIn') 292 ); 293 } 294 295 /** 296 * Authenticate user and password. Redirect if successful. 297 * 298 * @param string $login user name 299 * @param string $password plain-text or hashed password 300 * @param string $urlToRedirect URL to redirect to, if successfully authenticated 301 * @param bool $passwordHashed indicates if $password is hashed 302 * @return string failure message if unable to authenticate 303 */ 304 protected function authenticateAndRedirect($login, $password, $urlToRedirect = false, $passwordHashed = false) 305 { 306 Nonce::discardNonce('Login.login'); 307 308 $this->auth->setLogin($login); 309 if ($passwordHashed === false) { 310 $this->auth->setPassword($password); 311 } else { 312 $this->auth->setPasswordHash($password); 313 } 314 315 $this->sessionInitializer->initSession($this->auth); 316 317 // remove password reset entry if it exists 318 $this->passwordResetter->removePasswordResetInfo($login); 319 320 $parsedUrl = parse_url($urlToRedirect); 321 322 // only use redirect url if host is trusted 323 if (!empty($parsedUrl['host']) && !Url::isValidHost($parsedUrl['host'])) { 324 $e = new \Piwik\Exception\Exception('The redirect URL host is not valid, it is not a trusted host. If this URL is trusted, you can allow this in your config.ini.php file by adding the line <i>trusted_hosts[] = "'.Common::sanitizeInputValue($parsedUrl['host']).'"</i> under <i>[General]</i>'); 325 $e->setIsHtmlMessage(); 326 throw $e; 327 } 328 329 if (empty($urlToRedirect)) { 330 $redirect = Common::unsanitizeInputValue(Common::getRequestVar('form_redirect', false)); 331 $redirectParams = UrlHelper::getArrayFromQueryString(UrlHelper::getQueryFromUrl($redirect)); 332 $module = Common::getRequestVar('module', '', 'string', $redirectParams); 333 // when module is login, we redirect to home... 334 if (!empty($module) && $module !== 'Login' && $module !== Piwik::getLoginPluginName() && $redirect) { 335 $host = Url::getHostFromUrl($redirect); 336 $currentHost = Url::getHost(); 337 $currentHost = explode(':', $currentHost, 2)[0]; 338 339 // we only redirect to a trusted host 340 if (!empty($host) && !empty($currentHost) && $host == $currentHost && Url::isValidHost($host) 341 ) { 342 $urlToRedirect = $redirect; 343 } 344 } 345 } 346 347 if (empty($urlToRedirect)) { 348 $urlToRedirect = Url::getCurrentUrlWithoutQueryString(); 349 } 350 351 Url::redirectToUrl($urlToRedirect); 352 } 353 354 /** 355 * Reset password action. Stores new password as hash and sends email 356 * to confirm use. 357 * 358 */ 359 function resetPassword() 360 { 361 $infoMessage = null; 362 $formErrors = null; 363 364 $form = new FormResetPassword(); 365 if ($form->validate()) { 366 $nonce = $form->getSubmitValue('form_nonce'); 367 $errorMessage = Nonce::verifyNonceWithErrorMessage('Login.login', $nonce); 368 if ($errorMessage === "") { 369 $formErrors = $this->resetPasswordFirstStep($form); 370 if (empty($formErrors)) { 371 $infoMessage = Piwik::translate('Login_ConfirmationLinkSent'); 372 } 373 } else { 374 $formErrors = array($errorMessage); 375 } 376 } else { 377 // if invalid, display error 378 $formData = $form->getFormData(); 379 $formErrors = $formData['errors']; 380 } 381 382 $view = new View('@Login/resetPassword'); 383 $view->infoMessage = $infoMessage; 384 $view->formErrors = $formErrors; 385 386 return $view->render(); 387 } 388 389 /** 390 * Saves password reset info and sends confirmation email. 391 * 392 * @param QuickForm2 $form 393 * @return array Error message(s) if an error occurs. 394 */ 395 protected function resetPasswordFirstStep($form) 396 { 397 $loginMail = $form->getSubmitValue('form_login'); 398 $password = $form->getSubmitValue('form_password'); 399 400 try { 401 $this->passwordResetter->initiatePasswordResetProcess($loginMail, $password); 402 } catch (Exception $ex) { 403 Log::debug($ex); 404 405 return array($ex->getMessage()); 406 } 407 408 return null; 409 } 410 411 /** 412 * Password reset confirmation action. Finishes the password reset process. 413 * Users visit this action from a link supplied in an email. 414 */ 415 public function confirmResetPassword() 416 { 417 if (!Url::isValidHost()) { 418 throw new Exception("Cannot confirm reset password with untrusted hostname!"); 419 } 420 421 $errorMessage = null; 422 $passwordHash = null; 423 424 $login = Common::getRequestVar('login'); 425 $resetToken = Common::getRequestVar('resetToken'); 426 427 try { 428 $passwordHash = $this->passwordResetter->checkValidConfirmPasswordToken($login, $resetToken); 429 } catch (Exception $ex) { 430 Log::debug($ex); 431 432 $errorMessage = $ex->getMessage(); 433 } 434 435 if (!empty($errorMessage)) { 436 return $this->login($errorMessage); 437 } 438 439 if (!empty($_POST['nonce']) 440 && !empty($_POST['mtmpasswordconfirm']) 441 && !empty($resetToken) 442 && !empty($login) 443 && !empty($passwordHash) 444 && empty($errorMessage)) { 445 Nonce::checkNonce(self::NONCE_CONFIRMRESETPASSWORD, $_POST['nonce']); 446 if ($this->passwordResetter->doesResetPasswordHashMatchesPassword($_POST['mtmpasswordconfirm'], $passwordHash)) { 447 $this->passwordResetter->setHashedPasswordForLogin($login, $passwordHash); 448 return $this->resetPasswordSuccess(); 449 } else { 450 $errorMessage = Piwik::translate('Login_ConfirmPasswordResetWrongPassword'); 451 } 452 } 453 454 $nonce = Nonce::getNonce(self::NONCE_CONFIRMRESETPASSWORD); 455 456 return $this->renderTemplateAs('confirmResetPassword', array( 457 'nonce' => $nonce, 458 'errorMessage' => $errorMessage 459 ), 'basic'); 460 } 461 462 /** 463 * The action used after a password is successfully reset. Displays the login 464 * screen with an extra message. A separate action is used instead of returning 465 * the HTML in confirmResetPassword so the resetToken won't be in the URL. 466 */ 467 public function resetPasswordSuccess() 468 { 469 $_POST = array(); // prevent showing error message username and password is missing 470 return $this->login($errorMessage = null, $infoMessage = Piwik::translate('Login_PasswordChanged')); 471 } 472 473 /** 474 * Clear session information 475 * 476 * @return void 477 */ 478 public static function clearSession() 479 { 480 $sessionFingerprint = new Session\SessionFingerprint(); 481 $sessionFingerprint->clear(); 482 483 Session::expireSessionCookie(); 484 } 485 486 /** 487 * Logout current user 488 * 489 * @return void 490 */ 491 public function logout() 492 { 493 Piwik::postEvent('Login.logout', array(Piwik::getCurrentUserLogin())); 494 495 self::clearSession(); 496 497 $logoutUrl = @Config::getInstance()->General['login_logout_url']; 498 if (empty($logoutUrl)) { 499 Piwik::redirectToModule('CoreHome'); 500 } else { 501 Url::redirectToUrl($logoutUrl); 502 } 503 } 504} 505