login.php 37 KB

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