login.php 44 KB

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