login.php 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257
  1. <?php
  2. /**
  3. * @package Grav\Plugin\Login
  4. *
  5. * @copyright Copyright (C) 2014 - 2020 RocketTheme, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Plugin;
  9. use Composer\Autoload\ClassLoader;
  10. use Grav\Common\Data\Data;
  11. use Grav\Common\Debugger;
  12. use Grav\Common\Flex\Types\Users\UserObject;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Language\Language;
  15. use Grav\Common\Page\Interfaces\PageInterface;
  16. use Grav\Common\Page\Page;
  17. use Grav\Common\Page\Pages;
  18. use Grav\Common\Plugin;
  19. use Grav\Common\Twig\Twig;
  20. use Grav\Common\User\Interfaces\UserCollectionInterface;
  21. use Grav\Common\User\Interfaces\UserInterface;
  22. use Grav\Common\Utils;
  23. use Grav\Common\Uri;
  24. use Grav\Events\SessionStartEvent;
  25. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  26. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  27. use Grav\Framework\Session\SessionInterface;
  28. use Grav\Plugin\Form\Form;
  29. use Grav\Plugin\Login\Events\UserLoginEvent;
  30. use Grav\Plugin\Login\Login;
  31. use Grav\Plugin\Login\Controller;
  32. use Grav\Plugin\Login\RememberMe\RememberMe;
  33. use RocketTheme\Toolbox\Event\Event;
  34. use RocketTheme\Toolbox\Session\Message;
  35. /**
  36. * Class LoginPlugin
  37. * @package Grav\Plugin\Login
  38. */
  39. class LoginPlugin extends Plugin
  40. {
  41. const TMP_COOKIE_NAME = 'tmp-message';
  42. /** @var string */
  43. protected $route;
  44. /** @var bool */
  45. protected $authenticated = true;
  46. /** @var Login */
  47. protected $login;
  48. /** @var bool */
  49. protected $redirect_to_login;
  50. /**
  51. * @return array
  52. */
  53. public static function getSubscribedEvents()
  54. {
  55. return [
  56. SessionStartEvent::class => ['onSessionStart', 0],
  57. 'onPluginsInitialized' => [['autoload', 100000], ['initializeSession', 10000], ['initializeLogin', 1000]],
  58. 'onTask.login.login' => ['loginController', 0],
  59. 'onTask.login.twofa' => ['loginController', 0],
  60. 'onTask.login.twofa_cancel' => ['loginController', 0],
  61. 'onTask.login.forgot' => ['loginController', 0],
  62. 'onTask.login.logout' => ['loginController', 0],
  63. 'onTask.login.reset' => ['loginController', 0],
  64. 'onTask.login.regenerate2FASecret' => ['loginController', 0],
  65. 'onPagesInitialized' => ['storeReferrerPage', 0],
  66. 'onPageInitialized' => ['authorizePage', 0],
  67. 'onPageFallBackUrl' => ['authorizeFallBackUrl', 0],
  68. 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
  69. 'onTwigSiteVariables' => ['onTwigSiteVariables', -100000],
  70. 'onFormProcessed' => ['onFormProcessed', 0],
  71. 'onUserLoginAuthenticate' => [['userLoginAuthenticateRateLimit', 10003], ['userLoginAuthenticateByRegistration', 10002], ['userLoginAuthenticateByRememberMe', 10001], ['userLoginAuthenticateByEmail', 10000], ['userLoginAuthenticate', 0]],
  72. 'onUserLoginAuthorize' => ['userLoginAuthorize', 0],
  73. 'onUserLoginFailure' => ['userLoginGuest', 0],
  74. 'onUserLoginGuest' => ['userLoginGuest', 0],
  75. 'onUserLogin' => [['userLoginResetRateLimit', 1000], ['userLogin', 0]],
  76. 'onUserLogout' => ['userLogout', 0],
  77. ];
  78. }
  79. /**
  80. * [onPluginsInitialized:100000] Composer autoload.
  81. *
  82. * @return ClassLoader
  83. */
  84. public function autoload() : ClassLoader
  85. {
  86. return require __DIR__ . '/vendor/autoload.php';
  87. }
  88. public function onSessionStart(SessionStartEvent $event)
  89. {
  90. $session = $event->session;
  91. $user = $session->user ?? null;
  92. if ($user && $user->exists() && ($this->config()['session_user_sync'] ?? false)) {
  93. // User is stored into the filesystem.
  94. /** @var UserCollectionInterface $accounts */
  95. $accounts = $this->grav['accounts'];
  96. /** @var UserObject $stored */
  97. if ($accounts instanceof FlexCollectionInterface) {
  98. $stored = $accounts[$user->username];
  99. } else {
  100. // TODO: remove when removing legacy support.
  101. $stored = $accounts->load($user->username);
  102. }
  103. if ($stored && $stored->exists()) {
  104. // User still exists, update user object in the session.
  105. $user->update($stored->jsonSerialize());
  106. } else {
  107. // User doesn't exist anymore, prepare for session invalidation.
  108. $user->state = 'disabled';
  109. }
  110. if ($user->state !== 'enabled') {
  111. // If user isn't enabled, clear all session data and display error.
  112. $session->invalidate()->start();
  113. /** @var Message $messages */
  114. $messages = $this->grav['messages'];
  115. $messages->add($this->grav['language']->translate('PLUGIN_LOGIN.USER_ACCOUNT_DISABLED'), 'error');
  116. }
  117. }
  118. }
  119. /**
  120. * [onPluginsInitialized:10000] Initialize login plugin if path matches.
  121. * @throws \RuntimeException
  122. */
  123. public function initializeSession()
  124. {
  125. // Check to ensure sessions are enabled.
  126. if (!$this->config->get('system.session.enabled')) {
  127. throw new \RuntimeException('The Login plugin requires "system.session" to be enabled');
  128. }
  129. // Define login service.
  130. $this->grav['login'] = static function (Grav $c) {
  131. return new Login($c);
  132. };
  133. // Define current user service.
  134. $this->grav['user'] = static function (Grav $c) {
  135. $session = $c['session'];
  136. if (empty($session->user)) {
  137. // Try remember me login.
  138. $session->user = $c['login']->login(
  139. ['username' => ''],
  140. ['remember_me' => true, 'remember_me_login' => true, 'failureEvent' => 'onUserLoginGuest']
  141. );
  142. }
  143. return $session->user;
  144. };
  145. }
  146. /**
  147. * [onPluginsInitialized:1000] Initialize login plugin if path matches.
  148. * @throws \RuntimeException
  149. */
  150. public function initializeLogin()
  151. {
  152. $this->login = $this->grav['login'];
  153. /** @var Uri $uri */
  154. $uri = $this->grav['uri'];
  155. // Admin has its own login; make sure we're not in admin.
  156. if (!isset($this->grav['admin'])) {
  157. $this->route = $this->config->get('plugins.login.route');
  158. $this->enable([
  159. 'onPagesInitialized' => ['pageVisibility', 0],
  160. ]);
  161. }
  162. $path = $uri->path();
  163. $this->redirect_to_login = $this->config->get('plugins.login.redirect_to_login');
  164. // Register route to login page if it has been set.
  165. if ($this->route && $this->route === $path) {
  166. $this->enable([
  167. 'onPagesInitialized' => ['addLoginPage', 0],
  168. ]);
  169. return;
  170. }
  171. if ($path === $this->config->get('plugins.login.route_forgot')) {
  172. $this->enable([
  173. 'onPagesInitialized' => ['addForgotPage', 0],
  174. ]);
  175. return;
  176. }
  177. if ($path === $this->config->get('plugins.login.route_reset')) {
  178. $this->enable([
  179. 'onPagesInitialized' => ['addResetPage', 0],
  180. ]);
  181. return;
  182. }
  183. if ($path === $this->config->get('plugins.login.route_register')) {
  184. if ($this->config->get('plugins.login.user_registration.enabled')) {
  185. $this->enable([
  186. 'onPagesInitialized' => ['addRegisterPage', 0],
  187. ]);
  188. } else {
  189. throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.REGISTRATION_DISABLED'), 404);
  190. }
  191. return;
  192. }
  193. if ($path === $this->config->get('plugins.login.route_activate')) {
  194. $this->enable([
  195. 'onPagesInitialized' => ['handleUserActivation', 0],
  196. ]);
  197. return;
  198. }
  199. if ($path === $this->config->get('plugins.login.route_profile')) {
  200. $this->enable([
  201. 'onPagesInitialized' => ['addProfilePage', 0],
  202. ]);
  203. return;
  204. }
  205. }
  206. /**
  207. * Optional ability to dynamically set visibility based on page access and page header
  208. * that states `login.visibility_requires_access: true`
  209. *
  210. * Note that this setting may be slow on large sites as it loads all pages into memory for each page load!
  211. *
  212. * @param Event $e
  213. */
  214. public function pageVisibility(Event $e)
  215. {
  216. if ($this->config->get('plugins.login.dynamic_page_visibility')) {
  217. /** @var Pages $pages */
  218. $pages = $e['pages'];
  219. $user = $this->grav['user'];
  220. // TODO: This is super slow especially with Flex Pages. Better solution is required (on indexing / on load?).
  221. foreach ($pages->instances() as $page) {
  222. if ($page) {
  223. $header = $page->header();
  224. if ($header && isset($header->access) && isset($header->login['visibility_requires_access']) && $header->login['visibility_requires_access'] === true) {
  225. $config = $this->mergeConfig($page);
  226. $access = $this->login->isUserAuthorizedForPage($user, $page, $config);
  227. if ($access === false) {
  228. $page->visible(false);
  229. }
  230. }
  231. }
  232. }
  233. }
  234. }
  235. /**
  236. * [onPagesInitialized]
  237. */
  238. public function storeReferrerPage()
  239. {
  240. $invalid_redirect_routes = [
  241. $this->config->get('plugins.login.route') ?: '/login',
  242. $this->config->get('plugins.login.route_register') ?: '/register',
  243. $this->config->get('plugins.login.route_activate') ?: '/activate_user',
  244. $this->config->get('plugins.login.route_forgot') ?: '/forgot_password',
  245. $this->config->get('plugins.login.route_reset') ?: '/reset_password',
  246. ];
  247. /** @var Uri $uri */
  248. $uri = $this->grav['uri'];
  249. $current_route = $uri->route();
  250. $redirect = static::defaultRedirectAfterLogin();
  251. if (!$redirect && !in_array($current_route, $invalid_redirect_routes, true)) {
  252. // No login redirect set in the configuration; can we redirect to the current page?
  253. $allowed = true;
  254. /** @var PageInterface $page */
  255. $page = $this->grav['pages']->dispatch($current_route);
  256. if ($page) {
  257. $header = $page->header();
  258. if (isset($header->login_redirect_here) && $header->login_redirect_here === false) {
  259. $allowed = false;
  260. }
  261. if ($allowed && $page->routable()) {
  262. $redirect = $page->route();
  263. foreach ($uri->params(null, true) as $key => $value) {
  264. if (!in_array($key, ['task', 'nonce', 'login-nonce', 'logout-nonce'], true)) {
  265. $redirect .= $uri->params($key);
  266. }
  267. }
  268. }
  269. }
  270. } else {
  271. $redirect = $this->grav['session']->redirect_after_login;
  272. }
  273. $this->grav['session']->redirect_after_login = $redirect;
  274. }
  275. /**
  276. * Add Login page
  277. * @throws \Exception
  278. */
  279. public function addLoginPage()
  280. {
  281. /** @var Pages $pages */
  282. $pages = $this->grav['pages'];
  283. $page = $pages->dispatch($this->route);
  284. if (!$page) {
  285. // Only add login page if it hasn't already been defined.
  286. $page = new Page();
  287. $page->init(new \SplFileInfo(__DIR__ . '/pages/login.md'));
  288. $page->slug(basename($this->route));
  289. $pages->addPage($page, $this->route);
  290. }
  291. // Login page may not have the correct Cache-Control header set, force no-store for the proxies.
  292. $page->expires(0);
  293. }
  294. /**
  295. * Add Login page
  296. * @throws \Exception
  297. */
  298. public function addForgotPage()
  299. {
  300. $route = $this->config->get('plugins.login.route_forgot');
  301. /** @var Pages $pages */
  302. $pages = $this->grav['pages'];
  303. $page = $pages->dispatch($route);
  304. if (!$page) {
  305. // Only add forgot page if it hasn't already been defined.
  306. $page = new Page();
  307. $page->init(new \SplFileInfo(__DIR__ . '/pages/forgot.md'));
  308. $page->slug(basename($route));
  309. $pages->addPage($page, $route);
  310. }
  311. // Forgot page may not have the correct Cache-Control header set, force no-store for the proxies.
  312. $page->expires(0);
  313. }
  314. /**
  315. * Add Reset page
  316. * @throws \Exception
  317. */
  318. public function addResetPage()
  319. {
  320. $route = $this->config->get('plugins.login.route_reset');
  321. $uri = $this->grav['uri'];
  322. $token = $uri->param('token');
  323. $user = $uri->param('user');
  324. if (!$user || !$token) {
  325. return;
  326. }
  327. /** @var Pages $pages */
  328. $pages = $this->grav['pages'];
  329. $page = $pages->dispatch($route);
  330. if (!$page) {
  331. // Only add login page if it hasn't already been defined.
  332. $page = new Page();
  333. $page->init(new \SplFileInfo(__DIR__ . '/pages/reset.md'));
  334. $page->slug(basename($route));
  335. $pages->addPage($page, $route);
  336. }
  337. // Reset page may not have the correct Cache-Control header set, force no-store for the proxies.
  338. $page->expires(0);
  339. }
  340. /**
  341. * Add Register page
  342. * @throws \Exception
  343. */
  344. public function addRegisterPage()
  345. {
  346. $route = $this->config->get('plugins.login.route_register');
  347. /** @var Pages $pages */
  348. $pages = $this->grav['pages'];
  349. $page = $pages->dispatch($route);
  350. if (!$page) {
  351. $page = new Page();
  352. $page->init(new \SplFileInfo(__DIR__ . '/pages/register.md'));
  353. $page->slug(basename($route));
  354. $pages->addPage($page, $route);
  355. }
  356. // Register page may not have the correct Cache-Control header set, force no-store for the proxies.
  357. $page->expires(0);
  358. }
  359. /**
  360. * Handle user activation
  361. * @throws \RuntimeException
  362. */
  363. public function handleUserActivation()
  364. {
  365. /** @var Uri $uri */
  366. $uri = $this->grav['uri'];
  367. /** @var Message $messages */
  368. $messages = $this->grav['messages'];
  369. /** @var UserCollectionInterface $users */
  370. $users = $this->grav['accounts'];
  371. $username = $uri->param('username');
  372. $token = $uri->param('token');
  373. $user = $users->load($username);
  374. $redirect_route = $this->config->get('plugins.login.user_registration.redirect_after_activation');
  375. $redirect_code = null;
  376. if (empty($user->activation_token)) {
  377. $message = $this->grav['language']->translate('PLUGIN_LOGIN.INVALID_REQUEST');
  378. $messages->add($message, 'error');
  379. } else {
  380. [$good_token, $expire] = explode('::', $user->activation_token, 2);
  381. if ($good_token === $token) {
  382. if (time() > $expire) {
  383. $message = $this->grav['language']->translate('PLUGIN_LOGIN.ACTIVATION_LINK_EXPIRED');
  384. $messages->add($message, 'error');
  385. } else {
  386. if ($this->config->get('plugins.login.user_registration.options.manually_enable', false)) {
  387. $message = $this->grav['language']->translate('PLUGIN_LOGIN.USER_ACTIVATED_SUCCESSFULLY_NOT_ENABLED');
  388. } else {
  389. $user['state'] = 'enabled';
  390. $message = $this->grav['language']->translate('PLUGIN_LOGIN.USER_ACTIVATED_SUCCESSFULLY');
  391. }
  392. $messages->add($message, 'info');
  393. unset($user->activation_token);
  394. $user->save();
  395. if ($this->config->get('plugins.login.user_registration.options.send_welcome_email', false)) {
  396. $this->login->sendWelcomeEmail($user);
  397. }
  398. if ($this->config->get('plugins.login.user_registration.options.send_notification_email', false)) {
  399. $this->login->sendNotificationEmail($user);
  400. }
  401. if ($this->config->get('plugins.login.user_registration.options.login_after_registration', false)) {
  402. $loginEvent = $this->login->login(['username' => $username], ['after_registration' => true], ['user' => $user, 'return_event' => true]);
  403. // If there's no activation redirect, get one from login.
  404. if (!$redirect_route) {
  405. $message = $loginEvent->getMessage();
  406. if ($message) {
  407. $messages->add($message, $loginEvent->getMessageType());
  408. }
  409. $redirect_route = $loginEvent->getRedirect();
  410. $redirect_code = $loginEvent->getRedirectCode();
  411. }
  412. }
  413. $this->grav->fireEvent('onUserActivated', new Event(['user' => $user]));
  414. }
  415. } else {
  416. $message = $this->grav['language']->translate('PLUGIN_LOGIN.INVALID_REQUEST');
  417. $messages->add($message, 'error');
  418. }
  419. }
  420. $this->grav->redirectLangSafe($redirect_route ?: '/', $redirect_code);
  421. }
  422. /**
  423. * Add Profile page
  424. */
  425. public function addProfilePage()
  426. {
  427. $route = $this->config->get('plugins.login.route_profile');
  428. /** @var Pages $pages */
  429. $pages = $this->grav['pages'];
  430. $page = $pages->dispatch($route);
  431. if (!$page) {
  432. // Only add forgot page if it hasn't already been defined.
  433. $page = new Page();
  434. $page->init(new \SplFileInfo(__DIR__ . '/pages/profile.md'));
  435. $page->slug(basename($route));
  436. $pages->addPage($page, $route);
  437. }
  438. // Profile page may not have the correct Cache-Control header set, force no-store for the proxies.
  439. $page->expires(0);
  440. $this->storeReferrerPage();
  441. }
  442. /**
  443. * Set Unauthorized page
  444. * @throws \Exception
  445. */
  446. public function setUnauthorizedPage()
  447. {
  448. $route = $this->config->get('plugins.login.route_unauthorized');
  449. /** @var Pages $pages */
  450. $pages = $this->grav['pages'];
  451. $page = $pages->dispatch($route);
  452. if (!$page) {
  453. $page = new Page();
  454. $page->init(new \SplFileInfo(__DIR__ . '/pages/unauthorized.md'));
  455. $page->slug(basename($route));
  456. $pages->addPage($page, $route);
  457. }
  458. // Unauthorized page may not have the correct Cache-Control header set, force no-store for the proxies.
  459. $page->expires(0);
  460. unset($this->grav['page']);
  461. $this->grav['page'] = $page;
  462. }
  463. /**
  464. * Initialize login controller
  465. */
  466. public function loginController()
  467. {
  468. /** @var Uri $uri */
  469. $uri = $this->grav['uri'];
  470. $task = $_POST['task'] ?? $uri->param('task');
  471. $task = substr($task, \strlen('login.'));
  472. $post = !empty($_POST) ? $_POST : [];
  473. switch ($task) {
  474. case 'login':
  475. if (!isset($post['login-form-nonce']) || !Utils::verifyNonce($post['login-form-nonce'], 'login-form')) {
  476. $this->grav['messages']->add($this->grav['language']->translate('PLUGIN_LOGIN.ACCESS_DENIED'),
  477. 'info');
  478. $twig = $this->grav['twig'];
  479. $twig->twig_vars['notAuthorized'] = true;
  480. return;
  481. }
  482. break;
  483. case 'forgot':
  484. if (!isset($post['forgot-form-nonce']) || !Utils::verifyNonce($post['forgot-form-nonce'], 'forgot-form')) {
  485. $this->grav['messages']->add($this->grav['language']->translate('PLUGIN_LOGIN.ACCESS_DENIED'),'info');
  486. return;
  487. }
  488. break;
  489. }
  490. $controller = new Controller($this->grav, $task, $post);
  491. $controller->execute();
  492. $controller->redirect();
  493. }
  494. /**
  495. * Authorize the Page fallback url (page media accessed through the page route)
  496. */
  497. public function authorizeFallBackUrl()
  498. {
  499. if ($this->config->get('plugins.login.protect_protected_page_media', false)) {
  500. $page_url = \dirname($this->grav['uri']->path());
  501. $page = $this->grav['pages']->find($page_url);
  502. unset($this->grav['page']);
  503. $this->grav['page'] = $page;
  504. $this->authorizePage();
  505. }
  506. }
  507. /**
  508. * [onPageInitialized] Authorize Page
  509. */
  510. public function authorizePage()
  511. {
  512. if (!$this->authenticated) {
  513. return;
  514. }
  515. /** @var UserInterface $user */
  516. $user = $this->grav['user'];
  517. /** @var PageInterface $page */
  518. $page = $this->grav['page'];
  519. if (!$page || $this->grav['login']->isUserAuthorizedForPage($user, $page, $this->mergeConfig($page))) {
  520. return;
  521. }
  522. // If this is not an HTML page request, simply throw a 403 error
  523. $uri_extension = $this->grav['uri']->extension('html');
  524. $supported_types = $this->config->get('media.types');
  525. if ($uri_extension !== 'html' && array_key_exists($uri_extension, $supported_types)) {
  526. header('HTTP/1.0 403 Forbidden');
  527. exit;
  528. }
  529. $authorized = $user->authenticated && $user->authorized;
  530. // User is not logged in; redirect to login page.
  531. if ($this->redirect_to_login && $this->route && !$authorized) {
  532. $this->grav->redirectLangSafe($this->route, 302);
  533. }
  534. /** @var Twig $twig */
  535. $twig = $this->grav['twig'];
  536. $login_page = null;
  537. // Reset page with login page.
  538. if (!$authorized) {
  539. if ($this->route) {
  540. $login_page = $this->grav['pages']->dispatch($this->route);
  541. }
  542. if (!$login_page) {
  543. $login_page = new Page();
  544. // Get the admin Login page is needed, else the default
  545. if ($this->isAdmin()) {
  546. $login_file = $this->grav['locator']->findResource('plugins://admin/pages/admin/login.md');
  547. $login_page->init(new \SplFileInfo($login_file));
  548. } else {
  549. $login_page->init(new \SplFileInfo(__DIR__ . '/pages/login.md'));
  550. }
  551. $login_page->slug(basename($this->route));
  552. /** @var Pages $pages */
  553. $pages = $this->grav['pages'];
  554. $pages->addPage($login_page, $this->route);
  555. }
  556. // Login page may not have the correct Cache-Control header set, force no-store for the proxies.
  557. $login_page->expires(0);
  558. $this->authenticated = false;
  559. unset($this->grav['page']);
  560. $this->grav['page'] = $login_page;
  561. $twig->twig_vars['form'] = new Form($login_page);
  562. } else {
  563. /** @var Language $l */
  564. $l = $this->grav['language'];
  565. $this->grav['messages']->add($l->translate('PLUGIN_LOGIN.ACCESS_DENIED'), 'error');
  566. $twig->twig_vars['notAuthorized'] = true;
  567. $this->setUnauthorizedPage();
  568. }
  569. }
  570. /**
  571. * [onTwigTemplatePaths] Add twig paths to plugin templates.
  572. */
  573. public function onTwigTemplatePaths()
  574. {
  575. $twig = $this->grav['twig'];
  576. $twig->twig_paths[] = __DIR__ . '/templates';
  577. }
  578. /**
  579. * [onTwigSiteVariables] Set all twig variables for generating output.
  580. */
  581. public function onTwigSiteVariables()
  582. {
  583. /** @var Twig $twig */
  584. $twig = $this->grav['twig'];
  585. $this->grav->fireEvent('onLoginPage');
  586. $extension = $this->grav['uri']->extension();
  587. $extension = $extension ?: 'html';
  588. if (!$this->authenticated) {
  589. $twig->template = "login.{$extension}.twig";
  590. }
  591. // add CSS for frontend if required
  592. if (!$this->isAdmin() && $this->config->get('plugins.login.built_in_css')) {
  593. $this->grav['assets']->add('plugin://login/css/login.css');
  594. }
  595. $task = $this->grav['uri']->param('task') ?: ($_POST['task'] ?? '');
  596. $task = substr($task, \strlen('login.'));
  597. if ($task === 'reset') {
  598. $username = $this->grav['uri']->param('user');
  599. $token = $this->grav['uri']->param('token');
  600. if (!empty($username) && !empty($token)) {
  601. $twig->twig_vars['username'] = $username;
  602. $twig->twig_vars['token'] = $token;
  603. }
  604. } elseif ($task === 'login') {
  605. $twig->twig_vars['username'] = $_POST['username'] ?? '';
  606. }
  607. $flashData = $this->grav['session']->getFlashCookieObject(self::TMP_COOKIE_NAME);
  608. if (isset($flashData->message)) {
  609. $this->grav['messages']->add($flashData->message, $flashData->status);
  610. }
  611. }
  612. /**
  613. * Process the user registration, triggered by a registration form
  614. *
  615. * @param Form $form
  616. * @throws \RuntimeException
  617. */
  618. private function processUserRegistration($form, Event $event)
  619. {
  620. $language = $this->grav['language'];
  621. $messages = $this->grav['messages'];
  622. if (!$this->config->get('plugins.login.enabled')) {
  623. throw new \RuntimeException($language->translate('PLUGIN_LOGIN.PLUGIN_LOGIN_DISABLED'));
  624. }
  625. if (!$this->config->get('plugins.login.user_registration.enabled')) {
  626. throw new \RuntimeException($language->translate('PLUGIN_LOGIN.USER_REGISTRATION_DISABLED'));
  627. }
  628. $form->validate();
  629. /** @var Data $form_data */
  630. $form_data = $form->getData();
  631. /** @var UserCollectionInterface $users */
  632. $users = $this->grav['accounts'];
  633. // Check for existing username
  634. $username = $form_data->get('username');
  635. $existing_username = $users->find($username, ['username']);
  636. if ($existing_username->exists()) {
  637. $this->grav->fireEvent('onFormValidationError', new Event([
  638. 'form' => $form,
  639. 'message' => $language->translate([
  640. 'PLUGIN_LOGIN.USERNAME_NOT_AVAILABLE',
  641. $username
  642. ])
  643. ]));
  644. $event->stopPropagation();
  645. return;
  646. }
  647. // Check for existing email
  648. $email = $form_data->get('email');
  649. $existing_email = $users->find($email, ['email']);
  650. if ($existing_email->exists()) {
  651. $this->grav->fireEvent('onFormValidationError', new Event([
  652. 'form' => $form,
  653. 'message' => $language->translate([
  654. 'PLUGIN_LOGIN.EMAIL_NOT_AVAILABLE',
  655. $email
  656. ])
  657. ]));
  658. $event->stopPropagation();
  659. return;
  660. }
  661. $data = [];
  662. $data['username'] = $username;
  663. // if multiple password fields, check they match and set password field from it
  664. if ($this->config->get('plugins.login.user_registration.options.validate_password1_and_password2',
  665. false)
  666. ) {
  667. if ($form_data->get('password1') !== $form_data->get('password2')) {
  668. $this->grav->fireEvent('onFormValidationError', new Event([
  669. 'form' => $form,
  670. 'message' => $language->translate('PLUGIN_LOGIN.PASSWORDS_DO_NOT_MATCH')
  671. ]));
  672. $event->stopPropagation();
  673. return;
  674. }
  675. $data['password'] = $form_data->get('password1');
  676. }
  677. $fields = (array)$this->config->get('plugins.login.user_registration.fields', []);
  678. foreach ($fields as $field) {
  679. // Process value of field if set in the page process.register_user
  680. $default_values = (array)$this->config->get('plugins.login.user_registration.default_values');
  681. if ($default_values) {
  682. foreach ($default_values as $key => $param) {
  683. if ($key === $field) {
  684. if (\is_array($param)) {
  685. $values = explode(',', $param);
  686. } else {
  687. $values = $param;
  688. }
  689. $data[$field] = $values;
  690. }
  691. }
  692. }
  693. if (!isset($data[$field]) && $form_data->get($field)) {
  694. $data[$field] = $form_data->get($field);
  695. }
  696. }
  697. if ($this->config->get('plugins.login.user_registration.options.set_user_disabled', false)) {
  698. $data['state'] = 'disabled';
  699. } else {
  700. $data['state'] = 'enabled';
  701. }
  702. $data_object = (object) $data;
  703. $this->grav->fireEvent('onUserLoginRegisterData', new Event(['data' => &$data_object]));
  704. $flash = $form->getFlash();
  705. $user = $this->login->register((array)$data_object, $flash->getFilesByFields(true));
  706. if ($user instanceof FlexObjectInterface) {
  707. $flash->clearFiles();
  708. $flash->save();
  709. }
  710. $this->grav->fireEvent('onUserLoginRegisteredUser', new Event(['user' => &$user]));
  711. $fullname = $user->fullname ?? $user->username;
  712. if ($this->config->get('plugins.login.user_registration.options.send_activation_email', false)) {
  713. $this->login->sendActivationEmail($user);
  714. $message = $language->translate(['PLUGIN_LOGIN.ACTIVATION_NOTICE_MSG', $fullname]);
  715. $messages->add($message, 'info');
  716. } else {
  717. if ($this->config->get('plugins.login.user_registration.options.send_welcome_email', false)) {
  718. $this->login->sendWelcomeEmail($user);
  719. }
  720. if ($this->config->get('plugins.login.user_registration.options.send_notification_email', false)) {
  721. $this->login->sendNotificationEmail($user);
  722. }
  723. $message = $language->translate(['PLUGIN_LOGIN.WELCOME_NOTICE_MSG', $fullname]);
  724. $messages->add($message, 'info');
  725. }
  726. $this->grav->fireEvent('onUserLoginRegistered', new Event(['user' => $user]));
  727. $redirect = $this->config->get('plugins.login.user_registration.redirect_after_registration');
  728. $redirect_code = null;
  729. if (isset($user['state']) && $user['state'] === 'enabled' && $this->config->get('plugins.login.user_registration.options.login_after_registration', false)) {
  730. $loginEvent = $this->login->login(['username' => $user->username], ['after_registration' => true], ['user' => $user, 'return_event' => true]);
  731. // If there's no registration redirect, get one from login.
  732. if (!$redirect) {
  733. $message = $loginEvent->getMessage();
  734. if ($message) {
  735. $messages->add($message, $loginEvent->getMessageType());
  736. }
  737. $redirect = $loginEvent->getRedirect();
  738. $redirect_code = $loginEvent->getRedirectCode();
  739. }
  740. }
  741. if ($redirect) {
  742. $event['redirect'] = $redirect;
  743. $event['redirect_code'] = $redirect_code;
  744. }
  745. }
  746. /**
  747. * Save user profile information
  748. *
  749. * @param Form $form
  750. * @param Event $event
  751. * @return bool
  752. */
  753. private function processUserProfile($form, Event $event)
  754. {
  755. /** @var UserInterface $user */
  756. $user = $this->grav['user'];
  757. $language = $this->grav['language'];
  758. $form->validate();
  759. /** @var Data $form_data */
  760. $form_data = $form->getData();
  761. // Don't save if user doesn't exist
  762. if (!$user->exists()) {
  763. $this->grav->fireEvent('onFormValidationError', new Event([
  764. 'form' => $form,
  765. 'message' => $language->translate('PLUGIN_LOGIN.USER_IS_REMOTE_ONLY')
  766. ]));
  767. $event->stopPropagation();
  768. return false;
  769. }
  770. // Stop overloading of username
  771. $username = $form->data('username');
  772. if (isset($username)) {
  773. $this->grav->fireEvent('onFormValidationError', new Event([
  774. 'form' => $form,
  775. 'message' => $language->translate([
  776. 'PLUGIN_LOGIN.USERNAME_NOT_AVAILABLE',
  777. $username
  778. ])
  779. ]));
  780. $event->stopPropagation();
  781. return false;
  782. }
  783. /** @var UserCollectionInterface $users */
  784. $users = $this->grav['accounts'];
  785. // Check for existing email
  786. $email = $form->getData('email');
  787. $existing_email = $users->find($email, ['email']);
  788. if ($user->username !== $existing_email->username && $existing_email->exists()) {
  789. $this->grav->fireEvent('onFormValidationError', new Event([
  790. 'form' => $form,
  791. 'message' => $language->translate([
  792. 'PLUGIN_LOGIN.EMAIL_NOT_AVAILABLE',
  793. $email
  794. ])
  795. ]));
  796. $event->stopPropagation();
  797. return false;
  798. }
  799. $fields = (array)$this->config->get('plugins.login.user_registration.fields', []);
  800. $data = [];
  801. foreach ($fields as $field) {
  802. $data_field = $form_data->get($field);
  803. if (!isset($data[$field]) && isset($data_field)) {
  804. $data[$field] = $form_data->get($field);
  805. }
  806. }
  807. try {
  808. $flash = $form->getFlash();
  809. $user->update($data, $flash->getFilesByFields(true));
  810. $user->save();
  811. if ($user instanceof FlexObjectInterface) {
  812. $flash->clearFiles();
  813. $flash->save();
  814. }
  815. } catch (\Exception $e) {
  816. $form->setMessage($e->getMessage(), 'error');
  817. return false;
  818. }
  819. return true;
  820. }
  821. /**
  822. * [onFormProcessed] Process a registration form. Handles the following actions:
  823. *
  824. * - register_user: registers a user
  825. * - update_user: updates user profile
  826. *
  827. * @param Event $event
  828. * @throws \RuntimeException
  829. */
  830. public function onFormProcessed(Event $event)
  831. {
  832. $form = $event['form'];
  833. $action = $event['action'];
  834. switch ($action) {
  835. case 'register_user':
  836. $this->processUserRegistration($form, $event);
  837. break;
  838. case 'update_user':
  839. $this->processUserProfile($form, $event);
  840. break;
  841. }
  842. }
  843. /**
  844. * @param UserLoginEvent $event
  845. * @throws \RuntimeException
  846. */
  847. public function userLoginAuthenticateRateLimit(UserLoginEvent $event)
  848. {
  849. // Check that we're logging in with rate limit turned on.
  850. if (!$event->getOption('rate_limit')) {
  851. return;
  852. }
  853. $credentials = $event->getCredentials();
  854. $username = $credentials['username'];
  855. // Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
  856. if ($interval = $this->login->checkLoginRateLimit($username)) {
  857. /** @var Language $t */
  858. $t = $this->grav['language'];
  859. $event->setMessage($t->translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $interval]), 'error');
  860. $event->setRedirect($this->grav['config']->get('plugins.login.route', '/'));
  861. $event->setStatus(UserLoginEvent::AUTHENTICATION_CANCELLED);
  862. $event->stopPropagation();
  863. }
  864. }
  865. /**
  866. * @param UserLoginEvent $event
  867. * @throws \RuntimeException
  868. */
  869. public function userLoginAuthenticateByRegistration(UserLoginEvent $event)
  870. {
  871. // Check that we're logging in after registration.
  872. if (!$event->getOption('after_registration') || $this->isAdmin()) {
  873. return;
  874. }
  875. $event->setStatus($event::AUTHENTICATION_SUCCESS);
  876. $event->stopPropagation();
  877. }
  878. /**
  879. * @param UserLoginEvent $event
  880. * @throws \RuntimeException
  881. */
  882. public function userLoginAuthenticateByRememberMe(UserLoginEvent $event)
  883. {
  884. // Check that we're logging in with remember me.
  885. if (!$event->getOption('remember_me_login') || !$event->getOption('remember_me') || $this->isAdmin()) {
  886. return;
  887. }
  888. // Only use remember me if user isn't set and feature is enabled.
  889. if ($this->grav['config']->get('plugins.login.rememberme.enabled') && !$event->getUser()->exists()) {
  890. /** @var Debugger $debugger */
  891. $debugger = $this->grav['debugger'];
  892. /** @var RememberMe $rememberMe */
  893. $rememberMe = $this->grav['login']->rememberMe();
  894. $username = $rememberMe->login();
  895. if ($rememberMe->loginTokenWasInvalid()) {
  896. // Token was invalid. We will display error page as this was likely an attack.
  897. $debugger->addMessage('Remember Me: Stolen token!');
  898. throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.REMEMBER_ME_STOLEN_COOKIE'), 403);
  899. }
  900. if ($username === false) {
  901. // User has not been remembered, there is no point of continuing.
  902. $debugger->addMessage('Remember Me: No token matched.');
  903. $event->setStatus($event::AUTHENTICATION_FAILURE);
  904. $event->stopPropagation();
  905. return;
  906. }
  907. /** @var UserCollectionInterface $users */
  908. $users = $this->grav['accounts'];
  909. // Allow remember me to work with different login methods.
  910. $user = $users->load($username);
  911. $event->setCredential('username', $username);
  912. $event->setUser($user);
  913. if (!$user->exists()) {
  914. $debugger->addMessage('Remember Me: User does not exist');
  915. $event->setStatus($event::AUTHENTICATION_FAILURE);
  916. $event->stopPropagation();
  917. return;
  918. }
  919. $debugger->addMessage('Remember Me: Authenticated!');
  920. $event->setStatus($event::AUTHENTICATION_SUCCESS);
  921. $event->stopPropagation();
  922. return;
  923. }
  924. }
  925. public function userLoginAuthenticateByEmail(UserLoginEvent $event)
  926. {
  927. if (($username = $event->getCredential('username')) && !$event->getUser()->exists()) {
  928. /** @var UserCollectionInterface $users */
  929. $users = $this->grav['accounts'];
  930. $event->setUser($users->find($username));
  931. }
  932. }
  933. public function userLoginAuthenticate(UserLoginEvent $event)
  934. {
  935. $user = $event->getUser();
  936. $credentials = $event->getCredentials();
  937. if (!$user->exists()) {
  938. // Never let non-existing users to pass the authentication.
  939. // Higher level plugins may override this behavior by stopping propagation.
  940. $event->setStatus($event::AUTHENTICATION_FAILURE);
  941. $event->stopPropagation();
  942. return;
  943. }
  944. // Never let empty password to pass the authentication.
  945. // Higher level plugins may override this behavior by stopping propagation.
  946. if (empty($credentials['password'])) {
  947. $event->setStatus($event::AUTHENTICATION_FAILURE);
  948. $event->stopPropagation();
  949. return;
  950. }
  951. // Try default user authentication. Stop propagation if authentication succeeds.
  952. if ($user->authenticate($credentials['password'])) {
  953. $event->setStatus($event::AUTHENTICATION_SUCCESS);
  954. $event->stopPropagation();
  955. return;
  956. }
  957. // If authentication status is undefined, lower level event handlers may still be able to authenticate user.
  958. }
  959. public function userLoginAuthorize(UserLoginEvent $event)
  960. {
  961. // Always block access if authorize defaulting to site.login fails.
  962. $user = $event->getUser();
  963. foreach ($event->getAuthorize() as $authorize) {
  964. if (!$user->authorize($authorize)) {
  965. if ($user->state !== 'enabled') {
  966. $event->setMessage($this->grav['language']->translate('PLUGIN_LOGIN.USER_ACCOUNT_DISABLED'), 'error');
  967. }
  968. $event->setStatus($event::AUTHORIZATION_DENIED);
  969. $event->stopPropagation();
  970. return;
  971. }
  972. }
  973. if ($event->getOption('twofa') && $user->twofa_enabled && $user->twofa_secret) {
  974. $event->setStatus($event::AUTHORIZATION_DELAYED);
  975. }
  976. }
  977. public function userLoginGuest(UserLoginEvent $event)
  978. {
  979. /** @var UserCollectionInterface $users */
  980. $users = $this->grav['accounts'];
  981. $user = $users->load('');
  982. $event->setUser($user);
  983. $this->grav['session']->user = $user;
  984. }
  985. public function userLoginResetRateLimit(UserLoginEvent $event)
  986. {
  987. if ($event->getOption('rate_limit')) {
  988. // Reset user rate limit.
  989. $user = $event->getUser();
  990. $this->login->resetLoginRateLimit($user->get('username'));
  991. }
  992. }
  993. public function userLogin(UserLoginEvent $event)
  994. {
  995. /** @var SessionInterface $session */
  996. $session = $this->grav['session'];
  997. // Prevent session fixation if supported.
  998. // TODO: remove method_exists() test when requiring Grav v1.7
  999. if (method_exists($session, 'regenerateId')) {
  1000. $session->regenerateId();
  1001. }
  1002. $session->user = $user = $event->getUser();
  1003. if ($event->getOption('remember_me')) {
  1004. /** @var Login $login */
  1005. $login = $this->grav['login'];
  1006. $session->remember_me = (bool)$event->getOption('remember_me_login');
  1007. // If the user wants to be remembered, create Rememberme cookie.
  1008. $username = $user->get('username');
  1009. if ($event->getCredential('rememberme')) {
  1010. $login->rememberMe()->createCookie($username);
  1011. }
  1012. }
  1013. }
  1014. public function userLogout(UserLoginEvent $event)
  1015. {
  1016. if ($event->getOption('remember_me')) {
  1017. /** @var Login $login */
  1018. $login = $this->grav['login'];
  1019. if (!$login->rememberMe()->login()) {
  1020. $login->rememberMe()->getStorage()->cleanAllTriplets($event->getUser()->get('username'));
  1021. }
  1022. $login->rememberMe()->clearCookie();
  1023. }
  1024. /** @var SessionInterface $session */
  1025. $session = $this->grav['session'];
  1026. // Clear all session data.
  1027. $session->invalidate()->start();
  1028. }
  1029. public static function defaultRedirectAfterLogin()
  1030. {
  1031. $config = Grav::instance()['config'];
  1032. $redirect_after_login = $config->get('plugins.login.redirect_after_login');
  1033. $route_after_login = $config->get('plugins.login.route_after_login');
  1034. return is_bool($redirect_after_login) && $redirect_after_login == true ? $route_after_login : $redirect_after_login;
  1035. }
  1036. public static function defaultRedirectAfterLogout()
  1037. {
  1038. $config = Grav::instance()['config'];
  1039. $redirect_after_logout = $config->get('plugins.login.redirect_after_logout');
  1040. $route_after_logout = $config->get('plugins.login.route_after_logout');
  1041. return is_bool($redirect_after_logout) && $redirect_after_logout == true ? $route_after_logout : $redirect_after_logout;
  1042. }
  1043. }