login.php 45 KB

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