login.php 36 KB

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