login.php 35 KB

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