Login.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  1. <?php
  2. /**
  3. * @package Grav\Plugin\Login
  4. *
  5. * @copyright Copyright (C) 2014 - 2021 RocketTheme, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Plugin\Login;
  9. use Birke\Rememberme\Cookie;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Data\Data;
  12. use Grav\Common\Debugger;
  13. use Grav\Common\Grav;
  14. use Grav\Common\Language\Language;
  15. use Grav\Common\Language\LanguageCodes;
  16. use Grav\Common\Page\Interfaces\PageInterface;
  17. use Grav\Common\Page\Page;
  18. use Grav\Common\Page\Pages;
  19. use Grav\Common\Session;
  20. use Grav\Common\User\Interfaces\UserCollectionInterface;
  21. use Grav\Common\User\Interfaces\UserInterface;
  22. use Grav\Common\Uri;
  23. use Grav\Common\Utils;
  24. use Grav\Plugin\Email\Utils as EmailUtils;
  25. use Grav\Plugin\Login\Events\UserLoginEvent;
  26. use Grav\Plugin\Login\Invitations\Invitation;
  27. use Grav\Plugin\Login\RememberMe\RememberMe;
  28. use Grav\Plugin\Login\RememberMe\TokenStorage;
  29. use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
  30. /**
  31. * Class Login
  32. * @package Grav\Plugin
  33. */
  34. class Login
  35. {
  36. public const DEBUG = 0;
  37. /** @var Grav */
  38. protected $grav;
  39. /** @var Config */
  40. protected $config;
  41. /** @var Language $language */
  42. protected $language;
  43. /** @var Session */
  44. protected $session;
  45. /** @var Uri */
  46. protected $uri;
  47. /** @var RememberMe */
  48. protected $rememberMe;
  49. /** @var TwoFactorAuth */
  50. protected $twoFa;
  51. /** @var RateLimiter[] */
  52. protected $rateLimiters = [];
  53. /** @var array */
  54. protected $provider_login_templates = [];
  55. /**
  56. * Login constructor.
  57. *
  58. * @param Grav $grav
  59. */
  60. public function __construct(Grav $grav)
  61. {
  62. $this->grav = $grav;
  63. $this->config = $this->grav['config'];
  64. $this->language = $this->grav['language'];
  65. $this->session = $this->grav['session'];
  66. $this->uri = $this->grav['uri'];
  67. }
  68. /**
  69. * @param string $message
  70. * @param object|array $data
  71. */
  72. public static function addDebugMessage(string $message, $data = []): void
  73. {
  74. /** @var Debugger $debugger */
  75. $debugger = Grav::instance()['debugger'];
  76. $debugger->addMessage($message, 'debug', $data);
  77. }
  78. /**
  79. * Login user.
  80. *
  81. * @param array $credentials Login credentials, eg: ['username' => '', 'password' => '']
  82. * @param array $options Login options, eg: ['remember_me' => true]
  83. * @param array $extra Example: ['authorize' => 'site.login', 'user' => null], undefined variables get set.
  84. * @return UserInterface|UserLoginEvent Returns event if $extra['return_event'] is true.
  85. */
  86. public function login(array $credentials, array $options = [], array $extra = [])
  87. {
  88. $grav = Grav::instance();
  89. $eventOptions = [
  90. 'credentials' => $credentials,
  91. 'options' => $options
  92. ] + $extra;
  93. // Attempt to authenticate the user.
  94. $event = new UserLoginEvent($eventOptions);
  95. $grav->fireEvent('onUserLoginAuthenticate', $event);
  96. if ($event->isSuccess()) {
  97. static::DEBUG && static::addDebugMessage('Login onUserLoginAuthenticate: success', $event);
  98. // Make sure that event didn't mess up with the user authorization.
  99. $user = $event->getUser();
  100. $user->authenticated = true;
  101. $user->authorized = false;
  102. // Allow plugins to prevent login after successful authentication.
  103. $event = new UserLoginEvent($event->toArray());
  104. $grav->fireEvent('onUserLoginAuthorize', $event);
  105. }
  106. if ($event->isSuccess()) {
  107. static::DEBUG && static::addDebugMessage('Login onUserLoginAuthorize: success', $event);
  108. // User has been logged in, let plugins know.
  109. $event = new UserLoginEvent($event->toArray());
  110. $grav->fireEvent('onUserLogin', $event);
  111. // Make sure that event didn't mess up with the user authorization.
  112. $user = $event->getUser();
  113. $user->authenticated = true;
  114. $user->authorized = !$event->isDelayed();
  115. if ($user->authorized) {
  116. $event = new UserLoginEvent($event->toArray());
  117. $this->grav->fireEvent('onUserLoginAuthorized', $event);
  118. }
  119. } else {
  120. static::DEBUG && static::addDebugMessage('Login failed', $event);
  121. // Allow plugins to log errors or do other tasks on failure.
  122. $eventName = $event->getOption('failureEvent') ?? 'onUserLoginFailure';
  123. $event = new UserLoginEvent($event->toArray());
  124. $grav->fireEvent($eventName, $event);
  125. // Make sure that event didn't mess up with the user authorization.
  126. $user = $event->getUser();
  127. $user->authenticated = false;
  128. $user->authorized = false;
  129. }
  130. $user = $event->getUser();
  131. $user->def('language', 'en');
  132. return !empty($event['return_event']) ? $event : $user;
  133. }
  134. /**
  135. * Logout user.
  136. *
  137. * @param array $options
  138. * @param array|UserInterface $extra Array of: ['user' => $user, ...] or UserInterface object (deprecated).
  139. * @return UserInterface|UserLoginEvent Returns event if $extra['return_event'] is true.
  140. */
  141. public function logout(array $options = [], $extra = [])
  142. {
  143. $grav = Grav::instance();
  144. if ($extra instanceof UserInterface) {
  145. user_error(__METHOD__ . '($options, $user) is deprecated since Login Plugin 3.5.0, use logout($options, [\'user\' => $user]) instead', E_USER_DEPRECATED);
  146. $extra = ['user' => $extra];
  147. } elseif (isset($extra['user'])) {
  148. $extra['user'] = $grav['user'];
  149. }
  150. $eventOptions = [
  151. 'options' => $options
  152. ] + $extra;
  153. $event = new UserLoginEvent($eventOptions);
  154. // Logout the user.
  155. $grav->fireEvent('onUserLogout', $event);
  156. $user = $event->getUser();
  157. $user->authenticated = false;
  158. $user->authorized = false;
  159. return !empty($event['return_event']) ? $event : $user;
  160. }
  161. /**
  162. * Authenticate user.
  163. *
  164. * @param array $credentials Form fields.
  165. * @param array $options
  166. *
  167. * @return bool
  168. * @deprecated Uses the Controller::taskLogin() event
  169. */
  170. public function authenticate($credentials, $options = ['remember_me' => true])
  171. {
  172. $event = $this->login($credentials, $options, ['return_event' => true]);
  173. $user = $event['user'];
  174. $redirect = $event->getRedirect();
  175. $message = $event->getMessage();
  176. $messageType = $event->getMessageType();
  177. if ($user->authenticated && $user->authorized) {
  178. if (!$message) {
  179. $message = 'PLUGIN_LOGIN.LOGIN_SUCCESSFUL';
  180. $messageType = 'info';
  181. }
  182. if (!$redirect) {
  183. $redirect = $this->uri->route();
  184. }
  185. }
  186. if ($message) {
  187. $this->grav['messages']->add($this->language->translate($message, [$user->language]), $messageType);
  188. }
  189. if ($redirect) {
  190. $this->grav->redirectLangSafe($redirect, $event->getRedirectCode());
  191. }
  192. return $user->authenticated && $user->authorized;
  193. }
  194. /**
  195. * Create a new user file
  196. *
  197. * @param array $data
  198. * @param array $files
  199. *
  200. * @return UserInterface
  201. */
  202. public function register(array $data, array $files = [])
  203. {
  204. // Add defaults and mandatory fields.
  205. $data += [
  206. 'username' => null,
  207. 'email' => null
  208. ];
  209. if (!isset($data['groups'])) {
  210. //Add new user ACL settings
  211. $groups = (array) $this->config->get('plugins.login.user_registration.groups', []);
  212. if (\count($groups) > 0) {
  213. $data['groups'] = $groups;
  214. }
  215. }
  216. if (!isset($data['access'])) {
  217. $access = (array) $this->config->get('plugins.login.user_registration.access.site', []);
  218. if (\count($access) > 0) {
  219. $data['access']['site'] = $access;
  220. }
  221. }
  222. // Validate fields from the form.
  223. $password = $this->validateField('password1', $data['password'] ?? $data['password1'] ?? null);
  224. foreach ($data as $key => &$value) {
  225. $value = $this->validateField($key, $value, $key === 'password2' ? $password : '');
  226. }
  227. unset($value);
  228. /** @var UserCollectionInterface $accounts */
  229. $accounts = $this->grav['accounts'];
  230. // Check whether username already exists.
  231. $username = $data['username'];
  232. if (!$username || $accounts->find($username, ['username'])->exists()) {
  233. /** @var Language $language */
  234. $language = $this->grav['language'];
  235. throw new \RuntimeException($language->translate(['PLUGIN_LOGIN.USERNAME_NOT_AVAILABLE', $username]));
  236. }
  237. // Check whether email already exists.
  238. $email = $data['email'];
  239. if (!$email || $accounts->find($email, ['email'])->exists()) {
  240. /** @var Language $language */
  241. $language = $this->grav['language'];
  242. throw new \RuntimeException($language->translate(['PLUGIN_LOGIN.EMAIL_NOT_AVAILABLE', $email]));
  243. }
  244. $user = $accounts->load($username);
  245. $user->update($data, $files);
  246. if (isset($data['groups'])) {
  247. $user->groups = $data['groups'];
  248. }
  249. if (isset($data['access'])) {
  250. $user->access = $data['access'];
  251. }
  252. $user->save();
  253. return $user;
  254. }
  255. /**
  256. * @param string $username
  257. * @param string|null $ip
  258. * @return int Return positive number if rate limited, otherwise return 0.
  259. */
  260. public function checkLoginRateLimit(string $username, string $ip = null): int
  261. {
  262. $ipKey = $this->getIpKey($ip);
  263. $rateLimiter = $this->getRateLimiter('login_attempts');
  264. $rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($username);
  265. // Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
  266. $attempts = \count($rateLimiter->getAttempts($ipKey, 'ip'));
  267. if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($username))) {
  268. return $rateLimiter->getInterval();
  269. }
  270. return 0;
  271. }
  272. /**
  273. * @param string $username
  274. * @param string|null $ip
  275. */
  276. public function resetLoginRateLimit(string $username, string $ip = null): void
  277. {
  278. $ipKey = $this->getIpKey($ip);
  279. $rateLimiter = $this->getRateLimiter('login_attempts');
  280. $rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($username);
  281. }
  282. /**
  283. * @param string|null $ip
  284. * @return string
  285. */
  286. public function getIpKey(string $ip = null): string
  287. {
  288. if (null === $ip) {
  289. $ip = Uri::ip();
  290. }
  291. $isIPv4 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
  292. $ipKey = $isIPv4 ? $ip : Utils::getSubnet($ip, $this->grav['config']->get('plugins.login.ipv6_subnet_size'));
  293. // Pseudonymization of the IP
  294. return sha1($ipKey . $this->grav['config']->get('security.salt'));
  295. }
  296. /**
  297. * @param string $type
  298. * @param mixed $value
  299. * @param string $extra
  300. *
  301. * @return string
  302. */
  303. public function validateField($type, $value, $extra = '')
  304. {
  305. switch ($type) {
  306. case 'user':
  307. case 'username':
  308. /** @var Config $config */
  309. $config = Grav::instance()['config'];
  310. $username_regex = '/' . $config->get('system.username_regex') . '/';
  311. $value = \is_string($value) ? trim($value) : '';
  312. if ($value === '' || !preg_match($username_regex, $value)) {
  313. throw new \RuntimeException('Username does not pass the minimum requirements');
  314. }
  315. break;
  316. case 'password':
  317. case 'password1':
  318. /** @var Config $config */
  319. $config = Grav::instance()['config'];
  320. $pwd_regex = '/' . $config->get('system.pwd_regex') . '/';
  321. $value = \is_string($value) ? $value : '';
  322. if ($value === '' || !preg_match($pwd_regex, $value)) {
  323. throw new \RuntimeException('Password does not pass the minimum requirements');
  324. }
  325. break;
  326. case 'password2':
  327. $value = \is_string($value) ? $value : '';
  328. if ($value === '' || $value !== $extra) {
  329. throw new \RuntimeException('Passwords did not match.');
  330. }
  331. break;
  332. case 'email':
  333. $value = \is_string($value) ? trim($value) : '';
  334. if ($value === '' || !filter_var($value, FILTER_VALIDATE_EMAIL)) {
  335. throw new \RuntimeException('Not a valid email address');
  336. }
  337. break;
  338. case 'permissions':
  339. if (!\in_array($value, ['a', 's', 'b'], true)) {
  340. throw new \RuntimeException('Permissions ' . $value . ' are invalid.');
  341. }
  342. break;
  343. case 'state':
  344. if ($value !== 'enabled' && $value !== 'disabled') {
  345. throw new \RuntimeException('State is not valid');
  346. }
  347. break;
  348. case 'language':
  349. $languages = new LanguageCodes();
  350. if ($value !== null && !array_key_exists($value, $languages->getList())) {
  351. throw new \RuntimeException('Language code is not valid');
  352. }
  353. break;
  354. }
  355. return $value;
  356. }
  357. /**
  358. * Handle the email to notify the user account creation to the site admin.
  359. *
  360. * @param UserInterface $user
  361. *
  362. * @return bool True if the action was performed.
  363. * @throws \RuntimeException
  364. */
  365. public function sendNotificationEmail(UserInterface $user)
  366. {
  367. if (empty($user->email)) {
  368. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  369. }
  370. $site_name = $this->config->get('site.title', 'Website');
  371. $subject = $this->language->translate(['PLUGIN_LOGIN.NOTIFICATION_EMAIL_SUBJECT', $site_name]);
  372. $content = $this->language->translate([
  373. 'PLUGIN_LOGIN.NOTIFICATION_EMAIL_BODY',
  374. $site_name,
  375. $user->username,
  376. $user->email,
  377. $this->grav['base_url_absolute'],
  378. ]);
  379. $to = $this->config->get('plugins.email.to');
  380. if (empty($to)) {
  381. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_NOT_CONFIGURED'));
  382. }
  383. $sent = EmailUtils::sendEmail($subject, $content, $to);
  384. if ($sent < 1) {
  385. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  386. }
  387. return true;
  388. }
  389. /**
  390. * Handle the email to welcome the new user
  391. *
  392. * @param UserInterface $user
  393. *
  394. * @return bool True if the action was performed.
  395. * @throws \RuntimeException
  396. */
  397. public function sendWelcomeEmail(UserInterface $user)
  398. {
  399. if (empty($user->email)) {
  400. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  401. }
  402. $site_name = $this->config->get('site.title', 'Website');
  403. $author = $this->grav['config']->get('site.author.name', '');
  404. $fullname = $user->fullname ?: $user->username;
  405. $subject = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_SUBJECT', $site_name]);
  406. $content = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_BODY',
  407. $fullname,
  408. $this->grav['base_url_absolute'],
  409. $site_name,
  410. $author
  411. ]);
  412. $to = $user->email;
  413. $sent = EmailUtils::sendEmail($subject, $content, $to);
  414. if ($sent < 1) {
  415. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  416. }
  417. return true;
  418. }
  419. /**
  420. * Handle the email to activate the user account.
  421. *
  422. * @param UserInterface $user
  423. *
  424. * @return bool True if the action was performed.
  425. * @throws \RuntimeException
  426. */
  427. public function sendActivationEmail(UserInterface $user)
  428. {
  429. if (empty($user->email)) {
  430. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  431. }
  432. $token = md5(uniqid(mt_rand(), true));
  433. $expire = time() + 604800; // next week
  434. $user->activation_token = $token . '::' . $expire;
  435. $user->save();
  436. $param_sep = $this->config->get('system.param_sep', ':');
  437. $activationRoute = $this->getRoute('activate');
  438. $activation_link = $this->grav['base_url_absolute'] . $activationRoute . '/token' . $param_sep . $token . '/username' . $param_sep . $user->username;
  439. $site_name = $this->config->get('site.title', 'Website');
  440. $author = $this->grav['config']->get('site.author.name', '');
  441. $fullname = $user->fullname ?: $user->username;
  442. $subject = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_SUBJECT', $site_name]);
  443. $content = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_BODY',
  444. $fullname,
  445. $activation_link,
  446. $site_name,
  447. $author
  448. ]);
  449. $to = $user->email;
  450. $sent = EmailUtils::sendEmail($subject, $content, $to);
  451. if ($sent < 1) {
  452. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  453. }
  454. return true;
  455. }
  456. /**
  457. * Handle the email to invite user.
  458. *
  459. * @param Invitation $invitation
  460. * @param string|null $message
  461. * @param UserInterface|null $user
  462. * @return bool True if the action was performed.
  463. * @throws \RuntimeException
  464. */
  465. public function sendInviteEmail(Invitation $invitation, string $message = null, UserInterface $user = null)
  466. {
  467. /** @var UserInterface $user */
  468. $user = $user ?? $this->grav['user'];
  469. $param_sep = $this->config->get('system.param_sep', ':');
  470. $inviteRoute = $this->getRoute('register', true);
  471. $invitationLink = $this->grav['base_url_absolute'] . "{$inviteRoute}/{$param_sep}{$invitation->token}";
  472. $siteName = $this->config->get('site.title', 'Website');
  473. $subject = $this->language->translate(['PLUGIN_LOGIN.INVITATION_EMAIL_SUBJECT', $siteName]);
  474. $message = $message ?? $this->language->translate(['PLUGIN_LOGIN.INVITATION_EMAIL_MESSAGE']);
  475. $content = $this->language->translate(['PLUGIN_LOGIN.INVITATION_EMAIL_BODY',
  476. $siteName,
  477. $message,
  478. $invitationLink,
  479. $user->fullname
  480. ]);
  481. $to = $invitation->email;
  482. $sent = EmailUtils::sendEmail($subject, $content, $to);
  483. if ($sent < 1) {
  484. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  485. }
  486. return true;
  487. }
  488. /**
  489. * Gets and sets the RememberMe class
  490. *
  491. * @param mixed $var A rememberMe instance to set
  492. *
  493. * @return RememberMe Returns the current rememberMe instance
  494. * @throws \InvalidArgumentException
  495. */
  496. public function rememberMe($var = null)
  497. {
  498. if ($var !== null) {
  499. $this->rememberMe = $var;
  500. }
  501. if (!$this->rememberMe) {
  502. /** @var Config $config */
  503. $config = $this->grav['config'];
  504. $cookieName = $config->get('plugins.login.rememberme.name');
  505. $timeout = $config->get('plugins.login.rememberme.timeout');
  506. // Setup storage for RememberMe cookies
  507. $storage = new TokenStorage('user-data://rememberme', $timeout);
  508. $this->rememberMe = new RememberMe($storage);
  509. $this->rememberMe->setCookieName($cookieName);
  510. $this->rememberMe->setExpireTime($timeout);
  511. // Hardening cookies with user-agent and random salt or
  512. // fallback to use system based cache key
  513. $server_agent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
  514. $data = $server_agent . $config->get('security.salt', $this->grav['cache']->getKey());
  515. $this->rememberMe->setSalt(hash('sha512', $data));
  516. // Set cookie with correct base path of Grav install
  517. $cookie = new Cookie;
  518. $cookie->setPath($this->grav['base_url_relative'] ?: '/');
  519. $this->rememberMe->setCookie($cookie);
  520. }
  521. return $this->rememberMe;
  522. }
  523. /**
  524. * Gets and sets the TwoFactorAuth object
  525. *
  526. * @param TwoFactorAuth $var
  527. * @return TwoFactorAuth
  528. * @throws \RobThree\Auth\TwoFactorAuthException
  529. */
  530. public function twoFactorAuth($var = null)
  531. {
  532. if ($var !== null) {
  533. $this->twoFa = $var;
  534. }
  535. if (!$this->twoFa) {
  536. $this->twoFa = new TwoFactorAuth;
  537. }
  538. return $this->twoFa;
  539. }
  540. /**
  541. * @param string $context
  542. * @param int $maxCount
  543. * @param int $interval
  544. * @return RateLimiter
  545. */
  546. public function getRateLimiter($context, $maxCount = null, $interval = null)
  547. {
  548. if (!isset($this->rateLimiters[$context])) {
  549. switch ($context) {
  550. case 'login_attempts':
  551. $maxCount = $this->grav['config']->get('plugins.login.max_login_count', 5);
  552. $interval = $this->grav['config']->get('plugins.login.max_login_interval', 10);
  553. break;
  554. case 'pw_resets':
  555. $maxCount = $this->grav['config']->get('plugins.login.max_pw_resets_count', 2);
  556. $interval = $this->grav['config']->get('plugins.login.max_pw_resets_interval', 60);
  557. break;
  558. }
  559. $this->rateLimiters[$context] = new RateLimiter($context, $maxCount, $interval);
  560. }
  561. return $this->rateLimiters[$context];
  562. }
  563. /**
  564. * @param string $type
  565. * @param string|null $route
  566. * @param PageInterface|null $page
  567. * @return PageInterface|null
  568. */
  569. public function getPage(string $type, string $route = null, PageInterface $page = null): ?PageInterface
  570. {
  571. $route = $route ?? $this->getRoute($type, true);
  572. if (null === $route) {
  573. return null;
  574. }
  575. if ($page) {
  576. $page->route($route);
  577. $page->slug(basename($route));
  578. } else {
  579. /** @var Pages $pages */
  580. $pages = $this->grav['pages'];
  581. $page = $pages->find($route);
  582. }
  583. if (!$page instanceof PageInterface) {
  584. // Only add login page if it hasn't already been defined.
  585. $page = new Page();
  586. $page->init(new \SplFileInfo('plugin://login/pages/' . $type . '.md'));
  587. $page->route($route);
  588. $page->slug(basename($route));
  589. }
  590. // Login page may not have the correct Cache-Control header set, force no-store for the proxies.
  591. $cacheControl = $page->cacheControl();
  592. if (!$cacheControl) {
  593. $page->cacheControl('private, no-cache, must-revalidate');
  594. }
  595. return $page;
  596. }
  597. /**
  598. * Add Login page.
  599. *
  600. * @param string $type
  601. * @param string|null $route Optional route if we want to force-add the page.
  602. * @param PageInterface|null $page
  603. * @return PageInterface|null
  604. */
  605. public function addPage(string $type, string $route = null, PageInterface $page = null): ?PageInterface
  606. {
  607. $page = $this->getPage($type, $route, $page);
  608. if (null === $page) {
  609. return null;
  610. }
  611. /** @var Pages $pages */
  612. $pages = $this->grav['pages'];
  613. $pages->addPage($page, $route);
  614. return $page;
  615. }
  616. /**
  617. * Get route to a given login page.
  618. *
  619. * @param string $type Use one of: login, activate, forgot, reset, profile, unauthorized, after_login, after_logout,
  620. * register, after_registration, after_activation
  621. * @param bool|null $enabled
  622. * @return string|null Returns route or null if the route has been disabled.
  623. */
  624. public function getRoute(string $type, bool $enabled = null): ?string
  625. {
  626. switch ($type) {
  627. case 'login':
  628. $route = $this->config->get('plugins.login.route');
  629. break;
  630. case 'activate':
  631. case 'forgot':
  632. case 'reset':
  633. case 'profile':
  634. $route = $this->config->get('plugins.login.route_' . $type);
  635. break;
  636. case 'unauthorized':
  637. $route = $this->config->get('plugins.login.route_' . $type, '/');
  638. break;
  639. case 'after_login':
  640. case 'after_logout':
  641. $route = $this->config->get('plugins.login.redirect_' . $type);
  642. if ($route === true) {
  643. $route = $this->config->get('plugins.login.route_' . $type);
  644. }
  645. break;
  646. case 'register':
  647. $enabled = $enabled ?? $this->config->get('plugins.login.user_registration.enabled', false);
  648. $route = $enabled === true ? $this->config->get('plugins.login.route_' . $type) : null;
  649. break;
  650. case 'after_registration':
  651. case 'after_activation':
  652. $route = $this->config->get('plugins.login.redirect_' . $type);
  653. break;
  654. default:
  655. $route = null;
  656. }
  657. if (!is_string($route) || $route === '') {
  658. return null;
  659. }
  660. return $route;
  661. }
  662. /**
  663. * @param UserInterface $user
  664. * @param PageInterface $page
  665. * @param Data|null $config
  666. * @return bool
  667. */
  668. public function isUserAuthorizedForPage(UserInterface $user, PageInterface $page, $config = null)
  669. {
  670. $header = $page->header();
  671. $rules = (array)($header->access ?? []);
  672. if (!$rules && $config !== null && $config->get('parent_acl')) {
  673. // If page has no ACL rules, use its parent's rules
  674. $parent = $page->parent();
  675. while (!$rules and $parent) {
  676. $header = $parent->header();
  677. $rules = (array)($header->access ?? []);
  678. $parent = $parent->parent();
  679. }
  680. }
  681. // Continue to the page if it has no ACL rules.
  682. if (!$rules) {
  683. return true;
  684. }
  685. // All protected pages have a private cache-control. This includes pages which are for guests only.
  686. $cacheControl = $page->cacheControl();
  687. if (!$cacheControl) {
  688. $cacheControl = 'private, no-cache, must-revalidate';
  689. } else {
  690. // The response is intended for a single user only and must not be stored by a shared cache.
  691. $cacheControl = str_replace('public', 'private', $cacheControl);
  692. if (strpos($cacheControl, 'private') === false) {
  693. $cacheControl = 'private, ' . $cacheControl;
  694. }
  695. // The cache will send the request to the origin server for validation before releasing a cached copy.
  696. if (strpos($cacheControl, 'no-cache') === false) {
  697. $cacheControl .= ', no-cache';
  698. }
  699. // The cache must verify the status of the stale resources before using the copy and expired ones should not be used.
  700. if (strpos($cacheControl, 'must-revalidate') === false) {
  701. $cacheControl .= ', must-revalidate';
  702. }
  703. }
  704. $page->cacheControl($cacheControl);
  705. // Deny access if user has not completed 2FA challenge.
  706. if ($user->authenticated && !$user->authorized) {
  707. return false;
  708. }
  709. // Continue to the page if user is authorized to access the page.
  710. foreach ($rules as $rule => $value) {
  711. if (is_int($rule)) {
  712. if ($user->authorize($value) === true) {
  713. return true;
  714. }
  715. } elseif (\is_array($value)) {
  716. foreach ($value as $nested_rule => $nested_value) {
  717. if ($user->authorize($rule . '.' . $nested_rule) === Utils::isPositive($nested_value)) {
  718. return true;
  719. }
  720. }
  721. } elseif ($user->authorize($rule) === Utils::isPositive($value)) {
  722. return true;
  723. }
  724. }
  725. return false;
  726. }
  727. /**
  728. * Check if user may use password reset functionality.
  729. *
  730. * @param UserInterface $user
  731. * @param string $field
  732. * @param int $count
  733. * @param int $interval
  734. * @return bool
  735. * @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
  736. */
  737. public function isUserRateLimited(UserInterface $user, $field, $count, $interval)
  738. {
  739. if ($count > 0) {
  740. if (!isset($user->{$field})) {
  741. $user->{$field} = [];
  742. }
  743. //remove older than $interval x minute attempts
  744. $actual_resets = [];
  745. foreach ((array)$user->{$field} as $reset) {
  746. if ($reset > (time() - $interval * 60)) {
  747. $actual_resets[] = $reset;
  748. }
  749. }
  750. if (\count($actual_resets) >= $count) {
  751. return true;
  752. }
  753. $actual_resets[] = time(); // current reset
  754. $user->{$field} = $actual_resets;
  755. }
  756. return false;
  757. }
  758. /**
  759. * Reset the rate limit counter.
  760. *
  761. * @param UserInterface $user
  762. * @param string $field
  763. * @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
  764. */
  765. public function resetRateLimit(UserInterface $user, $field)
  766. {
  767. $user->{$field} = [];
  768. }
  769. /**
  770. * Get Current logged in user
  771. *
  772. * @return UserInterface
  773. * @deprecated 2.5.0 Use $grav['user'] instead.
  774. */
  775. public function getUser()
  776. {
  777. /** @var UserInterface $user */
  778. return $this->grav['user'];
  779. }
  780. public function addProviderLoginTemplate($template)
  781. {
  782. $this->provider_login_templates[] = $template;
  783. }
  784. public function getProviderLoginTemplates()
  785. {
  786. return $this->provider_login_templates;
  787. }
  788. }