123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- <?php
- /**
- * @package Grav\Plugin\Login
- *
- * @copyright Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Plugin\Login;
- use Birke\Rememberme\Cookie;
- use Grav\Common\Config\Config;
- use Grav\Common\Data\Data;
- use Grav\Common\Grav;
- use Grav\Common\File\CompiledYamlFile;
- use Grav\Common\Language\Language;
- use Grav\Common\Page\Page;
- use Grav\Common\Session;
- use Grav\Common\User\User;
- use Grav\Common\Uri;
- use Grav\Plugin\Email\Utils as EmailUtils;
- use Grav\Plugin\Login\Events\UserLoginEvent;
- use Grav\Plugin\Login\RememberMe\RememberMe;
- use Grav\Plugin\Login\RememberMe\TokenStorage;
- use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
- /**
- * Class Login
- * @package Grav\Plugin
- */
- class Login
- {
- /** @var Grav */
- protected $grav;
- /** @var Config */
- protected $config;
- /** @var Language $language */
- protected $language;
- /** @var Session */
- protected $session;
- /** @var Uri */
- protected $uri;
- /** @var RememberMe */
- protected $rememberMe;
- /** @var TwoFactorAuth */
- protected $twoFa;
- /** @var RateLimiter[] */
- protected $rateLimiters = [];
- /** @var array */
- protected $provider_login_templates = [];
- /**
- * Login constructor.
- *
- * @param Grav $grav
- */
- public function __construct(Grav $grav)
- {
- $this->grav = $grav;
- $this->config = $this->grav['config'];
- $this->language = $this->grav['language'];
- $this->session = $this->grav['session'];
- $this->uri = $this->grav['uri'];
- }
- /**
- * Login user.
- *
- * @param array $credentials Login credentials, eg: ['username' => '', 'password' => '']
- * @param array $options Login options, eg: ['remember_me' => true]
- * @param array $extra Example: ['authorize' => 'site.login', 'user' => null], undefined variables get set.
- * @return User|UserLoginEvent Returns event if $extra['return_event'] is true.
- */
- public function login(array $credentials, array $options = [], array $extra = [])
- {
- $grav = Grav::instance();
- $eventOptions = [
- 'credentials' => $credentials,
- 'options' => $options
- ] + $extra;
- // Attempt to authenticate the user.
- $event = new UserLoginEvent($eventOptions);
- $grav->fireEvent('onUserLoginAuthenticate', $event);
- if ($event->isSuccess()) {
- // Make sure that event didn't mess up with the user authorization.
- $user = $event->getUser();
- $user->authenticated = true;
- $user->authorized = false;
- // Allow plugins to prevent login after successful authentication.
- $event = new UserLoginEvent($event->toArray());
- $grav->fireEvent('onUserLoginAuthorize', $event);
- }
- if ($event->isSuccess()) {
- // User has been logged in, let plugins know.
- $event = new UserLoginEvent($event->toArray());
- $grav->fireEvent('onUserLogin', $event);
- // Make sure that event didn't mess up with the user authorization.
- $user = $event->getUser();
- $user->authenticated = true;
- $user->authorized = !$event->isDelayed();
- } else {
- // Allow plugins to log errors or do other tasks on failure.
- $event = new UserLoginEvent($event->toArray());
- $grav->fireEvent('onUserLoginFailure', $event);
- // Make sure that event didn't mess up with the user authorization.
- $user = $event->getUser();
- $user->authenticated = false;
- $user->authorized = false;
- }
- $user = $event->getUser();
- $user->def('language', 'en');
- return !empty($event['return_event']) ? $event : $user;
- }
- /**
- * Logout user.
- *
- * @param array $options
- * @param array|User $extra Array of: ['user' => $user, ...] or User object (deprecated).
- * @return User|UserLoginEvent Returns event if $extra['return_event'] is true.
- */
- public function logout(array $options = [], $extra = [])
- {
- $grav = Grav::instance();
- if ($extra instanceof User) {
- $extra = ['user' => $extra];
- } elseif (isset($extra['user'])) {
- $extra['user'] = $grav['user'];
- }
- $eventOptions = [
- 'options' => $options
- ] + $extra;
- $event = new UserLoginEvent($eventOptions);
- // Logout the user.
- $grav->fireEvent('onUserLogout', $event);
- $user = $event->getUser();
- $user->authenticated = false;
- $user->authorized = false;
- return !empty($event['return_event']) ? $event : $user;
- }
- /**
- * Authenticate user.
- *
- * @param array $credentials Form fields.
- * @param array $options
- *
- * @return bool
- */
- public function authenticate($credentials, $options = ['remember_me' => true])
- {
- $event = $this->login($credentials, $options, ['return_event' => true]);
- $user = $event['user'];
- $redirect = $event->getRedirect();
- $message = $event->getMessage();
- $messageType = $event->getMessageType();
- if ($user->authenticated && $user->authorized) {
- if (!$message) {
- $message = 'PLUGIN_LOGIN.LOGIN_SUCCESSFUL';
- $messageType = 'info';
- }
- if (!$redirect) {
- $redirect = $this->uri->route();
- }
- }
- if ($message) {
- $this->grav['messages']->add($this->language->translate($message, [$user->language]), $messageType);
- }
- if ($redirect) {
- $this->grav->redirectLangSafe($redirect, $event->getRedirectCode());
- }
- return $user->authenticated && $user->authorized;
- }
- /**
- * Create a new user file
- *
- * @param array $data
- *
- * @return User
- */
- public function register($data)
- {
- if (!isset($data['groups'])) {
- //Add new user ACL settings
- $groups = (array) $this->config->get('plugins.login.user_registration.groups', []);
- if (count($groups) > 0) {
- $data['groups'] = $groups;
- }
- }
- if (!isset($data['access'])) {
- $access = (array) $this->config->get('plugins.login.user_registration.access.site', []);
- if (count($access) > 0) {
- $data['access']['site'] = $access;
- }
- }
- $username = $this->validateField('username', $data['username']);
- $file = CompiledYamlFile::instance($this->grav['locator']->findResource('account://' . mb_strtolower($username) . YAML_EXT,
- true, true));
- // Create user object and save it
- $user = new User($data);
- $user->file($file);
- $user->save();
- return $user;
- }
- /**
- * @param string $type
- * @param mixed $value
- * @param string $extra
- *
- * @return string
- */
- public function validateField($type, $value, $extra = '')
- {
- switch ($type) {
- case 'user':
- case 'username':
- /** @var Config $config */
- $config = Grav::instance()['config'];
- $username_regex = '/' . $config->get('system.username_regex') . '/';
- if (!is_string($value) || !preg_match($username_regex, $value)) {
- throw new \RuntimeException('Username should be between 3 and 16 characters, including lowercase letters, numbers, underscores, and hyphens. Uppercase letters, spaces, and special characters are not allowed');
- }
- break;
- case 'password1':
- /** @var Config $config */
- $config = Grav::instance()['config'];
- $pwd_regex = '/' . $config->get('system.pwd_regex') . '/';
- if (!is_string($value) || !preg_match($pwd_regex, $value)) {
- throw new \RuntimeException('Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters');
- }
- break;
- case 'password2':
- if (!is_string($value) || strcmp($value, $extra)) {
- throw new \RuntimeException('Passwords did not match.');
- }
- break;
- case 'email':
- if (!is_string($value) || !filter_var($value, FILTER_VALIDATE_EMAIL)) {
- throw new \RuntimeException('Not a valid email address');
- }
- break;
- case 'permissions':
- if (!is_string($value) || !in_array($value, ['a', 's', 'b'])) {
- throw new \RuntimeException('Permissions ' . $value . ' are invalid.');
- }
- break;
- case 'fullname':
- if (!is_string($value) || trim($value) === '') {
- throw new \RuntimeException('Fullname cannot be empty');
- }
- break;
- case 'state':
- if ($value !== 'enabled' && $value !== 'disabled') {
- throw new \RuntimeException('State is not valid');
- }
- break;
- }
- return $value;
- }
- /**
- * Handle the email to notify the user account creation to the site admin.
- *
- * @param User $user
- *
- * @return bool True if the action was performed.
- * @throws \RuntimeException
- */
- public function sendNotificationEmail(User $user)
- {
- if (empty($user->email)) {
- throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
- }
- $site_name = $this->config->get('site.title', 'Website');
- $subject = $this->language->translate(['PLUGIN_LOGIN.NOTIFICATION_EMAIL_SUBJECT', $site_name]);
- $content = $this->language->translate([
- 'PLUGIN_LOGIN.NOTIFICATION_EMAIL_BODY',
- $site_name,
- $user->username,
- $user->email,
- $this->grav['base_url_absolute'],
- ]);
- $to = $this->config->get('plugins.email.from');
- if (empty($to)) {
- throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_NOT_CONFIGURED'));
- }
- $sent = EmailUtils::sendEmail($subject, $content, $to);
- if ($sent < 1) {
- throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
- }
- return true;
- }
- /**
- * Handle the email to welcome the new user
- *
- * @param User $user
- *
- * @return bool True if the action was performed.
- * @throws \RuntimeException
- */
- public function sendWelcomeEmail(User $user)
- {
- if (empty($user->email)) {
- throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
- }
- $site_name = $this->config->get('site.title', 'Website');
- $author = $this->grav['config']->get('site.author.name', '');
- $fullname = $user->fullname ?: $user->username;
- $subject = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_SUBJECT', $site_name]);
- $content = $this->language->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_BODY',
- $fullname,
- $this->grav['base_url_absolute'],
- $site_name,
- $author
- ]);
- $to = $user->email;
- $sent = EmailUtils::sendEmail($subject, $content, $to);
- if ($sent < 1) {
- throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
- }
- return true;
- }
- /**
- * Handle the email to activate the user account.
- *
- * @param User $user
- *
- * @return bool True if the action was performed.
- * @throws \RuntimeException
- */
- public function sendActivationEmail(User $user)
- {
- if (empty($user->email)) {
- throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD'));
- }
- $token = md5(uniqid(mt_rand(), true));
- $expire = time() + 604800; // next week
- $user->activation_token = $token . '::' . $expire;
- $user->save();
- $param_sep = $this->config->get('system.param_sep', ':');
- $activation_link = $this->grav['base_url_absolute'] . $this->config->get('plugins.login.route_activate') . '/token' . $param_sep . $token . '/username' . $param_sep . $user->username;
- $site_name = $this->config->get('site.title', 'Website');
- $author = $this->grav['config']->get('site.author.name', '');
- $fullname = $user->fullname ?: $user->username;
- $subject = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_SUBJECT', $site_name]);
- $content = $this->language->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_BODY',
- $fullname,
- $activation_link,
- $site_name,
- $author
- ]);
- $to = $user->email;
- $sent = EmailUtils::sendEmail($subject, $content, $to);
- if ($sent < 1) {
- throw new \RuntimeException($this->language->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE'));
- }
- return true;
- }
- /**
- * Gets and sets the RememberMe class
- *
- * @param mixed $var A rememberMe instance to set
- *
- * @return RememberMe Returns the current rememberMe instance
- * @throws \InvalidArgumentException
- */
- public function rememberMe($var = null)
- {
- if ($var !== null) {
- $this->rememberMe = $var;
- }
- if (!$this->rememberMe) {
- /** @var Config $config */
- $config = $this->grav['config'];
- $cookieName = $config->get('plugins.login.rememberme.name');
- $timeout = $config->get('plugins.login.rememberme.timeout');
- // Setup storage for RememberMe cookies
- $storage = new TokenStorage('user://data/rememberme', $timeout);
- $this->rememberMe = new RememberMe($storage);
- $this->rememberMe->setCookieName($cookieName);
- $this->rememberMe->setExpireTime($timeout);
- // Hardening cookies with user-agent and random salt or
- // fallback to use system based cache key
- $server_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'unknown';
- $data = $server_agent . $config->get('security.salt', $this->grav['cache']->getKey());
- $this->rememberMe->setSalt(hash('sha512', $data));
- // Set cookie with correct base path of Grav install
- $cookie = new Cookie;
- $cookie->setPath($this->grav['base_url_relative'] ?: '/');
- $this->rememberMe->setCookie($cookie);
- }
- return $this->rememberMe;
- }
- /**
- * Gets and sets the TwoFactorAuth object
- *
- * @param TwoFactorAuth $var
- * @return TwoFactorAuth
- * @throws \RobThree\Auth\TwoFactorAuthException
- */
- public function twoFactorAuth($var = null)
- {
- if ($var !== null) {
- $this->twoFa = $var;
- }
- if (!$this->twoFa) {
- $this->twoFa = new TwoFactorAuth;
- }
- return $this->twoFa;
- }
- /**
- * @param string $context
- * @param int $maxCount
- * @param int $interval
- * @return RateLimiter
- */
- public function getRateLimiter($context, $maxCount = null, $interval = null)
- {
- if (!isset($this->rateLimiters[$context])) {
- switch ($context) {
- case 'login_attempts':
- $maxCount = $this->grav['config']->get('plugins.login.max_login_count', 5);
- $interval = $this->grav['config']->get('plugins.login.max_login_interval', 10);
- break;
- case 'pw_resets':
- $maxCount = $this->grav['config']->get('plugins.login.max_pw_resets_count', 0);
- $interval = $this->grav['config']->get('plugins.login.max_pw_resets_interval', 2);
- break;
- }
- $this->rateLimiters[$context] = new RateLimiter($context, $maxCount, $interval);
- }
- return $this->rateLimiters[$context];
- }
- /**
- * @param User $user
- * @param Page $page
- * @param Data|null $config
- * @return bool
- */
- public function isUserAuthorizedForPage(User $user, Page $page, $config = null)
- {
- $header = $page->header();
- $rules = isset($header->access) ? (array)$header->access : [];
- if ($config !== null && $config->get('parent_acl')) {
- // If page has no ACL rules, use its parent's rules
- if (!$rules) {
- $parent = $page->parent();
- while (!$rules and $parent) {
- $header = $parent->header();
- $rules = isset($header->access) ? (array)$header->access : [];
- $parent = $parent->parent();
- }
- }
- }
- // Continue to the page if it has no ACL rules.
- if (!$rules) {
- return true;
- }
- if (!$user->authorized) {
- return false;
- }
- // Continue to the page if user is authorized to access the page.
- foreach ($rules as $rule => $value) {
- if (is_array($value)) {
- foreach ($value as $nested_rule => $nested_value) {
- if ($user->authorize($rule . '.' . $nested_rule) == $nested_value) {
- return true;
- }
- }
- } else {
- if ($user->authorize($rule) == $value) {
- return true;
- }
- }
- }
- return false;
- }
- /**
- * Check if user may use password reset functionality.
- *
- * @param User $user
- * @param string $field
- * @param int $count
- * @param int $interval
- * @return bool
- * @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
- */
- public function isUserRateLimited(User $user, $field, $count, $interval)
- {
- if ($count > 0) {
- if (!isset($user->{$field})) {
- $user->{$field} = [];
- }
- //remove older than $interval x minute attempts
- $actual_resets = [];
- foreach ((array)$user->{$field} as $reset) {
- if ($reset > (time() - $interval * 60)) {
- $actual_resets[] = $reset;
- }
- }
- if (count($actual_resets) >= $count) {
- return true;
- }
- $actual_resets[] = time(); // current reset
- $user->{$field} = $actual_resets;
- }
- return false;
- }
- /**
- * Reset the rate limit counter.
- *
- * @param User $user
- * @param string $field
- * @deprecated 2.5.0 Use $grav['login']->getRateLimiter($context) instead. See Grav\Plugin\Login\RateLimiter class.
- */
- public function resetRateLimit(User $user, $field)
- {
- $user->{$field} = [];
- }
- /**
- * Get Current logged in user
- *
- * @return User
- * @deprecated 2.5.0 Use $grav['user'] instead.
- */
- public function getUser()
- {
- /** @var User $user */
- return $this->grav['user'];
- }
- public function addProviderLoginTemplate($template)
- {
- $this->provider_login_templates[] = $template;
- }
- public function getProviderLoginTemplates()
- {
- $templates = $this->provider_login_templates;
- return $templates;
- }
- }
|