login.php 42 KB

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