Login.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  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\Login;
  9. use Birke\Rememberme\Cookie;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Data\Data;
  12. use Grav\Common\Grav;
  13. use Grav\Common\File\CompiledYamlFile;
  14. use Grav\Common\Language\Language;
  15. use Grav\Common\Page\Page;
  16. use Grav\Common\Session;
  17. use Grav\Common\User\User;
  18. use Grav\Common\Uri;
  19. use Grav\Plugin\Email\Utils as EmailUtils;
  20. use Grav\Plugin\Login\Events\UserLoginEvent;
  21. use Grav\Plugin\Login\RememberMe\RememberMe;
  22. use Grav\Plugin\Login\RememberMe\TokenStorage;
  23. use RocketTheme\Toolbox\Session\Message;
  24. /**
  25. * Class Login
  26. * @package Grav\Plugin
  27. */
  28. class Login
  29. {
  30. /** @var Grav */
  31. protected $grav;
  32. /** @var Config */
  33. protected $config;
  34. /** @var Language $language */
  35. protected $language;
  36. /** @var Session */
  37. protected $session;
  38. /** @var Uri */
  39. protected $uri;
  40. /** @var RememberMe */
  41. protected $rememberMe;
  42. /** @var RateLimiter[] */
  43. protected $rateLimiters = [];
  44. /**
  45. * Login constructor.
  46. *
  47. * @param Grav $grav
  48. */
  49. public function __construct(Grav $grav)
  50. {
  51. $this->grav = $grav;
  52. $this->config = $this->grav['config'];
  53. $this->language = $this->grav['language'];
  54. $this->session = $this->grav['session'];
  55. $this->uri = $this->grav['uri'];
  56. }
  57. /**
  58. * Login user.
  59. *
  60. * @param array $credentials
  61. * @param array $options
  62. * @param array $extra Example: ['authorize' => 'site.login', 'user' => null], undefined variables gets set.
  63. * @return User
  64. */
  65. public function login(array $credentials, array $options = [], array $extra = [])
  66. {
  67. $grav = Grav::instance();
  68. $eventOptions = [
  69. 'credentials' => $credentials,
  70. 'options' => $options
  71. ] + $extra;
  72. // Attempt to authenticate the user.
  73. $event = new UserLoginEvent($eventOptions);
  74. $grav->fireEvent('onUserLoginAuthenticate', $event);
  75. if ($event->isSuccess()) {
  76. // Make sure that event didn't mess up with the user authorization.
  77. $user = $event->getUser();
  78. $user->authenticated = true;
  79. $user->authorized = false;
  80. // Allow plugins to prevent login after successful authentication.
  81. $event = new UserLoginEvent($event->toArray());
  82. $grav->fireEvent('onUserLoginAuthorize', $event);
  83. }
  84. if ($event->isSuccess()) {
  85. // User has been logged in, let plugins know.
  86. $event = new UserLoginEvent($event->toArray());
  87. $grav->fireEvent('onUserLogin', $event);
  88. // Make sure that event didn't mess up with the user authorization.
  89. $user = $event->getUser();
  90. $user->authenticated = true;
  91. $user->authorized = $event->isDelayed();
  92. } else {
  93. // Allow plugins to log errors or do other tasks on failure.
  94. $event = new UserLoginEvent($event->toArray());
  95. $grav->fireEvent('onUserLoginFailure', $event);
  96. // Make sure that event didn't mess up with the user authorization.
  97. $user = $event->getUser();
  98. $user->authenticated = false;
  99. $user->authorized = false;
  100. }
  101. $user = $event->getUser();
  102. $user->def('language', 'en');
  103. return $user;
  104. }
  105. /**
  106. * Logout user.
  107. *
  108. * @param array $options
  109. * @param User $user
  110. * @return User
  111. */
  112. public function logout(array $options = [], User $user = null)
  113. {
  114. $grav = Grav::instance();
  115. $eventOptions = [
  116. 'user' => $user ?: $grav['user'],
  117. 'options' => $options
  118. ];
  119. $event = new UserLoginEvent($eventOptions);
  120. // Logout the user.
  121. $grav->fireEvent('onUserLogout', $event);
  122. $user = $event->getUser();
  123. $user->authenticated = false;
  124. return $user;
  125. }
  126. /**
  127. * Authenticate user.
  128. *
  129. * @param array $credentials Form fields.
  130. * @param array $options
  131. *
  132. * @return bool
  133. */
  134. public function authenticate($credentials, $options = ['remember_me' => true])
  135. {
  136. $user = $this->login($credentials, $options);
  137. if ($user->authenticated) {
  138. $this->grav['messages']->add($this->language->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL',
  139. [$user->language]), 'info');
  140. $redirect_route = $this->uri->route();
  141. $this->grav->redirect($redirect_route);
  142. }
  143. return $user->authenticated;
  144. }
  145. /**
  146. * Create a new user file
  147. *
  148. * @param array $data
  149. *
  150. * @return User
  151. */
  152. public function register($data)
  153. {
  154. //Add new user ACL settings
  155. $groups = $this->config->get('plugins.login.user_registration.groups', []);
  156. if (count($groups) > 0) {
  157. $data['groups'] = $groups;
  158. }
  159. $access = $this->config->get('plugins.login.user_registration.access.site', []);
  160. if (count($access) > 0) {
  161. $data['access']['site'] = $access;
  162. }
  163. $username = $data['username'];
  164. $file = CompiledYamlFile::instance($this->grav['locator']->findResource('account://' . $username . YAML_EXT,
  165. true, true));
  166. // Create user object and save it
  167. $user = new User($data);
  168. $user->file($file);
  169. $user->save();
  170. return $user;
  171. }
  172. /**
  173. * Handle the email to notify the user account creation to the site admin.
  174. *
  175. * @param User $user
  176. *
  177. * @return bool True if the action was performed.
  178. * @throws \RuntimeException
  179. */
  180. public function sendNotificationEmail(User $user)
  181. {
  182. if (empty($user->email)) {
  183. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  184. }
  185. $site_name = $this->config->get('site.title', 'Website');
  186. $subject = $this->language->translate(['PLUGIN_LOGIN.NOTIFICATION_EMAIL_SUBJECT', $site_name]);
  187. $content = $this->language->translate([
  188. 'PLUGIN_LOGIN.NOTIFICATION_EMAIL_BODY',
  189. $site_name,
  190. $user->username,
  191. $user->email,
  192. $this->grav['base_url_absolute'],
  193. ]);
  194. $to = $this->config->get('plugins.email.from');
  195. if (empty($to)) {
  196. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_NOT_CONFIGURED'));
  197. }
  198. $sent = EmailUtils::sendEmail($subject, $content, $to);
  199. if ($sent < 1) {
  200. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  201. }
  202. return true;
  203. }
  204. /**
  205. * Handle the email to welcome the new user
  206. *
  207. * @param User $user
  208. *
  209. * @return bool True if the action was performed.
  210. * @throws \RuntimeException
  211. */
  212. public function sendWelcomeEmail(User $user)
  213. {
  214. if (empty($user->email)) {
  215. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  216. }
  217. $site_name = $this->config->get('site.title', 'Website');
  218. $author = $this->grav['config']->get('site.author.name', '');
  219. $fullname = $user->fullname ?: $user->username;
  220. $subject = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_SUBJECT', $site_name]);
  221. $content = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_BODY',
  222. $fullname,
  223. $this->grav['base_url_absolute'],
  224. $site_name,
  225. $author
  226. ]);
  227. $to = $user->email;
  228. $sent = EmailUtils::sendEmail($subject, $content, $to);
  229. if ($sent < 1) {
  230. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  231. }
  232. return true;
  233. }
  234. /**
  235. * Handle the email to activate the user account.
  236. *
  237. * @param User $user
  238. *
  239. * @return bool True if the action was performed.
  240. * @throws \RuntimeException
  241. */
  242. public function sendActivationEmail(User $user)
  243. {
  244. if (empty($user->email)) {
  245. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
  246. }
  247. $token = md5(uniqid(mt_rand(), true));
  248. $expire = time() + 604800; // next week
  249. $user->activation_token = $token . '::' . $expire;
  250. $user->save();
  251. $param_sep = $this->config->get('system.param_sep', ':');
  252. $activation_link = $this->grav['base_url_absolute'] . $this->config->get('plugins.login.route_activate') . '/token' . $param_sep . $token . '/username' . $param_sep . $user->username;
  253. $site_name = $this->config->get('site.title', 'Website');
  254. $author = $this->grav['config']->get('site.author.name', '');
  255. $fullname = $user->fullname ?: $user->username;
  256. $subject = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_SUBJECT', $site_name]);
  257. $content = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_BODY',
  258. $fullname,
  259. $activation_link,
  260. $site_name,
  261. $author
  262. ]);
  263. $to = $user->email;
  264. $sent = EmailUtils::sendEmail($subject, $content, $to);
  265. if ($sent < 1) {
  266. throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
  267. }
  268. return true;
  269. }
  270. /**
  271. * Gets and sets the RememberMe class
  272. *
  273. * @param mixed $var A rememberMe instance to set
  274. *
  275. * @return RememberMe Returns the current rememberMe instance
  276. * @throws \InvalidArgumentException
  277. */
  278. public function rememberMe($var = null)
  279. {
  280. if ($var !== null) {
  281. $this->rememberMe = $var;
  282. }
  283. if (!$this->rememberMe) {
  284. /** @var Config $config */
  285. $config = $this->grav['config'];
  286. // Setup storage for RememberMe cookies
  287. $storage = new TokenStorage;
  288. $this->rememberMe = new RememberMe($storage);
  289. $this->rememberMe->setCookieName($config->get('plugins.login.rememberme.name'));
  290. $this->rememberMe->setExpireTime($config->get('plugins.login.rememberme.timeout'));
  291. // Hardening cookies with user-agent and random salt or
  292. // fallback to use system based cache key
  293. $server_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown';
  294. $data = $server_agent . $config->get('security.salt', $this->grav['cache']->getKey());
  295. $this->rememberMe->setSalt(hash('sha512', $data));
  296. // Set cookie with correct base path of Grav install
  297. $cookie = new Cookie;
  298. $cookie->setPath($this->grav['base_url_relative'] ?: '/');
  299. $this->rememberMe->setCookie($cookie);
  300. }
  301. return $this->rememberMe;
  302. }
  303. /**
  304. * @param string $context
  305. * @param int $maxCount
  306. * @param int $interval
  307. * @return RateLimiter
  308. */
  309. public function getRateLimiter($context, $maxCount = null, $interval = null)
  310. {
  311. if (!isset($this->rateLimiters[$context])) {
  312. switch ($context) {
  313. case 'login_attempts':
  314. $maxCount = $this->grav['config']->get('plugins.login.max_login_count', 5);
  315. $interval = $this->grav['config']->get('plugins.login.max_login_interval', 10);
  316. break;
  317. case 'pw_resets':
  318. $maxCount = $this->grav['config']->get('plugins.login.max_pw_resets_count', 0);
  319. $interval = $this->grav['config']->get('plugins.login.max_pw_resets_interval', 2);
  320. break;
  321. }
  322. $this->rateLimiters[$context] = new RateLimiter($context, $maxCount, $interval);
  323. }
  324. return $this->rateLimiters[$context];
  325. }
  326. /**
  327. * @param User $user
  328. * @param Page $page
  329. * @param Data|null $config
  330. * @return bool
  331. */
  332. public function isUserAuthorizedForPage(User $user, Page $page, $config = null)
  333. {
  334. $header = $page->header();
  335. $rules = isset($header->access) ? (array)$header->access : [];
  336. if ($config !== null && $config->get('parent_acl')) {
  337. // If page has no ACL rules, use its parent's rules
  338. if (!$rules) {
  339. $parent = $page->parent();
  340. while (!$rules and $parent) {
  341. $header = $parent->header();
  342. $rules = isset($header->access) ? (array)$header->access : [];
  343. $parent = $parent->parent();
  344. }
  345. }
  346. }
  347. // Continue to the page if it has no ACL rules.
  348. if (!$rules) {
  349. return true;
  350. }
  351. // Continue to the page if user is authorized to access the page.
  352. foreach ($rules as $rule => $value) {
  353. if (is_array($value)) {
  354. foreach ($value as $nested_rule => $nested_value) {
  355. if ($user->authorize($rule . '.' . $nested_rule) == $nested_value) {
  356. return true;
  357. }
  358. }
  359. } else {
  360. if ($user->authorize($rule) == $value) {
  361. return true;
  362. }
  363. }
  364. }
  365. return false;
  366. }
  367. /**
  368. * Check if user may use password reset functionality.
  369. *
  370. * @param User $user
  371. * @param string $field
  372. * @param int $count
  373. * @param int $interval
  374. * @return bool
  375. * @deprecated 3.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
  376. */
  377. public function isUserRateLimited(User $user, $field, $count, $interval)
  378. {
  379. if ($count > 0) {
  380. if (!isset($user->{$field})) {
  381. $user->{$field} = array();
  382. }
  383. //remove older than 1 hour attempts
  384. $actual_resets = array();
  385. foreach ($user->{$field} as $reset) {
  386. if ($reset > (time() - $interval * 60)) {
  387. $actual_resets[] = $reset;
  388. }
  389. }
  390. if (count($actual_resets) >= $count) {
  391. return true;
  392. }
  393. $actual_resets[] = time(); // current reset
  394. $user->{$field} = $actual_resets;
  395. }
  396. return false;
  397. }
  398. /**
  399. * Reset the rate limit counter.
  400. *
  401. * @param User $user
  402. * @param string $field
  403. * @deprecated 3.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
  404. */
  405. public function resetRateLimit(User $user, $field)
  406. {
  407. $user->{$field} = [];
  408. }
  409. /**
  410. * Get Current logged in user
  411. *
  412. * @return User
  413. * @deprecated 3.0 Use $grav['user'] instead.
  414. */
  415. public function getUser()
  416. {
  417. /** @var User $user */
  418. return $this->grav['user'];
  419. }
  420. }