login.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991
  1. <?php
  2. /**
  3. * @package Grav\Plugin\Login
  4. *
  5. * @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Plugin;
  9. use Grav\Common\Grav;
  10. use Grav\Common\Language\Language;
  11. use Grav\Common\Page\Page;
  12. use Grav\Common\Page\Pages;
  13. use Grav\Common\Plugin;
  14. use Grav\Common\Twig\Twig;
  15. use Grav\Common\User\User;
  16. use Grav\Common\Utils;
  17. use Grav\Common\Uri;
  18. use Grav\Plugin\Login\Events\UserLoginEvent;
  19. use Grav\Plugin\Login\Login;
  20. use Grav\Plugin\Login\Controller;
  21. use Grav\Plugin\Login\RememberMe\RememberMe;
  22. use RocketTheme\Toolbox\Event\Event;
  23. use RocketTheme\Toolbox\Session\Message;
  24. /**
  25. * Class LoginPlugin
  26. * @package Grav\Plugin\Login
  27. */
  28. class LoginPlugin extends Plugin
  29. {
  30. const TMP_COOKIE_NAME = 'tmp-message';
  31. /** @var string */
  32. protected $route;
  33. /** @var string */
  34. protected $route_register;
  35. /** @var string */
  36. protected $route_forgot;
  37. /** @var bool */
  38. protected $authenticated = true;
  39. /** @var Login */
  40. protected $login;
  41. protected $redirect_to_login;
  42. /**
  43. * @return array
  44. */
  45. public static function getSubscribedEvents()
  46. {
  47. return [
  48. 'onPluginsInitialized' => [['initializeSession', 10000], ['initializeLogin', 1000]],
  49. 'onTask.login.login' => ['loginController', 0],
  50. 'onTask.login.twofa' => ['loginController', 0],
  51. 'onTask.login.forgot' => ['loginController', 0],
  52. 'onTask.login.logout' => ['loginController', 0],
  53. 'onTask.login.reset' => ['loginController', 0],
  54. 'onPagesInitialized' => [['storeReferrerPage', 0], ['pageVisibility', 0]],
  55. 'onPageInitialized' => ['authorizePage', 0],
  56. 'onPageFallBackUrl' => ['authorizeFallBackUrl', 0],
  57. 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
  58. 'onTwigSiteVariables' => ['onTwigSiteVariables', -100000],
  59. 'onFormProcessed' => ['onFormProcessed', 0],
  60. 'onUserLoginAuthenticate' => [['userLoginAuthenticateByRegistration', 10002], ['userLoginAuthenticateByRememberMe', 10001], ['userLoginAuthenticateByEmail', 10000], ['userLoginAuthenticate', 0]],
  61. 'onUserLoginAuthorize' => ['userLoginAuthorize', 0],
  62. 'onUserLoginFailure' => ['userLoginFailure', 0],
  63. 'onUserLogin' => ['userLogin', 0],
  64. 'onUserLogout' => ['userLogout', 0],
  65. ];
  66. }
  67. /**
  68. * [onPluginsInitialized] Initialize login plugin if path matches.
  69. * @throws \RuntimeException
  70. */
  71. public function initializeSession()
  72. {
  73. // Check to ensure sessions are enabled.
  74. if (!$this->config->get('system.session.enabled')) {
  75. throw new \RuntimeException('The Login plugin requires "system.session" to be enabled');
  76. }
  77. // Autoload classes
  78. $autoload = __DIR__ . '/vendor/autoload.php';
  79. if (!is_file($autoload)) {
  80. throw new \RuntimeException('Login Plugin failed to load. Composer dependencies not met.');
  81. }
  82. require_once $autoload;
  83. // Define login service.
  84. $this->grav['login'] = function (Grav $c) {
  85. return new Login($c);
  86. };
  87. // Define current user service.
  88. $this->grav['user'] = function (Grav $c) {
  89. $session = $c['session'];
  90. if (empty($session->user)) {
  91. $session->user = $c['login']->login(['username' => ''], ['remember_me' => true, 'remember_me_login' => true]);
  92. }
  93. return $session->user;
  94. };
  95. }
  96. /**
  97. * [onPluginsInitialized] Initialize login plugin if path matches.
  98. * @throws \RuntimeException
  99. */
  100. public function initializeLogin()
  101. {
  102. $this->login = $this->grav['login'];
  103. /** @var Uri $uri */
  104. $uri = $this->grav['uri'];
  105. // Admin has its own login; make sure we're not in admin.
  106. if (!isset($this->grav['admin'])) {
  107. $this->route = $this->config->get('plugins.login.route');
  108. }
  109. $path = $uri->path();
  110. $this->redirect_to_login = $this->config->get('plugins.login.redirect_to_login');
  111. // Register route to login page if it has been set.
  112. if ($this->route && $this->route === $path) {
  113. $this->enable([
  114. 'onPagesInitialized' => ['addLoginPage', 0],
  115. ]);
  116. return;
  117. }
  118. if ($path === $this->config->get('plugins.login.route_forgot')) {
  119. $this->enable([
  120. 'onPagesInitialized' => ['addForgotPage', 0],
  121. ]);
  122. return;
  123. }
  124. if ($path === $this->config->get('plugins.login.route_reset')) {
  125. $this->enable([
  126. 'onPagesInitialized' => ['addResetPage', 0],
  127. ]);
  128. return;
  129. }
  130. if ($path === $this->config->get('plugins.login.route_register')) {
  131. if ($this->config->get('plugins.login.user_registration.enabled')) {
  132. $this->enable([
  133. 'onPagesInitialized' => ['addRegisterPage', 0],
  134. ]);
  135. } else {
  136. throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.REGISTRATION_DISABLED'), 404);
  137. }
  138. return;
  139. }
  140. if ($path === $this->config->get('plugins.login.route_activate')) {
  141. $this->enable([
  142. 'onPagesInitialized' => ['handleUserActivation', 0],
  143. ]);
  144. return;
  145. }
  146. if ($path === $this->config->get('plugins.login.route_profile')) {
  147. $this->enable([
  148. 'onPagesInitialized' => ['addProfilePage', 0],
  149. ]);
  150. return;
  151. }
  152. }
  153. /**
  154. * Optional ability to dynamically set visibility based on page access and page header
  155. * that states `login.visibility_requires_access: true`
  156. *
  157. * Note that this setting may be slow on large sites as it loads all pages into memory for each page load!
  158. *
  159. * @param Event $e
  160. */
  161. public function pageVisibility(Event $e)
  162. {
  163. if ($this->config->get('plugins.login.dynamic_page_visibility')) {
  164. /** @var Pages $pages */
  165. $pages = $e['pages'];
  166. $user = $this->grav['user'];
  167. foreach ($pages->instances() as $page) {
  168. $header = $page->header();
  169. if (isset($header->login['visibility_requires_access'])) {
  170. $config = $this->mergeConfig($page);
  171. $access = $this->login->isUserAuthorizedForPage($user, $page, $config);
  172. if ($access === false) {
  173. $page->visible(false);
  174. }
  175. }
  176. }
  177. }
  178. }
  179. /**
  180. * [onPagesInitialized]
  181. */
  182. public function storeReferrerPage()
  183. {
  184. $invalid_redirect_routes = [
  185. $this->config->get('plugins.login.route') ?: '/login',
  186. $this->config->get('plugins.login.route_register') ?: '/register',
  187. $this->config->get('plugins.login.route_activate') ?: '/activate_user',
  188. $this->config->get('plugins.login.route_forgot') ?: '/forgot_password',
  189. $this->config->get('plugins.login.route_reset') ?: '/reset_password',
  190. ];
  191. /** @var Uri $uri */
  192. $uri = $this->grav['uri'];
  193. $current_route = $uri->route();
  194. $redirect = $this->grav['config']->get('plugins.login.redirect_after_login');
  195. if (!$redirect && !in_array($current_route, $invalid_redirect_routes, true)) {
  196. // No login redirect set in the configuration; can we redirect to the current page?
  197. $allowed = true;
  198. /** @var Page $page */
  199. $page = $this->grav['pages']->dispatch($current_route);
  200. if ($page) {
  201. $header = $page->header();
  202. if (isset($header->login_redirect_here) && $header->login_redirect_here === false) {
  203. $allowed = false;
  204. }
  205. if ($allowed && $page->routable()) {
  206. $redirect = $page->route() . ($uri->params() ?: '');
  207. }
  208. }
  209. }
  210. $this->grav['session']->redirect_after_login = $redirect;
  211. }
  212. /**
  213. * Add Login page
  214. * @throws \Exception
  215. */
  216. public function addLoginPage()
  217. {
  218. /** @var Pages $pages */
  219. $pages = $this->grav['pages'];
  220. $page = $pages->dispatch($this->route);
  221. if (!$page) {
  222. // Only add login page if it hasn't already been defined.
  223. $page = new Page;
  224. $page->init(new \SplFileInfo(__DIR__ . '/pages/login.md'));
  225. $page->slug(basename($this->route));
  226. $pages->addPage($page, $this->route);
  227. }
  228. }
  229. /**
  230. * Add Login page
  231. * @throws \Exception
  232. */
  233. public function addForgotPage()
  234. {
  235. $route = $this->config->get('plugins.login.route_forgot');
  236. /** @var Pages $pages */
  237. $pages = $this->grav['pages'];
  238. $page = $pages->dispatch($route);
  239. if (!$page) {
  240. // Only add forgot page if it hasn't already been defined.
  241. $page = new Page;
  242. $page->init(new \SplFileInfo(__DIR__ . '/pages/forgot.md'));
  243. $page->slug(basename($route));
  244. $pages->addPage($page, $route);
  245. }
  246. }
  247. /**
  248. * Add Reset page
  249. * @throws \Exception
  250. */
  251. public function addResetPage()
  252. {
  253. $route = $this->config->get('plugins.login.route_reset');
  254. $uri = $this->grav['uri'];
  255. $token = $uri->param('token');
  256. $user = $uri->param('user');
  257. if (!$user || !$token) {
  258. return;
  259. }
  260. /** @var Pages $pages */
  261. $pages = $this->grav['pages'];
  262. $page = $pages->dispatch($route);
  263. if (!$page) {
  264. // Only add login page if it hasn't already been defined.
  265. $page = new Page;
  266. $page->init(new \SplFileInfo(__DIR__ . '/pages/reset.md'));
  267. $page->slug(basename($route));
  268. $pages->addPage($page, $route);
  269. }
  270. }
  271. /**
  272. * Add Register page
  273. * @throws \Exception
  274. */
  275. public function addRegisterPage()
  276. {
  277. $route = $this->config->get('plugins.login.route_register');
  278. /** @var Pages $pages */
  279. $pages = $this->grav['pages'];
  280. $page = $pages->dispatch($route);
  281. if (!$page) {
  282. $page = new Page;
  283. $page->init(new \SplFileInfo(__DIR__ . '/pages/register.md'));
  284. $page->template('form');
  285. $page->slug(basename($route));
  286. $pages->addPage($page, $route);
  287. }
  288. }
  289. /**
  290. * Handle user activation
  291. * @throws \RuntimeException
  292. */
  293. public function handleUserActivation()
  294. {
  295. /** @var Uri $uri */
  296. $uri = $this->grav['uri'];
  297. /** @var Message $messages */
  298. $messages = $this->grav['messages'];
  299. $username = $uri->param('username');
  300. $token = $uri->param('token');
  301. $user = User::load($username);
  302. if (empty($user->activation_token)) {
  303. $message = $this->grav['language']->translate('PLUGIN_LOGIN.INVALID_REQUEST');
  304. $messages->add($message, 'error');
  305. } else {
  306. list($good_token, $expire) = explode('::', $user->activation_token, 2);
  307. if ($good_token === $token) {
  308. if (time() > $expire) {
  309. $message = $this->grav['language']->translate('PLUGIN_LOGIN.ACTIVATION_LINK_EXPIRED');
  310. $messages->add($message, 'error');
  311. } else {
  312. $user['state'] = 'enabled';
  313. unset($user['activation_token']);
  314. $user->save();
  315. $message = $this->grav['language']->translate('PLUGIN_LOGIN.USER_ACTIVATED_SUCCESSFULLY');
  316. $messages->add($message, 'info');
  317. if ($this->config->get('plugins.login.user_registration.options.send_welcome_email', false)) {
  318. $this->login->sendWelcomeEmail($user);
  319. }
  320. if ($this->config->get('plugins.login.user_registration.options.send_notification_email', false)) {
  321. $this->login->sendNotificationEmail($user);
  322. }
  323. if ($this->config->get('plugins.login.user_registration.options.login_after_registration', false)) {
  324. $this->login->login(['username' => $username], ['after_registration' => true]);
  325. }
  326. }
  327. } else {
  328. $message = $this->grav['language']->translate('PLUGIN_LOGIN.INVALID_REQUEST');
  329. $messages->add($message, 'error');
  330. }
  331. }
  332. $redirect_route = $this->config->get('plugins.login.user_registration.redirect_after_activation', '/');
  333. $this->grav->redirect($redirect_route);
  334. }
  335. /**
  336. * Add Profile page
  337. */
  338. public function addProfilePage()
  339. {
  340. $route = $this->config->get('plugins.login.route_profile');
  341. /** @var Pages $pages */
  342. $pages = $this->grav['pages'];
  343. $page = $pages->dispatch($route);
  344. if (!$page) {
  345. // Only add forgot page if it hasn't already been defined.
  346. $page = new Page;
  347. $page->init(new \SplFileInfo(__DIR__ . "/pages/profile.md"));
  348. $page->slug(basename($route));
  349. $pages->addPage($page, $route);
  350. }
  351. $this->storeReferrerPage();
  352. }
  353. /**
  354. * Set Unauthorized page
  355. * @throws \Exception
  356. */
  357. public function setUnauthorizedPage()
  358. {
  359. $route = $this->config->get('plugins.login.route_unauthorized');
  360. /** @var Pages $pages */
  361. $pages = $this->grav['pages'];
  362. $page = $pages->dispatch($route);
  363. if (!$page) {
  364. $page = new Page;
  365. $page->init(new \SplFileInfo(__DIR__ . '/pages/unauthorized.md'));
  366. $page->template('default');
  367. $page->slug(basename($route));
  368. $pages->addPage($page, $route);
  369. }
  370. unset($this->grav['page']);
  371. $this->grav['page'] = $page;
  372. }
  373. /**
  374. * Initialize login controller
  375. */
  376. public function loginController()
  377. {
  378. /** @var Uri $uri */
  379. $uri = $this->grav['uri'];
  380. $task = !empty($_POST['task']) ? $_POST['task'] : $uri->param('task');
  381. $task = substr($task, strlen('login.'));
  382. $post = !empty($_POST) ? $_POST : [];
  383. switch ($task) {
  384. case 'login':
  385. if (!isset($post['login-form-nonce']) || !Utils::verifyNonce($post['login-form-nonce'], 'login-form')) {
  386. $this->grav['messages']->add($this->grav['language']->translate('PLUGIN_LOGIN.ACCESS_DENIED'),
  387. 'info');
  388. $twig = $this->grav['twig'];
  389. $twig->twig_vars['notAuthorized'] = true;
  390. return;
  391. }
  392. break;
  393. case 'forgot':
  394. if (!isset($post['forgot-form-nonce']) || !Utils::verifyNonce($post['forgot-form-nonce'], 'forgot-form')) {
  395. $this->grav['messages']->add($this->grav['language']->translate('PLUGIN_LOGIN.ACCESS_DENIED'),'info');
  396. return;
  397. }
  398. break;
  399. }
  400. $controller = new Controller($this->grav, $task, $post);
  401. $controller->execute();
  402. $controller->redirect();
  403. }
  404. /**
  405. * Authorize the Page fallback url (page media accessed through the page route)
  406. */
  407. public function authorizeFallBackUrl()
  408. {
  409. if ($this->config->get('plugins.login.protect_protected_page_media', false)) {
  410. $page_url = dirname($this->grav['uri']->path());
  411. $page = $this->grav['pages']->find($page_url);
  412. unset($this->grav['page']);
  413. $this->grav['page'] = $page;
  414. $this->authorizePage();
  415. }
  416. }
  417. /**
  418. * [onPageInitialized] Authorize Page
  419. */
  420. public function authorizePage()
  421. {
  422. if (!$this->authenticated) {
  423. return;
  424. }
  425. /** @var User $user */
  426. $user = $this->grav['user'];
  427. /** @var Page $page */
  428. $page = $this->grav['page'];
  429. if (!$page || $this->grav['login']->isUserAuthorizedForPage($user, $page, $this->mergeConfig($page))) {
  430. return;
  431. }
  432. // If this is not an HTML page request, simply throw a 403 error
  433. $uri_extension = $this->grav['uri']->extension('html');
  434. $supported_types = $this->config->get('media.types');
  435. if ($uri_extension !== 'html' && array_key_exists($uri_extension, $supported_types)) {
  436. header('HTTP/1.0 403 Forbidden');
  437. exit;
  438. }
  439. $authorized = $user->authenticated && $user->authorized;
  440. // User is not logged in; redirect to login page.
  441. if ($this->redirect_to_login && $this->route && !$authorized) {
  442. $this->grav->redirect($this->route, 302);
  443. }
  444. /** @var Twig $twig */
  445. $twig = $this->grav['twig'];
  446. // Reset page with login page.
  447. if (!$authorized) {
  448. if ($this->route) {
  449. $page = $this->grav['pages']->dispatch($this->route);
  450. } else {
  451. $page = new Page;
  452. // $this->grav['session']->redirect_after_login = $this->grav['uri']->path() . ($this->grav['uri']->params() ?: '');
  453. // Get the admin Login page is needed, else teh default
  454. if ($this->isAdmin()) {
  455. $login_file = $this->grav['locator']->findResource('plugins://admin/pages/admin/login.md');
  456. $page->init(new \SplFileInfo($login_file));
  457. } else {
  458. $page->init(new \SplFileInfo(__DIR__ . '/pages/login.md'));
  459. }
  460. $page->slug(basename($this->route));
  461. /** @var Pages $pages */
  462. $pages = $this->grav['pages'];
  463. $pages->addPage($page, $this->route);
  464. }
  465. $this->authenticated = false;
  466. unset($this->grav['page']);
  467. $this->grav['page'] = $page;
  468. $twig->twig_vars['form'] = new Form($page);
  469. } else {
  470. /** @var Language $l */
  471. $l = $this->grav['language'];
  472. $this->grav['messages']->add($l->translate('PLUGIN_LOGIN.ACCESS_DENIED'), 'error');
  473. $twig->twig_vars['notAuthorized'] = true;
  474. $this->setUnauthorizedPage();
  475. }
  476. }
  477. /**
  478. * [onTwigTemplatePaths] Add twig paths to plugin templates.
  479. */
  480. public function onTwigTemplatePaths()
  481. {
  482. $twig = $this->grav['twig'];
  483. $twig->twig_paths[] = __DIR__ . '/templates';
  484. }
  485. /**
  486. * [onTwigSiteVariables] Set all twig variables for generating output.
  487. */
  488. public function onTwigSiteVariables()
  489. {
  490. /** @var Twig $twig */
  491. $twig = $this->grav['twig'];
  492. $this->grav->fireEvent('onLoginPage');
  493. $extension = $this->grav['uri']->extension();
  494. $extension = $extension ?: 'html';
  495. if (!$this->authenticated) {
  496. $twig->template = "login.{$extension}.twig";
  497. }
  498. // add CSS for frontend if required
  499. if (!$this->isAdmin() && $this->config->get('plugins.login.built_in_css')) {
  500. $this->grav['assets']->add('plugin://login/css/login.css');
  501. }
  502. $task = $this->grav['uri']->param('task') ?: (isset($_POST['task']) ? $_POST['task'] : '');
  503. $task = substr($task, strlen('login.'));
  504. if ($task === 'reset') {
  505. $username = $this->grav['uri']->param('user');
  506. $token = $this->grav['uri']->param('token');
  507. if (!empty($username) && !empty($token)) {
  508. $twig->twig_vars['username'] = $username;
  509. $twig->twig_vars['token'] = $token;
  510. }
  511. } elseif ($task === 'login') {
  512. $twig->twig_vars['username'] = isset($_POST['username']) ? $_POST['username'] : '';
  513. }
  514. $flashData = $this->grav['session']->getFlashCookieObject(self::TMP_COOKIE_NAME);
  515. if (isset($flashData->message)) {
  516. $this->grav['messages']->add($flashData->message, $flashData->status);
  517. }
  518. }
  519. /**
  520. * Process the user registration, triggered by a registration form
  521. *
  522. * @param Form $form
  523. * @throws \RuntimeException
  524. */
  525. private function processUserRegistration($form, Event $event)
  526. {
  527. $language = $this->grav['language'];
  528. $messages = $this->grav['messages'];
  529. if (!$this->config->get('plugins.login.enabled')) {
  530. throw new \RuntimeException($language->translate('PLUGIN_LOGIN.PLUGIN_LOGIN_DISABLED'));
  531. }
  532. if (!$this->config->get('plugins.login.user_registration.enabled')) {
  533. throw new \RuntimeException($language->translate('PLUGIN_LOGIN.USER_REGISTRATION_DISABLED'));
  534. }
  535. // Check for existing username
  536. $username = $form->value('username');
  537. $existing_username = User::find($username,['username']);
  538. if ($existing_username->exists()) {
  539. $this->grav->fireEvent('onFormValidationError', new Event([
  540. 'form' => $form,
  541. 'message' => $language->translate([
  542. 'PLUGIN_LOGIN.USERNAME_NOT_AVAILABLE',
  543. $username
  544. ])
  545. ]));
  546. $event->stopPropagation();
  547. return;
  548. }
  549. // Check for existing email
  550. $email = $form->value('email');
  551. $existing_email = User::find($email,['email']);
  552. if ($existing_email->exists()) {
  553. $this->grav->fireEvent('onFormValidationError', new Event([
  554. 'form' => $form,
  555. 'message' => $language->translate([
  556. 'PLUGIN_LOGIN.EMAIL_NOT_AVAILABLE',
  557. $email
  558. ])
  559. ]));
  560. $event->stopPropagation();
  561. return;
  562. }
  563. $data = [];
  564. $data['username'] = $username;
  565. // if multiple password fields, check they match and set password field from it
  566. if ($this->config->get('plugins.login.user_registration.options.validate_password1_and_password2',
  567. false)
  568. ) {
  569. if ($form->value('password1') !== $form->value('password2')) {
  570. $this->grav->fireEvent('onFormValidationError', new Event([
  571. 'form' => $form,
  572. 'message' => $language->translate('PLUGIN_LOGIN.PASSWORDS_DO_NOT_MATCH')
  573. ]));
  574. $event->stopPropagation();
  575. return;
  576. }
  577. $data['password'] = $form->value('password1');
  578. }
  579. $fields = (array)$this->config->get('plugins.login.user_registration.fields', []);
  580. foreach ($fields as $field) {
  581. // Process value of field if set in the page process.register_user
  582. $default_values = (array)$this->config->get('plugins.login.user_registration.default_values');
  583. if ($default_values) {
  584. foreach ($default_values as $key => $param) {
  585. if ($key === $field) {
  586. if (is_array($param)) {
  587. $values = explode(',', $param);
  588. } else {
  589. $values = $param;
  590. }
  591. $data[$field] = $values;
  592. }
  593. }
  594. }
  595. if (!isset($data[$field]) && $form->value($field)) {
  596. $data[$field] = $form->value($field);
  597. }
  598. }
  599. if ($this->config->get('plugins.login.user_registration.options.set_user_disabled', false)) {
  600. $data['state'] = 'disabled';
  601. } else {
  602. $data['state'] = 'enabled';
  603. }
  604. $this->grav->fireEvent('onUserLoginRegisterData', new Event(['data' => &$data]));
  605. $user = $this->login->register($data);
  606. $fullname = $user->fullname ?: $user->username;
  607. if ($this->config->get('plugins.login.user_registration.options.send_activation_email', false)) {
  608. $this->login->sendActivationEmail($user);
  609. $message = $language->translate(['PLUGIN_LOGIN.ACTIVATION_NOTICE_MSG', $fullname]);
  610. $messages->add($message, 'info');
  611. } else {
  612. if ($this->config->get('plugins.login.user_registration.options.send_welcome_email', false)) {
  613. $this->login->sendWelcomeEmail($user);
  614. }
  615. if ($this->config->get('plugins.login.user_registration.options.send_notification_email', false)) {
  616. $this->login->sendNotificationEmail($user);
  617. }
  618. $message = $language->translate(['PLUGIN_LOGIN.WELCOME_NOTICE_MSG', $fullname]);
  619. $messages->add($message, 'info');
  620. }
  621. $this->grav->fireEvent('onUserLoginRegistered', new Event(['user' => $user]));
  622. if (isset($data['state']) && $data['state'] === 'enabled' && $this->config->get('plugins.login.user_registration.options.login_after_registration', false)) {
  623. $this->login->login(['username' => $username], ['after_registration' => true], ['user' => $user]);
  624. }
  625. $redirect = $this->config->get('plugins.login.user_registration.redirect_after_registration', false);
  626. if ($redirect) {
  627. $this->grav->redirect($redirect);
  628. }
  629. }
  630. /**
  631. * Save user profile information
  632. *
  633. * @param Form $form
  634. * @param Event $event
  635. * @return bool
  636. */
  637. private function processUserProfile($form, Event $event)
  638. {
  639. /** @var User $user */
  640. $user = $this->grav['user'];
  641. $language = $this->grav['language'];
  642. // Don't save if user doesn't exist
  643. if (!$user->exists()) {
  644. $this->grav->fireEvent('onFormValidationError', new Event([
  645. 'form' => $form,
  646. 'message' => $language->translate('PLUGIN_LOGIN.USER_IS_REMOTE_ONLY')
  647. ]));
  648. $event->stopPropagation();
  649. return;
  650. }
  651. // Stop overloading of username
  652. $username = $form->value('username');
  653. if (isset($username)) {
  654. $this->grav->fireEvent('onFormValidationError', new Event([
  655. 'form' => $form,
  656. 'message' => $language->translate([
  657. 'PLUGIN_LOGIN.USERNAME_NOT_AVAILABLE',
  658. $username
  659. ])
  660. ]));
  661. $event->stopPropagation();
  662. return;
  663. }
  664. // Check for existing email
  665. $email = $form->value('email');
  666. $existing_email = User::find($email,['email']);
  667. if ($user->username != $existing_email->username && $existing_email->exists()) {
  668. $this->grav->fireEvent('onFormValidationError', new Event([
  669. 'form' => $form,
  670. 'message' => $language->translate([
  671. 'PLUGIN_LOGIN.EMAIL_NOT_AVAILABLE',
  672. $email
  673. ])
  674. ]));
  675. $event->stopPropagation();
  676. return;
  677. }
  678. $user->merge($form->getData()->toArray());
  679. try {
  680. $user->save();
  681. } catch (\Exception $e) {
  682. $form->setMessage($e->getMessage(), 'error');
  683. return false;
  684. }
  685. return true;
  686. }
  687. /**
  688. * [onFormProcessed] Process a registration form. Handles the following actions:
  689. *
  690. * - register_user: registers a user
  691. * - update_user: updates user profile
  692. *
  693. * @param Event $event
  694. * @throws \RuntimeException
  695. */
  696. public function onFormProcessed(Event $event)
  697. {
  698. $form = $event['form'];
  699. $action = $event['action'];
  700. switch ($action) {
  701. case 'register_user':
  702. $this->processUserRegistration($form, $event);
  703. break;
  704. case 'update_user':
  705. $this->processUserProfile($form, $event);
  706. break;
  707. }
  708. }
  709. /**
  710. * @param UserLoginEvent $event
  711. * @throws \RuntimeException
  712. */
  713. public function userLoginAuthenticateByRegistration(UserLoginEvent $event)
  714. {
  715. // Check that we're logging in after registration.
  716. if (!$event->getOption('after_registration') || $this->isAdmin()) {
  717. return;
  718. }
  719. $event->setStatus($event::AUTHENTICATION_SUCCESS);
  720. $event->stopPropagation();
  721. }
  722. /**
  723. * @param UserLoginEvent $event
  724. * @throws \RuntimeException
  725. */
  726. public function userLoginAuthenticateByRememberMe(UserLoginEvent $event)
  727. {
  728. // Check that we're logging in with remember me.
  729. if (!$event->getOption('remember_me_login') || !$event->getOption('remember_me') || $this->isAdmin()) {
  730. return;
  731. }
  732. // Only use remember me if user isn't set and feature is enabled.
  733. if ($this->grav['config']->get('plugins.login.rememberme.enabled') && !$event->getUser()->exists()) {
  734. /** @var RememberMe $rememberMe */
  735. $rememberMe = $this->grav['login']->rememberMe();
  736. $username = $rememberMe->login();
  737. if ($rememberMe->loginTokenWasInvalid()) {
  738. // Token was invalid. We will display error page as this was likely an attack.
  739. throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.REMEMBER_ME_STOLEN_COOKIE'), 403);
  740. }
  741. if ($username === false) {
  742. // User has not been remembered, there is no point of continuing.
  743. $event->setStatus($event::AUTHENTICATION_FAILURE);
  744. $event->stopPropagation();
  745. return;
  746. }
  747. // Allow remember me to work with different login methods.
  748. $event->setCredential('username', $username);
  749. $event->setUser(User::load($username, false));
  750. }
  751. }
  752. public function userLoginAuthenticateByEmail(UserLoginEvent $event)
  753. {
  754. if (($username = $event->getCredential('username')) && !$event->getUser()->exists()) {
  755. $event->setUser(User::find($username));
  756. }
  757. }
  758. public function userLoginAuthenticate(UserLoginEvent $event)
  759. {
  760. $user = $event->getUser();
  761. $credentials = $event->getCredentials();
  762. if (!$user->exists()) {
  763. // Never let non-existing users to pass the authentication.
  764. // Higher level plugins may override this behavior by stopping propagation.
  765. $event->setStatus($event::AUTHENTICATION_FAILURE);
  766. $event->stopPropagation();
  767. return;
  768. }
  769. // Never let empty password to pass the authentication.
  770. // Higher level plugins may override this behavior by stopping propagation.
  771. if (empty($credentials['password'])) {
  772. $event->setStatus($event::AUTHENTICATION_FAILURE);
  773. $event->stopPropagation();
  774. return;
  775. }
  776. // Try default user authentication. Stop propagation if authentication succeeds.
  777. if ($user->authenticate($credentials['password'])) {
  778. $event->setStatus($event::AUTHENTICATION_SUCCESS);
  779. $event->stopPropagation();
  780. return;
  781. }
  782. // If authentication status is undefined, lower level event handlers may still be able to authenticate user.
  783. }
  784. public function userLoginAuthorize(UserLoginEvent $event)
  785. {
  786. // Always block access if authorize defaulting to site.login fails.
  787. $user = $event->getUser();
  788. foreach ($event->getAuthorize() as $authorize) {
  789. if (!$user->authorize($authorize)) {
  790. $event->setStatus($event::AUTHORIZATION_DENIED);
  791. $event->stopPropagation();
  792. return;
  793. }
  794. }
  795. if ($event->getOption('twofa') && $user->twofa_enabled && $user->twofa_secret) {
  796. $event->setStatus($event::AUTHORIZATION_DELAYED);
  797. }
  798. }
  799. public function userLoginFailure(UserLoginEvent $event)
  800. {
  801. $this->grav['session']->user = User::load('', false);
  802. }
  803. public function userLogin(UserLoginEvent $event)
  804. {
  805. $session = $this->grav['session'];
  806. $session->user = $event->getUser();
  807. if ($event->getOption('remember_me')) {
  808. /** @var Login $login */
  809. $login = $this->grav['login'];
  810. $session->remember_me = (bool)$event->getOption('remember_me_login');
  811. // If the user wants to be remembered, create Rememberme cookie.
  812. $username = $event->getUser()->get('username');
  813. if ($event->getCredential('rememberme')) {
  814. $login->rememberMe()->createCookie($username);
  815. } else {
  816. $login->rememberMe()->getStorage()->cleanAllTriplets($username);
  817. $login->rememberMe()->clearCookie();
  818. }
  819. }
  820. }
  821. public function userLogout(UserLoginEvent $event)
  822. {
  823. if ($event->getOption('remember_me')) {
  824. /** @var Login $login */
  825. $login = $this->grav['login'];
  826. if (!$login->rememberMe()->login()) {
  827. $login->rememberMe()->getStorage()->cleanAllTriplets($event->getUser()->get('username'));
  828. }
  829. $login->rememberMe()->clearCookie();
  830. }
  831. $this->grav['session']->invalidate()->start();
  832. }
  833. }