PageRenderTime 48ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/Login/Controller.php

https://github.com/CodeYellowBV/piwik
PHP | 465 lines | 263 code | 60 blank | 142 comment | 29 complexity | 675804942109f3ef14a84e9d7be6aa1a MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik\Plugins\Login;
  10. use Exception;
  11. use Piwik\Common;
  12. use Piwik\Config;
  13. use Piwik\Cookie;
  14. use Piwik\IP;
  15. use Piwik\Mail;
  16. use Piwik\Nonce;
  17. use Piwik\Piwik;
  18. use Piwik\Plugins\UsersManager\API;
  19. use Piwik\Plugins\UsersManager\UsersManager;
  20. use Piwik\QuickForm2;
  21. use Piwik\Session;
  22. use Piwik\SettingsPiwik;
  23. use Piwik\Url;
  24. use Piwik\View;
  25. require_once PIWIK_INCLUDE_PATH . '/core/Config.php';
  26. /**
  27. * Login controller
  28. *
  29. */
  30. class Controller extends \Piwik\Plugin\Controller
  31. {
  32. /**
  33. * Generate hash on user info and password
  34. *
  35. * @param string $userInfo User name, email, etc
  36. * @param string $password
  37. * @return string
  38. */
  39. private function generateHash($userInfo, $password)
  40. {
  41. // mitigate rainbow table attack
  42. $passwordLen = strlen($password) / 2;
  43. $hash = Common::hash(
  44. $userInfo . substr($password, 0, $passwordLen)
  45. . SettingsPiwik::getSalt() . substr($password, $passwordLen)
  46. );
  47. return $hash;
  48. }
  49. /**
  50. * Default action
  51. *
  52. * @param none
  53. * @return string
  54. */
  55. function index()
  56. {
  57. return $this->login();
  58. }
  59. /**
  60. * Login form
  61. *
  62. * @param string $messageNoAccess Access error message
  63. * @param bool $infoMessage
  64. * @internal param string $currentUrl Current URL
  65. * @return string
  66. */
  67. function login($messageNoAccess = null, $infoMessage = false)
  68. {
  69. $form = new FormLogin();
  70. if ($form->validate()) {
  71. $nonce = $form->getSubmitValue('form_nonce');
  72. if (Nonce::verifyNonce('Login.login', $nonce)) {
  73. $login = $form->getSubmitValue('form_login');
  74. $password = $form->getSubmitValue('form_password');
  75. $rememberMe = $form->getSubmitValue('form_rememberme') == '1';
  76. $md5Password = md5($password);
  77. try {
  78. $this->authenticateAndRedirect($login, $md5Password, $rememberMe);
  79. } catch (Exception $e) {
  80. $messageNoAccess = $e->getMessage();
  81. }
  82. } else {
  83. $messageNoAccess = $this->getMessageExceptionNoAccess();
  84. }
  85. }
  86. $view = new View('@Login/login');
  87. $view->AccessErrorString = $messageNoAccess;
  88. $view->infoMessage = nl2br($infoMessage);
  89. $view->addForm($form);
  90. $this->configureView($view);
  91. self::setHostValidationVariablesView($view);
  92. return $view->render();
  93. }
  94. /**
  95. * Configure common view properties
  96. *
  97. * @param View $view
  98. */
  99. private function configureView($view)
  100. {
  101. $this->setBasicVariablesView($view);
  102. $view->linkTitle = Piwik::getRandomTitle();
  103. // crsf token: don't trust the submitted value; generate/fetch it from session data
  104. $view->nonce = Nonce::getNonce('Login.login');
  105. }
  106. /**
  107. * Form-less login
  108. * @see how to use it on http://piwik.org/faq/how-to/#faq_30
  109. * @throws Exception
  110. * @return void
  111. */
  112. function logme()
  113. {
  114. $password = Common::getRequestVar('password', null, 'string');
  115. $this->checkPasswordHash($password);
  116. $login = Common::getRequestVar('login', null, 'string');
  117. if (Piwik::hasTheUserSuperUserAccess($login)) {
  118. throw new Exception(Piwik::translate('Login_ExceptionInvalidSuperUserAccessAuthenticationMethod', array("logme")));
  119. }
  120. $currentUrl = 'index.php';
  121. if (($idSite = Common::getRequestVar('idSite', false, 'int')) !== false) {
  122. $currentUrl .= '?idSite=' . $idSite;
  123. }
  124. $urlToRedirect = Common::getRequestVar('url', $currentUrl, 'string');
  125. $urlToRedirect = Common::unsanitizeInputValue($urlToRedirect);
  126. $this->authenticateAndRedirect($login, $password, false, $urlToRedirect);
  127. }
  128. /**
  129. * Authenticate user and password. Redirect if successful.
  130. *
  131. * @param string $login user name
  132. * @param string $md5Password md5 hash of password
  133. * @param bool $rememberMe Remember me?
  134. * @param string $urlToRedirect URL to redirect to, if successfully authenticated
  135. * @return string failure message if unable to authenticate
  136. */
  137. protected function authenticateAndRedirect($login, $md5Password, $rememberMe, $urlToRedirect = false)
  138. {
  139. Nonce::discardNonce('Login.login');
  140. \Piwik\Registry::get('auth')->initSession($login, $md5Password, $rememberMe);
  141. if(empty($urlToRedirect)) {
  142. $urlToRedirect = Url::getCurrentUrlWithoutQueryString();
  143. }
  144. Url::redirectToUrl($urlToRedirect);
  145. }
  146. protected function getMessageExceptionNoAccess()
  147. {
  148. $message = Piwik::translate('Login_InvalidNonceOrHeadersOrReferrer', array('<a href="?module=Proxy&action=redirect&url=' . urlencode('http://piwik.org/faq/how-to-install/#faq_98') . '" target="_blank">', '</a>'));
  149. // Should mention trusted_hosts or link to FAQ
  150. return $message;
  151. }
  152. /**
  153. * Reset password action. Stores new password as hash and sends email
  154. * to confirm use.
  155. *
  156. * @param none
  157. */
  158. function resetPassword()
  159. {
  160. $infoMessage = null;
  161. $formErrors = null;
  162. $form = new FormResetPassword();
  163. if ($form->validate()) {
  164. $nonce = $form->getSubmitValue('form_nonce');
  165. if (Nonce::verifyNonce('Login.login', $nonce)) {
  166. $formErrors = $this->resetPasswordFirstStep($form);
  167. if (empty($formErrors)) {
  168. $infoMessage = Piwik::translate('Login_ConfirmationLinkSent');
  169. }
  170. } else {
  171. $formErrors = array($this->getMessageExceptionNoAccess());
  172. }
  173. } else {
  174. // if invalid, display error
  175. $formData = $form->getFormData();
  176. $formErrors = $formData['errors'];
  177. }
  178. $view = new View('@Login/resetPassword');
  179. $view->infoMessage = $infoMessage;
  180. $view->formErrors = $formErrors;
  181. return $view->render();
  182. }
  183. /**
  184. * Saves password reset info and sends confirmation email.
  185. *
  186. * @param QuickForm2 $form
  187. * @return array Error message(s) if an error occurs.
  188. */
  189. private function resetPasswordFirstStep($form)
  190. {
  191. $loginMail = $form->getSubmitValue('form_login');
  192. $password = $form->getSubmitValue('form_password');
  193. // check the password
  194. try {
  195. UsersManager::checkPassword($password);
  196. } catch (Exception $ex) {
  197. return array($ex->getMessage());
  198. }
  199. // get the user's login
  200. if ($loginMail === 'anonymous') {
  201. return array(Piwik::translate('Login_InvalidUsernameEmail'));
  202. }
  203. $user = self::getUserInformation($loginMail);
  204. if ($user === null) {
  205. return array(Piwik::translate('Login_InvalidUsernameEmail'));
  206. }
  207. $login = $user['login'];
  208. // if valid, store password information in options table, then...
  209. Login::savePasswordResetInfo($login, $password);
  210. // ... send email with confirmation link
  211. try {
  212. $this->sendEmailConfirmationLink($user);
  213. } catch (Exception $ex) {
  214. // remove password reset info
  215. Login::removePasswordResetInfo($login);
  216. return array($ex->getMessage() . Piwik::translate('Login_ContactAdmin'));
  217. }
  218. return null;
  219. }
  220. /**
  221. * Sends email confirmation link for a password reset request.
  222. *
  223. * @param array $user User info for the requested password reset.
  224. */
  225. private function sendEmailConfirmationLink($user)
  226. {
  227. $login = $user['login'];
  228. $email = $user['email'];
  229. // construct a password reset token from user information
  230. $resetToken = self::generatePasswordResetToken($user);
  231. $ip = IP::getIpFromHeader();
  232. $url = Url::getCurrentUrlWithoutQueryString()
  233. . "?module=Login&action=confirmResetPassword&login=" . urlencode($login)
  234. . "&resetToken=" . urlencode($resetToken);
  235. // send email with new password
  236. $mail = new Mail();
  237. $mail->addTo($email, $login);
  238. $mail->setSubject(Piwik::translate('Login_MailTopicPasswordChange'));
  239. $bodyText = str_replace(
  240. '\n',
  241. "\n",
  242. sprintf(Piwik::translate('Login_MailPasswordChangeBody'), $login, $ip, $url)
  243. ) . "\n";
  244. $mail->setBodyText($bodyText);
  245. $fromEmailName = Config::getInstance()->General['login_password_recovery_email_name'];
  246. $fromEmailAddress = Config::getInstance()->General['login_password_recovery_email_address'];
  247. $mail->setFrom($fromEmailAddress, $fromEmailName);
  248. @$mail->send();
  249. }
  250. /**
  251. * Password reset confirmation action. Finishes the password reset process.
  252. * Users visit this action from a link supplied in an email.
  253. */
  254. public function confirmResetPassword()
  255. {
  256. $errorMessage = null;
  257. $login = Common::getRequestVar('login', '');
  258. $resetToken = Common::getRequestVar('resetToken', '');
  259. try {
  260. // get password reset info & user info
  261. $user = self::getUserInformation($login);
  262. if ($user === null) {
  263. throw new Exception(Piwik::translate('Login_InvalidUsernameEmail'));
  264. }
  265. // check that the reset token is valid
  266. $resetPassword = Login::getPasswordToResetTo($login);
  267. if ($resetPassword === false || !self::isValidToken($resetToken, $user)) {
  268. throw new Exception(Piwik::translate('Login_InvalidOrExpiredToken'));
  269. }
  270. // reset password of user
  271. $this->setNewUserPassword($user, $resetPassword);
  272. } catch (Exception $ex) {
  273. $errorMessage = $ex->getMessage();
  274. }
  275. if (is_null($errorMessage)) // if success, show login w/ success message
  276. {
  277. $this->redirectToIndex(Piwik::getLoginPluginName(), 'resetPasswordSuccess');
  278. return;
  279. } else {
  280. // show login page w/ error. this will keep the token in the URL
  281. return $this->login($errorMessage);
  282. }
  283. }
  284. /**
  285. * Sets the password for a user.
  286. *
  287. * @param array $user User info.
  288. * @param string $passwordHash The hashed password to use.
  289. * @throws Exception
  290. */
  291. private function setNewUserPassword($user, $passwordHash)
  292. {
  293. $this->checkPasswordHash($passwordHash);
  294. API::getInstance()->updateUser(
  295. $user['login'], $passwordHash, $email = false, $alias = false, $isPasswordHashed = true);
  296. }
  297. /**
  298. * The action used after a password is successfully reset. Displays the login
  299. * screen with an extra message. A separate action is used instead of returning
  300. * the HTML in confirmResetPassword so the resetToken won't be in the URL.
  301. */
  302. public function resetPasswordSuccess()
  303. {
  304. return $this->login($errorMessage = null, $infoMessage = Piwik::translate('Login_PasswordChanged'));
  305. }
  306. /**
  307. * Get user information
  308. *
  309. * @param string $loginMail user login or email address
  310. * @return array ("login" => '...', "email" => '...', "password" => '...') or null, if user not found
  311. */
  312. protected function getUserInformation($loginMail)
  313. {
  314. Piwik::setUserHasSuperUserAccess();
  315. $user = null;
  316. if (API::getInstance()->userExists($loginMail)) {
  317. $user = API::getInstance()->getUser($loginMail);
  318. } else if (API::getInstance()->userEmailExists($loginMail)) {
  319. $user = API::getInstance()->getUserByEmail($loginMail);
  320. }
  321. return $user;
  322. }
  323. /**
  324. * Generate a password reset token. Expires in (roughly) 24 hours.
  325. *
  326. * @param array $user user information
  327. * @param int $timestamp Unix timestamp
  328. * @return string generated token
  329. */
  330. protected function generatePasswordResetToken($user, $timestamp = null)
  331. {
  332. /*
  333. * Piwik does not store the generated password reset token.
  334. * This avoids a database schema change and SQL queries to store, retrieve, and purge (expired) tokens.
  335. */
  336. if (!$timestamp) {
  337. $timestamp = time() + 24 * 60 * 60; /* +24 hrs */
  338. }
  339. $expiry = strftime('%Y%m%d%H', $timestamp);
  340. $token = $this->generateHash(
  341. $expiry . $user['login'] . $user['email'],
  342. $user['password']
  343. );
  344. return $token;
  345. }
  346. /**
  347. * Validate token.
  348. *
  349. * @param string $token
  350. * @param array $user user information
  351. * @return bool true if valid, false otherwise
  352. */
  353. protected function isValidToken($token, $user)
  354. {
  355. $now = time();
  356. // token valid for 24 hrs (give or take, due to the coarse granularity in our strftime format string)
  357. for ($i = 0; $i <= 24; $i++) {
  358. $generatedToken = self::generatePasswordResetToken($user, $now + $i * 60 * 60);
  359. if ($generatedToken === $token) {
  360. return true;
  361. }
  362. }
  363. // fails if token is invalid, expired, password already changed, other user information has changed, ...
  364. return false;
  365. }
  366. /**
  367. * Clear session information
  368. *
  369. * @param none
  370. * @return void
  371. */
  372. static public function clearSession()
  373. {
  374. $authCookieName = Config::getInstance()->General['login_cookie_name'];
  375. $cookie = new Cookie($authCookieName);
  376. $cookie->delete();
  377. Session::expireSessionCookie();
  378. }
  379. /**
  380. * Logout current user
  381. *
  382. * @param none
  383. * @return void
  384. */
  385. public function logout()
  386. {
  387. self::clearSession();
  388. $logoutUrl = @Config::getInstance()->General['login_logout_url'];
  389. if (empty($logoutUrl)) {
  390. Piwik::redirectToModule('CoreHome');
  391. } else {
  392. Url::redirectToUrl($logoutUrl);
  393. }
  394. }
  395. /**
  396. * @param $password
  397. * @throws \Exception
  398. */
  399. protected function checkPasswordHash($password)
  400. {
  401. if (strlen($password) != 32) {
  402. throw new Exception(Piwik::translate('Login_ExceptionPasswordMD5HashExpected'));
  403. }
  404. }
  405. }