diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8d546..fe1ab8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,65 @@ +# v2.0.0 +## 07/14/2016 + +1. [](#improved) + * Optimized nonce creation + * Point account path to core's account stream [#85](https://github.com/getgrav/grav-plugin-login/issues/85) + +# v2.0.0-rc.2 +## 06/21/2016 + +1. [](#new) + * Add an option to login protect a login-protected page media accessed through the page route [#45](https://github.com/getgrav/grav-plugin-login/issues/45) +1. [](#improved) + * Fixed some language keys +1. [](#bugfix) + * Correctly show an error message when the reset password form does not provide the correct nonce + +# v2.0.0-rc.1 +## 06/01/2016 + +1. [](#improved) + * French updated +1. [](#bugfix) + * Enable twig processing in a page #75 + * Deny access to registration when user registration is disabled #72 + +# v2.0.0-beta.3 +## 05/23/2016 + +1. [](#improved) + * Added a redirect after activation + * Changed hardcoded redirect routes to config-based +1. [](#bugfix) + * Fix a redirect issue #74 + * Don't error if missing a HTTP_USER_AGENT browser string + +# v2.0.0-beta.2 +## 05/03/2016 + +1. [](#improved) + * Improved the login form page once logged in + * Translate welcome and logout strings +1. [](#bugfix) + * Fixed logging out on the homepage + * Fixed an issue in processing user registration + +# v2.0.0-beta.1 +## 04/20/2016 + +1. [](#new) + * Introduce a more flexible Login plugin architecture, which allows separate authentication plugins to hook into the Login events. Separated OAuth to its own plugin. + * OAuth has been separated to its own plugin, needs to be installed separately and configured. The users account filename format has changed too, to fix an issue that involved people with the same name on a service. + * The `redirect` option has been changed to `redirect_after_login`. Make sure you update your configuration file. +1. [](#improved) + * Add a proper 'Access levels' config section for Login. + * Various underlying improvements + * Updated french, added german +1. [](#bugfix) + * Make username field autofocus + * Add validation to the password reset form + * Fixed an issue that allowed a user logged in, without access to the actual permissions set to view a page, to see its content, and the login form again even if already logged in. + # v1.3.1 ## 02/05/2016 @@ -9,7 +71,7 @@ * Add the correct message type when raising a form processing error 1. [](#bugfix) * Show the correct error message when the user is not authorized to view a page - * Fix showing the OAuth links in the login form + * Fix showing the OAuth links in the login form # v1.3.0 ## 01/06/2016 @@ -60,7 +122,132 @@ * Check page exists so as not to fail hard * Fix for static Inflector references #17 - + +# v1.0.1 +## 11/23/2015 + +1. [](#improved) + * Hardening cookies with user-agent and system cache key instead of deprecated system hash + * Set a custom route for login only if it's not an admin path + +# v1.0.0 +## 11/21/2015 + +1. [](#new) + * Added OAuth login support for _Facebook_, _Google_, _GitHub_ and _Twitter_ + * Added **Nonce** form security support + * Added option to "redirect after login" + * Added "remember me" functionality + * Added Hungarian translation +2. [](#improved) + * Added blueprints for Grav Admin plugin (multi-language support!) + +# v0.3.3 +## 09/11/2015 + +1. [](#improved) + * Changed authorise to authorize +1. [](#bugfix) + * Fix denied string + +# v0.3.2 +## 09/01/2015 + +1. [](#improved) + * Broke out login form into its own partial + +# v0.3.1 +## 08/31/2015 + +1. [](#improved) + * Added username field autofocus + +# v0.3.0 +## 08/24/2015 + +1. [](#new) + * Added simple CSS styling + * Added simple login status with logout +1. [](#improved) + * Improved README documentation + * More strings translated + * Updated blueprints + +# v0.2.0 +## 08/11/2015 + +1. [](#improved) + * Disable `enable` in admin + +# v0.1.0 +## 08/04/2015 + +1. [](#new) + * ChangeLog started... + +# v1.3.1 +## 02/05/2016 + +1. [](#new) + * Add translations for Username and Password (placeholders are not translated) +1. [](#improved) + * Improve registration, forgot, reset and login forms accessibility by setting the id attribute + * Improved french translation + * Add the correct message type when raising a form processing error +1. [](#bugfix) + * Show the correct error message when the user is not authorized to view a page + * Fix showing the OAuth links in the login form + +# v1.3.0 +## 01/06/2016 + +1. [](#new) + * Added a new CLI command to change a user's password + * Added a new CLI command to edit the user state +1. [](#improved) + * Improved french translation + +# v1.2.1 +## 12/18/2015 + +1. [](#new) + * Croatian translation +1. [](#improved) + * Use type `email` in registration form + * Drop manual validation in registration + +# v1.2.0 +## 12/11/2015 + +1. [](#new) + * Added account activation email upon registration + * Added forgot password functionality + * Support ACL from parent page + * Allow login immediately after account activation +1. [](#improved) + * Handle admin login page if available + * Example registration form now provided by plugin + * Better error handling of registration + * Tab-based plugin configuration + * Updated translations +1. [](#bugfix) + * Prevent failing when no default values are set + +# v1.1.0 +## 12/01/2015 + +1. [](#new) + * Support new **User Registration** +1. [](#improved) + * Use new security salt for newer and fallback otherwise + * Composer update of libraries + * Check for session existence else throw a runtime error +1. [](#bugfix) + * Fix remember-me functionality + * Check page exists so as not to fail hard + * Fix for static Inflector references #17 + + # v1.0.1 ## 11/23/2015 diff --git a/LICENSE b/LICENSE index 0e788c6..4bb7092 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Grav +Copyright (c) 2016 Grav Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index fec5656..a3c7ed3 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,15 @@ These are available via GPM, and because the plugin has dependencies you just ne $ bin/gpm install login ``` +# Changes in version 2.0 (STILL TO BE RELEASED) + +The Login Plugin 2.0 has the following changes compared to 1.0: + +- OAuth has been separated to its own plugin, needs to be installed separately and configured. The users account filename format has changed too, to fix an issue that involved people with the same name on a service. +- The `redirect` option has been changed to `redirect_after_login`. +- The Remember Me session minimum length is now 1 week. +- Removed the option to login from oauth without creating the corresponding user file under `user/accounts/`. + # Creating Users You can either use the built-in CLI capabilities, or you create a user manually by creating a new YAML file in your `user/acounts` folder. @@ -332,60 +341,6 @@ You can set the "Redirect after registration" option in the Login plugin, or as ``` -# OAuth - -You can add OAuth providers to the login plugin as another method to have users on your site. To enable OAuth change `oauth.enabled` to `true` in `login.yaml`. By default OAuth allows users to login though they do not create an account file for the user. If you want an account file created (ex: for tracking purposes) change `oauth.user.autocreate` to `true` in `login.yaml`. ->Note: OAuth has not been tested with Grav's multilang feature! Due to this, certain OAuth providers may not function properly on multilang sites - ->IMPORTANT: `localhost` may NOT be used for callback and allowed URLs when creating OAuth provider applications due to certificate verification issues. Some services allow other URLs and it may be possible to add custom domains pointing to 127.0.0.1 in your hosts file and point applications there. - -## Facebook - -Visit https://developers.facebook.com/quickstarts/?platform=web and create an app name then click **Create New Facebook App ID.** - -Choose a category most similar to your business then click **Create App ID.** - -Scroll down on the next screen to the section titled **Tell us about your website.** Input a URL for the site (no need to include the protocol). Click **Next** - -Click **Skip Quick Start** Copy the **App ID** and **App Secret** into `login.yaml` - -On the left hand side click **Settings** -In the **Basic** tab add your domain into the **App Domains** section as well as enter a contact email (required for facebook developer program). In the **Advanced** tab scroll down to the **Client OAuth Settings** Make sure that **Client OAuth Login** is enabled as well as **Web OAuth Login** is enabled. In the **Valid OAuth redirect URIs** section add the routes of all pages that are protected by login. This includes the domains. EX: `http://getgrav.org/`, `http://getgrav.org/login`, `http://getgrav.org/en/login`, and `http://getgrav.org/protected/page/route` - - -## Github - -Visit Github's [Developer Applications Console](https://github.com/settings/developers) and press button **Register new application** (login if necesarry). ![](assets/github/github.png) - -Fill out the name and the URL (can be anything) and fill in the **callback**, which must be equal to where your grav site is located, generally just the host, i.e. `http://getgrav.org`. ![](assets/github/github_2.png) - -Copy **Client ID** and **client secret** into login.yaml under Github. ![](assets/github/github_3.png)Be sure to change `Github.enabled` to `true` - -## Google - -Visit the [Google Developers Console](https://console.developers.google.com) (sign in with a google account, preferably your businesses gmail). - -Select **Create Project** and give the project a name (can be anything). Click **Create**. ![](assets/google/google.png) - -When it's finished creating in the left hand menu choose **Credentials** under **APIs & Auth** (you may need to click **APIs & Auth** in order to display **Credntials**). ![](assets/google/google_3.png) - -Under **Add credentials** (center of screen) select **OAuth 2.0 client ID**.![](assets/google/google_4.png) - -Then select **Configure consent screen** in the top right corner. ![](assets/google/google_5.png) - -The only requirement is **Product name** which should be the name of your website/business (not a url). You may fill in the other options as you want on the consent screen. (The consent screen can also be changed later). ![](assets/google/google_6.png) - -Then once you save the consent screen select **Web application** from the radio buttons and fill in the fields. **Name** being name of product/business. **Authorized Javascript origins** is the root domain name of the login page (no routes or wildcards) such as `http://getgrav.org`. - -If needed, enter multiple sub domains, creating an entry for each. **Authorized redirect URIs** include the **same** Authorized Javascript origins used along with the **route** to the login page such as `http://getgrav.org/login`. Click **create**. - -![](assets/google/google_7.png) - -Copy **Client ID** and **client secret** into login.yaml under Google. ![](assets/google/google_8.png)Be sure to change `Google.enabled` to `true` - -## Twitter - -Login if necessary. Create a [new Twitter App](https://apps.twitter.com/app/new) , fill out name, application website, choose "Browser" as application type, choose the callback URL like above, default access type can be set to read-only, click on "Register application" and then you should be directed to your new application with the Client ID and secret ready to be copied and pasted into the YAML file. # Known issues diff --git a/assets/github/github.png b/assets/github/github.png deleted file mode 100644 index a8595d8..0000000 Binary files a/assets/github/github.png and /dev/null differ diff --git a/assets/github/github_2.png b/assets/github/github_2.png deleted file mode 100644 index ff84e98..0000000 Binary files a/assets/github/github_2.png and /dev/null differ diff --git a/assets/github/github_3.png b/assets/github/github_3.png deleted file mode 100644 index ee9fd76..0000000 Binary files a/assets/github/github_3.png and /dev/null differ diff --git a/assets/google/google.png b/assets/google/google.png deleted file mode 100644 index 189c490..0000000 Binary files a/assets/google/google.png and /dev/null differ diff --git a/assets/google/google_2.png b/assets/google/google_2.png deleted file mode 100644 index 0d2b116..0000000 Binary files a/assets/google/google_2.png and /dev/null differ diff --git a/assets/google/google_3.png b/assets/google/google_3.png deleted file mode 100644 index 822ffab..0000000 Binary files a/assets/google/google_3.png and /dev/null differ diff --git a/assets/google/google_4.png b/assets/google/google_4.png deleted file mode 100644 index bba7305..0000000 Binary files a/assets/google/google_4.png and /dev/null differ diff --git a/assets/google/google_5.png b/assets/google/google_5.png deleted file mode 100644 index 966e45d..0000000 Binary files a/assets/google/google_5.png and /dev/null differ diff --git a/assets/google/google_6.png b/assets/google/google_6.png deleted file mode 100644 index 694bb33..0000000 Binary files a/assets/google/google_6.png and /dev/null differ diff --git a/assets/google/google_7.png b/assets/google/google_7.png deleted file mode 100644 index 5a164c5..0000000 Binary files a/assets/google/google_7.png and /dev/null differ diff --git a/assets/google/google_8.png b/assets/google/google_8.png deleted file mode 100644 index 8fffa27..0000000 Binary files a/assets/google/google_8.png and /dev/null differ diff --git a/blueprints.yaml b/blueprints.yaml index 4ccda16..fe5f9a0 100644 --- a/blueprints.yaml +++ b/blueprints.yaml @@ -1,5 +1,5 @@ name: Login -version: 1.3.1 +version: 2.0.0 description: Enables user authentication and login screen. icon: sign-in author: @@ -23,6 +23,7 @@ form: tabs: type: tabs active: 1 + class: subtle fields: login: @@ -33,7 +34,7 @@ form: enabled: type: hidden - label: PLUGIN_ADMIN.PLUGIN_STATUS + label: PLUGIN_LOGIN.PLUGIN_STATUS highlight: 1 default: 1 options: @@ -61,7 +62,7 @@ form: help: PLUGIN_LOGIN.ROUTE_HELP placeholder: "/my-custom-login" - redirect: + redirect_after_login: type: text label: PLUGIN_LOGIN.REDIRECT_AFTER_LOGIN help: PLUGIN_LOGIN.REDIRECT_AFTER_LOGIN_HELP @@ -69,10 +70,22 @@ form: parent_acl: type: toggle - label: Use parent access rules + label: PLUGIN_LOGIN.USE_PARENT_ACL_LABEL highlight: 1 default: 0 - help: "Check for parent access rules if no rules are defined" + help: PLUGIN_LOGIN.USE_PARENT_ACL_HELP + options: + 1: PLUGIN_ADMIN.ENABLED + 0: PLUGIN_ADMIN.DISABLED + validate: + type: bool + + protect_protected_page_media: + type: toggle + label: PLUGIN_LOGIN.PROTECT_PROTECTED_PAGE_MEDIA_LABEL + highlight: 1 + default: 0 + help: PLUGIN_LOGIN.PROTECT_PROTECTED_PAGE_MEDIA_HELP options: 1: PLUGIN_ADMIN.ENABLED 0: PLUGIN_ADMIN.DISABLED @@ -81,7 +94,7 @@ form: rememberme: type: section - title: PLUGIN_LOGIN.SESSION + title: PLUGIN_LOGIN.REMEMBER_ME fields: rememberme.enabled: @@ -98,8 +111,9 @@ form: rememberme.timeout: type: text size: small + default: 604800 label: PLUGIN_ADMIN.TIMEOUT - help: PLUGIN_ADMIN.TIMEOUT_HELP + help: PLUGIN_LOGIN.TIMEOUT_HELP validate: type: number min: 1 @@ -137,7 +151,13 @@ form: type: text label: PLUGIN_LOGIN.REDIRECT_AFTER_REGISTRATION help: PLUGIN_LOGIN.REDIRECT_AFTER_REGISTRATION_HELP - placeholder: "/my-page" + placeholder: "/page-to-show-after-registration" + + user_registration.redirect_after_activation: + type: text + label: PLUGIN_LOGIN.REDIRECT_AFTER_ACTIVATION + help: PLUGIN_LOGIN.REDIRECT_AFTER_ACTIVATION_HELP + placeholder: "/page-to-show-after-activation" registration_fields: type: section @@ -159,6 +179,30 @@ form: placeholder_key: PLUGIN_LOGIN.ADDITIONAL_PARAM_KEY placeholder_value: PLUGIN_LOGIN.ADDITIONAL_PARAM_VALUE + access_levels: + title: PLUGIN_ADMIN.ACCESS_LEVELS + type: section + security: admin.super + + fields: + user_registration.groups: + type: selectize + size: large + label: PLUGIN_ADMIN.GROUPS + '@data-options': '\Grav\User\Groups::groups' + classes: fancy + help: PLUGIN_LOGIN.GROUPS_HELP + validate: + type: commalist + + user_registration.access.site: + type: array + label: PLUGIN_ADMIN.SITE_ACCESS + help: PLUGIN_LOGIN.SITE_ACCESS_HELP + multiple: false + validate: + type: array + options: type: section title: PLUGIN_LOGIN.OPTIONS @@ -229,155 +273,3 @@ form: 1: PLUGIN_ADMIN.YES 0: PLUGIN_ADMIN.NO validate: - type: bool - - oauth: - type: tab - title: PLUGIN_LOGIN.OAUTH_SECTION - - fields: - oauth.enabled: - type: toggle - label: PLUGIN_LOGIN.OAUTH_ENABLE - highlight: 1 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - oauth.user.autocreate: - type: toggle - label: PLUGIN_LOGIN.OAUTH_USER_AUTOCREATE - highlight: 1 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - oauth.user.access: - type: array - label: PLUGIN_LOGIN.OAUTH_ACCESS - placeholder_key: signin.login - placeholder_value: true - - oauth.providers: - type: section - title: PLUGIN_LOGIN.OAUTH_PROVIDER_SECTION - underline: true - - fields: - oauth.providers.Facebook: - type: section - title: PLUGIN_LOGIN.FACEBOOK - - fields: - oauth.providers.Facebook.enabled: - type: toggle - label: PLUGIN_LOGIN.OAUTH_PROVIDER_FACEBOOK - highlight: 1 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - oauth.providers.Facebook.credentials.key: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_ID - validate: - type: string - - oauth.providers.Facebook.credentials.secret: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_SECRET - validate: - type: string - - oauth.providers.Google: - type: section - title: PLUGIN_LOGIN.GOOGLE - - fields: - oauth.providers.Google.enabled: - type: toggle - label: PLUGIN_LOGIN.OAUTH_PROVIDER_GOOGLE - highlight: 1 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - oauth.providers.Google.credentials.key: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_ID - validate: - type: string - - oauth.providers.Google.credentials.secret: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_SECRET - validate: - type: string - - oauth.providers.GitHub: - type: section - title: PLUGIN_LOGIN.GITHUB - - fields: - oauth.providers.GitHub.enabled: - type: toggle - label: PLUGIN_LOGIN.OAUTH_PROVIDER_GITHUB - highlight: 1 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - oauth.providers.GitHub.credentials.key: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_ID - validate: - type: string - - oauth.providers.GitHub.credentials.secret: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_SECRET - validate: - type: string - - oauth.providers.Twitter: - type: section - title: PLUGIN_LOGIN.TWITTER - - fields: - oauth.providers.Twitter.enabled: - type: toggle - label: PLUGIN_LOGIN.OAUTH_PROVIDER_TWITTER - highlight: 1 - default: 0 - options: - 1: PLUGIN_ADMIN.ENABLED - 0: PLUGIN_ADMIN.DISABLED - validate: - type: bool - - oauth.providers.Twitter.credentials.key: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_ID - validate: - type: string - - oauth.providers.Twitter.credentials.secret: - type: text - label: PLUGIN_LOGIN.OAUTH_CLIENT_SECRET - validate: - type: string diff --git a/classes/Controller.php b/classes/Controller.php index a626893..dbca281 100644 --- a/classes/Controller.php +++ b/classes/Controller.php @@ -2,13 +2,23 @@ namespace Grav\Plugin\Login; +use Grav\Common\Config\Config; use Grav\Common\Grav; -use Grav\Common\Utils; use Grav\Plugin\Login\RememberMe; +use Grav\Plugin\Login\Login; +use Grav\Common\Language\Language; +use Grav\Common\User\User; +use Grav\Common\Utils; +use Grav\Plugin\Login\Utils as LoginUtils; use Birke\Rememberme\Cookie; +use RocketTheme\Toolbox\Session\Message; -class Controller implements ControllerInterface +/** + * Class Controller + * @package Grav\Plugin\Login + */ +class Controller { /** * @var \Grav\Common\Grav @@ -38,13 +48,18 @@ class Controller implements ControllerInterface /** * @var string */ - protected $prefix = 'do'; + protected $prefix = 'task'; /** * @var \Birke\Rememberme\Authenticator */ protected $rememberMe; + /** + * @var Login + */ + protected $login; + /** * @param Grav $grav * @param string $action @@ -54,6 +69,7 @@ public function __construct(Grav $grav, $action, $post = null) { $this->grav = $grav; $this->action = $action; + $this->login = isset($this->grav['login']) ? $this->grav['login'] : ''; $this->post = $this->getPost($post); $this->rememberMe(); @@ -78,7 +94,7 @@ public function execute() } try { - $success = call_user_func(array($this, $method)); + $success = call_user_func([$this, $method]); } catch (\RuntimeException $e) { $this->setMessage($e->getMessage(), 'error'); } @@ -90,6 +106,245 @@ public function execute() return $success; } + /** + * Handle login. + * + * @return bool True if the action was performed. + */ + public function taskLogin() + { + /** @var Language $t */ + $t = $this->grav['language']; + if ($this->authenticate($this->post)) { + $this->login->setMessage($t->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL')); + + $redirect = $this->grav['config']->get('plugins.login.redirect_after_login'); + if (!$redirect) { + $redirect = $this->grav['uri']->referrer('/'); + } + $this->setRedirect($redirect); + } else { + $user = $this->grav['user']; + if ($user->username) { + $this->setMessage($t->translate('PLUGIN_LOGIN.ACCESS_DENIED'), 'error'); + } else { + $this->setMessage($t->translate('PLUGIN_LOGIN.LOGIN_FAILED'), 'error'); + } + } + + return true; + } + + /** + * Handle logout. + * + * @return bool True if the action was performed. + */ + public function taskLogout() + { + /** @var User $user */ + $user = $this->grav['user']; + + if (!$this->rememberMe->login()) { + $credentials = $user->get('username'); + $this->rememberMe->getStorage()->cleanAllTriplets($credentials); + } + $this->rememberMe->clearCookie(); + + $this->grav['session']->invalidate()->start(); + $this->setRedirect('/'); + + return true; + } + + /** + * Handle the email password recovery procedure. + * + * @return bool True if the action was performed. + */ + protected function taskForgot() + { + $param_sep = $this->grav['config']->get('system.param_sep', ':'); + $data = $this->post; + + $username = isset($data['username']) ? $data['username'] : ''; + $user = !empty($username) ? User::load($username) : null; + + /** @var Language $l */ + $language = $this->grav['language']; + $messages = $this->grav['messages']; + + if (!isset($this->grav['Email'])) { + $messages->add($language->translate('PLUGIN_LOGIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error'); + $this->setRedirect('/'); + + return true; + } + + if (!$user || !$user->exists()) { + $messages->add($language->translate(['PLUGIN_LOGIN.FORGOT_USERNAME_DOES_NOT_EXIST', $username]), 'error'); + $this->setRedirect($this->grav['config']->get('plugins.login.route_forgot')); + + return true; + } + + if (empty($user->email)) { + $messages->add($language->translate(['PLUGIN_LOGIN.FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL', $username]), + 'error'); + $this->setRedirect($this->grav['config']->get('plugins.login.route_forgot')); + + return true; + } + + $token = md5(uniqid(mt_rand(), true)); + $expire = time() + 604800; // next week + + $user->reset = $token . '::' . $expire; + $user->save(); + + $author = $this->grav['config']->get('site.author.name', ''); + $fullname = $user->fullname ?: $username; + + $reset_link = $this->grav['base_url_absolute'] . $this->grav['config']->get('plugins.login.route_reset') . '/task:login.reset/token' . $param_sep . $token . '/user' . $param_sep . $username . '/nonce' . $param_sep . Utils::getNonce('reset-form'); + + $sitename = $this->grav['config']->get('site.title', 'Website'); + $from = $this->grav['config']->get('plugins.email.from'); + + if (empty($from)) { + $messages->add($language->translate('PLUGIN_LOGIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error'); + $this->setRedirect($this->grav['config']->get('plugins.login.route_forgot')); + + return true; + } + + $to = $user->email; + + $subject = $language->translate(['PLUGIN_LOGIN.FORGOT_EMAIL_SUBJECT', $sitename]); + $content = $language->translate(['PLUGIN_LOGIN.FORGOT_EMAIL_BODY', $fullname, $reset_link, $author, $sitename]); + + $sent = LoginUtils::sendEmail($subject, $content, $to); + + if ($sent < 1) { + $messages->add($language->translate('PLUGIN_LOGIN.FORGOT_FAILED_TO_EMAIL'), 'error'); + } else { + $messages->add($language->translate(['PLUGIN_LOGIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL', $to]), 'info'); + } + + $this->setRedirect('/'); + + return true; + } + + /** + * Handle the reset password action. + * + * @return bool True if the action was performed. + */ + public function taskReset() + { + $data = $this->post; + $language = $this->grav['language']; + $messages = $this->grav['messages']; + + if (isset($data['password'])) { + $username = isset($data['username']) ? $data['username'] : null; + $user = !empty($username) ? User::load($username) : null; + $password = isset($data['password']) ? $data['password'] : null; + $token = isset($data['token']) ? $data['token'] : null; + + if (!empty($user) && $user->exists() && !empty($user->reset)) { + list($good_token, $expire) = explode('::', $user->reset); + + if ($good_token === $token) { + if (time() > $expire) { + $messages->add($language->translate('PLUGIN_LOGIN.RESET_LINK_EXPIRED'), 'error'); + $this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot')); + + return true; + } + + unset($user->hashed_password); + unset($user->reset); + $user->password = $password; + + $user->validate(); + $user->filter(); + $user->save(); + + $messages->add($language->translate('PLUGIN_LOGIN.RESET_PASSWORD_RESET'), 'info'); + $this->grav->redirect('/'); + + return true; + } + } + + $messages->add($language->translate('PLUGIN_LOGIN.RESET_INVALID_LINK'), 'error'); + $this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot')); + + return true; + + } else { + $user = $this->grav['uri']->param('user'); + $token = $this->grav['uri']->param('token'); + + if (empty($user) || empty($token)) { + $messages->add($language->translate('PLUGIN_LOGIN.RESET_INVALID_LINK'), 'error'); + $this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot')); + + return true; + } + } + + return true; + } + + /** + * Authenticate user. + * + * @param array $form Form fields. + * + * @return bool + */ + protected function authenticate($form) + { + /** @var User $user */ + $user = $this->grav['user']; + + if (!$user->authenticated) { + $username = isset($form['username']) ? $form['username'] : $this->rememberMe->login(); + + // Normal login process + $user = User::load($username); + if ($user->exists()) { + if (!empty($form['username']) && !empty($form['password'])) { + // Authenticate user + $user->authenticated = $user->authenticate($form['password']); + + if ($user->authenticated) { + $this->grav['session']->user = $user; + + unset($this->grav['user']); + $this->grav['user'] = $user; + + // If the user wants to be remembered, create Rememberme cookie + if (!empty($form['rememberme'])) { + $this->rememberMe->createCookie($form['username']); + } else { + $this->rememberMe->clearCookie(); + $this->rememberMe->getStorage()->cleanAllTriplets($user->get('username')); + } + } + } + } + } + + // Authorize against user ACL + $user_authorized = $user->authorize('site.login'); + $user->authenticated = ($user->authenticated && $user_authorized); + + return $user->authenticated; + } + /** * Redirects an action */ @@ -97,21 +352,19 @@ public function redirect() { if ($this->redirect) { $this->grav->redirect($this->redirect, $this->redirectCode); - } else if ($redirect = $this->grav['config']->get('plugins.login.redirect')) { - $this->grav->redirect($redirect, $this->redirectCode); } } /** * Set redirect. * - * @param $path + * @param $path * @param int $code */ public function setRedirect($path, $code = 303) { $this->redirect = $path; - $this->code = $code; + $this->redirectCode = $code; } /** @@ -130,15 +383,16 @@ public function setMessage($msg, $type = 'info') /** * Gets and sets the RememberMe class * - * @param mixed $var A rememberMe instance to set + * @param mixed $var A rememberMe instance to set * - * @return Authenticator Returns the current rememberMe instance + * @return RememberMe\RememberMe Returns the current rememberMe instance */ public function rememberMe($var = null) { if ($var !== null) { $this->rememberMe = $var; } + if (!$this->rememberMe) { /** @var Config $config */ $config = $this->grav['config']; @@ -151,7 +405,8 @@ public function rememberMe($var = null) // Hardening cookies with user-agent and random salt or // fallback to use system based cache key - $data = $_SERVER['HTTP_USER_AGENT'] . $config->get('security.salt', $this->grav['cache']->getKey()); + $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 @@ -167,6 +422,7 @@ public function rememberMe($var = null) * Prepare and return POST data. * * @param array $post + * * @return array */ protected function &getPost($post) @@ -178,6 +434,7 @@ protected function &getPost($post) $post = array_merge_recursive($post, $this->jsonDecode($post['_json'])); unset($post['_json']); } + return $post; } @@ -185,6 +442,7 @@ protected function &getPost($post) * Recursively JSON decode data. * * @param array $data + * * @return array */ protected function jsonDecode(array $data) @@ -196,6 +454,7 @@ protected function jsonDecode(array $data) $value = json_decode($value, true); } } + return $data; } } diff --git a/classes/ControllerInterface.php b/classes/ControllerInterface.php deleted file mode 100644 index a80e14d..0000000 --- a/classes/ControllerInterface.php +++ /dev/null @@ -1,16 +0,0 @@ -grav = $grav; + $this->config = $this->grav['config']; + //$this->route = $route; + $this->session = $this->grav['session']; + $this->user = $this->grav['user']; + + $this->uri = $this->grav['uri']; + } + + /** + * Add message into the session queue. + * + * @param string $msg + * @param string $type + */ + public function setMessage($msg, $type = 'info') + { + /** @var Message $messages */ + $messages = $this->grav['messages']; + $messages->add($msg, $type); + } + + /** + * Fetch and delete messages from the session queue. + * + * @param string $type + * + * @return array + */ + public function messages($type = null) + { + /** @var Message $messages */ + $messages = $this->grav['messages']; + + return $messages->fetch($type); + } + + /** + * Authenticate user. + * + * @param array $form Form fields. + * + * @return bool + */ + public function authenticate($form) + { + if (!$this->user->authenticated && isset($form['username']) && isset($form['password'])) { + $user = User::load($form['username']); + + //default to english if language not set + if (empty($user->language)) { + $user->set('language', 'en'); + } + + if ($user->exists()) { + $user->authenticated = true; + + // Authenticate user. + $result = $user->authenticate($form['password']); + + if ($result) { + $this->user = $this->session->user = $user; + + /** @var Grav $grav */ + $grav = $this->grav; + + $this->setMessage($this->grav['language']->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL', + [$this->user->language]), 'info'); + + $redirect_route = $this->uri->route(); + $grav->redirect($redirect_route); + } + } + } + + return $this->authorize(); + } + + /** + * Checks user authorisation to the action. + * + * @param string $action + * + * @return bool + */ + public function authorize($action = 'admin.login') + { + $action = (array)$action; + + foreach ($action as $a) { + if ($this->user->authorize($a)) { + return true; + } + } + + return false; + } + + /** + * Create a new user file + * + * @param array $data + * + * @return User + */ + public function register($data) + { + //Add new user ACL settings + $groups = $this->config->get('plugins.login.user_registration.groups', []); + + if (count($groups) > 0) { + $data['groups'] = $groups; + } + + $access = $this->config->get('plugins.login.user_registration.access.site', []); + if (count($access) > 0) { + $data['access']['site'] = $access; + } + + $username = $data['username']; + $file = CompiledYamlFile::instance($this->grav['locator']->findResource('account://' . $username . YAML_EXT, + true, true)); + + // Create user object and save it + $user = new User($data); + $user->file($file); + $user->save(); + + if (isset($data['state']) && $data['state'] == 'enabled' && $this->config->get('plugins.login.user_registration.options.login_after_registration', false)) { + //Login user + $this->grav['session']->user = $user; + unset($this->grav['user']); + $this->grav['user'] = $user; + $user->authenticated = $user->authorize('site.login'); + } + + return $user; + } + + + /** + * Handle the email to notificate the user account creation to the site admin. + * + * @param $user + * + * @return bool True if the action was performed. + */ + public function sendNotificationEmail($user) + { + if (empty($user->email)) { + throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD')); + } + + $sitename = $this->grav['config']->get('site.title', 'Website'); + + $subject = $this->grav['language']->translate(['PLUGIN_LOGIN.NOTIFICATION_EMAIL_SUBJECT', $sitename]); + $content = $this->grav['language']->translate([ + 'PLUGIN_LOGIN.NOTIFICATION_EMAIL_BODY', + $sitename, + $user->username, + $user->email + ]); + $to = $this->grav['config']->get('plugins.email.from'); + + if (empty($to)) { + throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.EMAIL_NOT_CONFIGURED')); + } + + $sent = LoginUtils::sendEmail($subject, $content, $to); + + if ($sent < 1) { + throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE')); + } + + return true; + } + + /** + * Handle the email to welcome the new user + * + * @param $user + * + * @return bool True if the action was performed. + */ + public function sendWelcomeEmail($user) + { + if (empty($user->email)) { + throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.USER_NEEDS_EMAIL_FIELD')); + } + + $sitename = $this->grav['config']->get('site.title', 'Website'); + + $subject = $this->grav['language']->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_SUBJECT', $sitename]); + $content = $this->grav['language']->translate(['PLUGIN_LOGIN.WELCOME_EMAIL_BODY', $user->username, $sitename]); + $to = $user->email; + + $sent = LoginUtils::sendEmail($subject, $content, $to); + + if ($sent < 1) { + throw new \RuntimeException($this->grav['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. + */ + public function sendActivationEmail($user) + { + if (empty($user->email)) { + throw new \RuntimeException($this->grav['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->grav['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 . '/nonce' . $param_sep . Utils::getNonce('user-activation'); + + $site_name = $this->grav['config']->get('site.title', 'Website'); + + $subject = $this->grav['language']->translate(['PLUGIN_LOGIN.ACTIVATION_EMAIL_SUBJECT', $site_name]); + $content = $this->grav['language']->translate([ + 'PLUGIN_LOGIN.ACTIVATION_EMAIL_BODY', + $user->username, + $activation_link, + $site_name + ]); + $to = $user->email; + + $sent = LoginUtils::sendEmail($subject, $content, $to); + + if ($sent < 1) { + throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.EMAIL_SENDING_FAILURE')); + } + + return true; + } +} diff --git a/classes/LoginController.php b/classes/LoginController.php deleted file mode 100644 index a0ff8b2..0000000 --- a/classes/LoginController.php +++ /dev/null @@ -1,242 +0,0 @@ -grav['language']; - if ($this->authenticate($this->post)) { - $this->setMessage($t->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL')); - $referrer = $this->grav['uri']->referrer('/'); - $this->setRedirect($referrer); - } else { - $user = $this->grav['user']; - if ($user->username) { - $this->setMessage($t->translate('PLUGIN_LOGIN.ACCESS_DENIED'), 'error'); - } else { - $this->setMessage($t->translate('PLUGIN_LOGIN.LOGIN_FAILED'), 'error'); - } - } - - return true; - } - - /** - * Handle logout. - * - * @return bool True if the action was performed. - */ - public function taskLogout() - { - /** @var User $user */ - $user = $this->grav['user']; - - if (!$this->rememberMe->login()) { - $credentials = $user->get('username'); - $this->rememberMe->getStorage()->cleanAllTriplets($credentials); - } - $this->rememberMe->clearCookie(); - - $this->grav['session']->invalidate()->start(); - $this->setRedirect('/'); - - return true; - } - - /** - * Handle the email password recovery procedure. - * - * @return bool True if the action was performed. - */ - protected function taskForgot() - { - $param_sep = $this->grav['config']->get('system.param_sep', ':'); - $data = $this->post; - - $username = isset($data['username']) ? $data['username'] : ''; - $user = !empty($username) ? User::load($username) : null; - - /** @var Language $l */ - $language = $this->grav['language']; - $messages = $this->grav['messages']; - - if (!isset($this->grav['Email'])) { - $messages->add($language->translate('PLUGIN_ADMIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error'); - $this->setRedirect('/'); - return true; - } - - if (!$user || !$user->exists()) { - $messages->add($language->translate(['PLUGIN_ADMIN.FORGOT_USERNAME_DOES_NOT_EXIST', $username]), 'error'); - $this->setRedirect('/forgot'); - return true; - } - - if (empty($user->email)) { - $messages->add($language->translate(['PLUGIN_ADMIN.FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL', $username]), 'error'); - $this->setRedirect('/forgot'); - return true; - } - - $token = md5(uniqid(mt_rand(), true)); - $expire = time() + 604800; // next week - - $user->reset = $token . '::' . $expire; - $user->save(); - - $author = $this->grav['config']->get('site.author.name', ''); - $fullname = $user->fullname ?: $username; - - $reset_link = $this->grav['base_url_absolute'] . $this->grav['config']->get('plugins.login.route_reset') . '/task:login.reset/token' . $param_sep . $token . '/user' . $param_sep . $username . '/nonce' . $param_sep . Utils::getNonce('reset-form'); - - $sitename = $this->grav['config']->get('site.title', 'Website'); - $from = $this->grav['config']->get('plugins.email.from'); - - if (empty($from)) { - $messages->add($language->translate('PLUGIN_ADMIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error'); - $this->setRedirect('/forgot'); - return true; - } - - $to = $user->email; - - $subject = $language->translate(['PLUGIN_ADMIN.FORGOT_EMAIL_SUBJECT', $sitename]); - $content = $language->translate(['PLUGIN_ADMIN.FORGOT_EMAIL_BODY', $fullname, $reset_link, $author, $sitename]); - - $sent = LoginUtils::sendEmail($subject, $content, $to); - - if ($sent < 1) { - $messages->add($language->translate('PLUGIN_ADMIN.FORGOT_FAILED_TO_EMAIL'), 'error'); - } else { - $messages->add($language->translate(['PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL', $to]), 'info'); - } - - $this->setRedirect('/'); - return true; - } - - /** - * Handle the reset password action. - * - * @return bool True if the action was performed. - */ - public function taskReset() - { - $data = $this->post; - $language = $this->grav['language']; - $messages = $this->grav['messages']; - - if (isset($data['password'])) { - $username = isset($data['username']) ? $data['username'] : null; - $user = !empty($username) ? User::load($username) : null; - $password = isset($data['password']) ? $data['password'] : null; - $token = isset($data['token']) ? $data['token'] : null; - - if (!empty($user) && $user->exists() && !empty($user->reset)) { - list($good_token, $expire) = explode('::', $user->reset); - - if ($good_token === $token) { - if (time() > $expire) { - $messages->add($language->translate('PLUGIN_ADMIN.RESET_LINK_EXPIRED'), 'error'); - $this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot')); - return true; - } - - unset($user->hashed_password); - unset($user->reset); - $user->password = $password; - - $user->validate(); - $user->filter(); - $user->save(); - - $messages->add($language->translate('PLUGIN_ADMIN.RESET_PASSWORD_RESET'), 'info'); - $this->grav->redirect('/'); - return true; - } - } - - $messages->add($language->translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error'); - $this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot')); - return true; - - } else { - $user = $this->grav['uri']->param('user'); - $token = $this->grav['uri']->param('token'); - - if (empty($user) || empty($token)) { - $messages->add($language->translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error'); - $this->grav->redirect($this->grav['config']->get('plugins.login.route_forgot')); - return true; - } - } - - return true; - } - - /** - * Authenticate user. - * - * @param array $form Form fields. - * @return bool - */ - protected function authenticate($form) - { - /** @var User $user */ - $user = $this->grav['user']; - - if (!$user->authenticated) { - $username = isset($form['username']) ? $form['username'] : $this->rememberMe->login(); - - // Normal login process - $user = User::load($username); - if ($user->exists()) { - if (!empty($form['username']) && !empty($form['password'])) { - // Authenticate user. - $user->authenticated = $user->authenticate($form['password']); - - if ($user->authenticated) { - $this->grav['session']->user = $user; - - unset($this->grav['user']); - $this->grav['user'] = $user; - - // If the user wants to be remembered, create - // Rememberme cookie - if (!empty($form['rememberme'])) { - $this->rememberMe->createCookie($form['username']); - } else { - $this->rememberMe->clearCookie(); - $this->rememberMe->getStorage()->cleanAllTriplets($user->get('username')); - } - } - } - } - } - - // Authorize against user ACL - $user_authorized = $user->authorize('site.login'); - $user->authenticated = ($user->authenticated && $user_authorized); - - return $user->authenticated; - } -} diff --git a/classes/OAuthLoginController.php b/classes/OAuthLoginController.php deleted file mode 100644 index ec13795..0000000 --- a/classes/OAuthLoginController.php +++ /dev/null @@ -1,433 +0,0 @@ - - */ -class OAuthLoginController extends Controller -{ - /** - * @var string - */ - public $provider; - - /** - * @var \OAuth\Common\Storage\Session - */ - protected $storage; - - /** - * @var \OAuth\ServiceFactory - */ - protected $factory; - - /** - * @var \OAuth\Common\Service\AbstractService - */ - protected $service; - - /** - * @var string - */ - protected $prefix = 'oauth'; - - /** - * @var array - */ - protected $scopes = [ - 'github' => ['user'], - 'google' => ['userinfo_email', 'userinfo_profile'], - 'facebook' => ['public_profile'] - ]; - - /** - * Constructor. - * - * @param Grav $grav Grav instance - * @param string $action The name of the action - * @param array $post An array of values passed to the action - */ - public function __construct(Grav $grav, $action, $post = null) - { - parent::__construct($grav, ucfirst($action), $post); - - // Session storage - $this->storage = new Session(false, 'oauth_token', 'oauth_state'); - - /** @var $serviceFactory \OAuth\ServiceFactory */ - $this->factory = new ServiceFactory(); - - // Use curl client instead of fopen stream - if (extension_loaded('curl')) { - $this->factory->setHttpClient(new CurlCLient()); - } - } - - /** - * Performs an OAuth authentication - */ - public function execute() - { - /** @var \Grav\Common\Language\Language */ - $t = $this->grav['language']; - - $provider = strtolower($this->action); - $config = $this->grav['config']->get('plugins.login.oauth.providers.' . $this->action, []); - - if (isset($config['credentials'])) { - // Setup the credentials for the requests - $credentials = new Credentials( - $config['credentials']['key'], $config['credentials']['secret'], $this->grav['uri']->url(true) - ); - - // Instantiate service using the credentials, http client - // and storage mechanism for the token - $scope = isset($this->scopes[$provider]) ? $this->scopes[$provider] : []; - $this->service = $this->factory->createService($this->action, $credentials, $this->storage, $scope); - } - - if (!$this->service || empty($config)) { - $this->setMessage($t->translate(['PLUGIN_LOGIN.OAUTH_PROVIDER_NOT_SUPPORTED', $this->action]), 'error'); - return true; - } - - // Check OAuth authentication status - $authenticated = parent::execute(); - if (is_bool($authenticated)) { - $this->reset(); - if ($authenticated) { - $this->setMessage($t->translate('PLUGIN_LOGIN.LOGIN_SUCCESSFUL')); - } else { - $this->setMessage($t->translate('PLUGIN_LOGIN.ACCESS_DENIED'), 'error'); - } - - // Redirect to current URI - $referrer = $this->grav['uri']->url(true); - $this->setRedirect($referrer); - } elseif (!$this->grav['session']->oauth) { - $this->setMessage($t->translate(['PLUGIN_LOGIN.OAUTH_PROVIDER_NOT_SUPPORTED', $this->action]), 'error'); - } - - return true; - } - - /** - * Reset state of OAuth authentication. - */ - public function reset() { - /** @var Grav\Common\Session */ - $session = $this->grav['session']; - - unset($session->oauth); - $this->storage->clearAllTokens(); - $this->storage->clearAllAuthorizationStates(); - } - - /** - * Implements a generic OAuth service provider authentication - * - * @param callable $callback A callable to call when OAuth authentication - * starts - * @param string $oauth OAuth version to be used for authentication - * - * @return null|User Returns a Grav user instance on success. - */ - protected function genericOAuthProvider($callback, $oauth = 'oauth2') - { - /** @var Grav\Common\Session */ - $session = $this->grav['session']; - - switch ($oauth) { - case 'oauth1': - if (empty($_GET['oauth_token']) && empty($_GET['oauth_verifier'])) { - // Extra request needed for OAuth1 to request a request token :-) - $token = $this->service->requestRequestToken(); - - // Create a state token to prevent request forgery. - // Store it in the session for later validation. - $redirect = $this->service->getAuthorizationUri([ - 'oauth_token' => $token->getRequestToken() - ]); - $this->setRedirect($redirect); - - // Update OAuth session - $session->oauth = $this->action; - } else { - $token = $this->storage->retrieveAccessToken($session->oauth); - - // This was a callback request from OAuth1 service, get the token - $this->service->requestAccessToken( - $_GET['oauth_token'], - $_GET['oauth_verifier'], - $token->getRequestTokenSecret() - ); - - return $callback(); - } - break; - - case 'oauth2': - default: - if (empty($_GET['code'])) { - // Create a state token to prevent request forgery (CSRF). - $state = sha1($this->getRandomBytes(1024, false)); - $redirect = $this->service->getAuthorizationUri([ - 'state' => $state - ]); - $this->setRedirect($redirect); - - // Update OAuth session - $session->oauth = $this->action; - - // Store CSRF in the session for later validation. - $this->storage->storeAuthorizationState($this->action, $state); - } else { - // Retrieve the CSRF state parameter - $state = isset($_GET['state']) ? $_GET['state'] : null; - - // This was a callback request from the OAuth2 service, get the token - $this->service->requestAccessToken($_GET['code'], $state); - - return $callback(); - } - break; - } - } - - /** - * Implements OAuth authentication for Facebook - * - * @return null|bool Returns a boolean on finished authentication. - */ - public function oauthFacebook() - { - return $this->genericOAuthProvider(function() { - // Send a request now that we have access token - $data = json_decode($this->service->request('/me'), true); - $email = isset($data['email']) ? $data['email'] : ''; - - // Authenticate OAuth user against Grav system. - return $this->authenticate($data['name'], $data['id'], $email); - }); - } - - /** - * Implements OAuth authentication for Google - * - * @return null|bool Returns a boolean on finished authentication. - */ - public function oauthGoogle() - { - return $this->genericOAuthProvider(function() { - // Get username, email and language - $data = json_decode($this->service->request('userinfo'), true); - - $username = $data['given_name'] . ' ' . $data['family_name']; - if (preg_match('~[\w\s]+\((\w+)\)~i', $data['name'], $matches)) { - $username = $matches[1]; - } - $lang = isset($data['lang']) ? $data['lang'] : ''; - - // Authenticate OAuth user against Grav system. - return $this->authenticate($username, $data['id'], $data['email'], $lang); - }); - } - - /** - * Implements OAuth authentication for GitHub - * - * @return null|bool Returns a boolean on finished authentication. - */ - public function oauthGithub() - { - return $this->genericOAuthProvider(function() { - // Get username, email and language - $user = json_decode($this->service->request('user'), true); - $emails = json_decode($this->service->request('user/emails'), true); - - // Authenticate OAuth user against Grav system. - return $this->authenticate($user['login'], $user['id'], reset($emails)); - }); - } - - /** - * Implements OAuth authentication for Twitter - * - * @return null|bool Returns a boolean on finished authentication. - */ - public function oauthTwitter() - { - return $this->genericOAuthProvider(function() { - // Get username, email and language - $data = json_decode( - $this->service->request('account/verify_credentials.json?include_email=true'), - true); - $lang = isset($data['lang']) ? $data['lang'] : ''; - - // Authenticate OAuth user against Grav system. - return $this->authenticate($data['screen_name'], $data['id'], '', $lang); - }, 'oauth1'); - } - - /** - * Authenticate user. - * - * @param string $username The username of the OAuth user - * @param string $email The email of the OAuth user - * @param string $language Language - * - * @return bool True if user was authenticated - */ - protected function authenticate($username, $id, $email, $language = '') - { - $accountFile = $this->grav['inflector']->underscorize($username); - $user = User::load(strtolower("$accountFile.{$this->action}")); - - if ($user->exists()) { - // Update username (hide OAuth from user) - $user->set('username', $username); - - $password = md5($id); - $authenticated = $user->authenticate($password); - } else { - /** @var User $user */ - $user = $this->grav['user']; - - // Check user rights - if (!$user->authenticated) { - $oauthUser = $this->grav['config']->get('plugins.login.oauth.user', []); - - // Create new user from OAuth request - $user = $this->createUser([ - 'id' => $id, - 'username' => $username, - 'email' => $email, - 'lang' => $language, - 'access' => $oauthUser['access'] - ], $oauthUser['autocreate']); - } - - // Authenticate user against oAuth rules - $authenticated = $user->authenticated; - } - - // Store user in session - if ($authenticated) { - $this->grav['session']->user = $user; - - unset($this->grav['user']); - $this->grav['user'] = $user; - } - - return $authenticated; - } - - /** - * Create user. - * - * @param string $data['username'] The username of the OAuth user - * @param string $data['password'] The unique id of the Oauth user - * setting as password - * @param string $data['email'] The email of the OAuth user - * @param string $data['language'] Language - * @param bool $save Save user - * - * @return User A user object - */ - protected function createUser($data, $save = false) - { - /** @var User $user */ - $user = $this->grav['user']; - - $accountFile = $this->grav['inflector']->underscorize($data['username']); - $accountFile = $this->grav['locator']->findResource('user://accounts/' . strtolower("$accountFile.{$this->action}") . YAML_EXT, true, true); - - $user->set('username', $data['username']); - $user->set('password', md5($data['id'])); - $user->set('email', $data['email']); - $user->set('lang', $data['lang']); - - // Set access rights - $user->set('access', $this->grav['config']->get('plugins.login.oauth.user.access', [])); - - // Authorize OAuth user to access page(s) - $user->authenticated = $user->authorize('site.login'); - - if ($save) { - $user->file(CompiledYamlFile::instance($accountFile)); - $user->save(); - } - - return $user; - } - - /** - * Generates Random Bytes for the given $length. - * - * @param int $length The number of bytes to generate - * @param bool $secure Return cryptographic secure string or not - * - * @return string - * - * @throws InvalidArgumentException when an invalid length is specified. - * @throws RuntimeException when no secure way of making bytes is posible - */ - protected function getRandomBytes($length = 0, $secure = true) - { - if ($length < 1) { - throw new \InvalidArgumentException('The length parameter must be a number greater than zero!'); - } - - /** - * Our primary choice for a cryptographic strong randomness function is - * openssl_random_pseudo_bytes. - */ - if (function_exists('openssl_random_pseudo_bytes')) { - $bytes = openssl_random_pseudo_bytes($length, $sec); - if ($sec === true) { - return $bytes; - } - } - - /** - * If mcrypt extension is available then we use it to gather entropy from - * the operating system's PRNG. This is better than reading /dev/urandom - * directly since it avoids reading larger blocks of data than needed. - * Older versions of mcrypt_create_iv may be broken or take too much time - * to finish so we only use this function with PHP 5.3.7 and above. - * @see https://bugs.php.net/bug.php?id=55169 - */ - if (function_exists('mcrypt_create_iv') && - (strtolower(substr(PHP_OS, 0, 3)) !== 'win' || - version_compare(PHP_VERSION, '5.3.7') >= 0)) { - $bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); - if ($bytes !== false) { - return $bytes; - } - } - - if ($secure) { - throw new \RuntimeException('There is no possible way of making secure bytes'); - } - - /** - * Fallback (not really secure, but better than nothing) - */ - return hex2bin(substr(str_shuffle(str_repeat('0123456789abcdef', $length*16)), 0, $length)); - } -} diff --git a/classes/Utils.php b/classes/Utils.php index ab47ee3..f12a3a6 100644 --- a/classes/Utils.php +++ b/classes/Utils.php @@ -4,11 +4,16 @@ use Grav\Common\Grav; +/** + * Class Utils + * @package Grav\Plugin\Login + */ class Utils { /** * Handle sending an email. * + * @param $subject * @param string $content * @param string $to * diff --git a/cli/ChangePasswordCommand.php b/cli/ChangePasswordCommand.php index 1fc8604..d15abb7 100644 --- a/cli/ChangePasswordCommand.php +++ b/cli/ChangePasswordCommand.php @@ -97,7 +97,7 @@ protected function serve() $username = strtolower($username); // Grab the account file and read in the information before setting the file (prevent setting erase) - $oldUserFile = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('user://accounts/' . $username . YAML_EXT, true, true)); + $oldUserFile = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('account://' . $username . YAML_EXT, true, true)); $oldData = $oldUserFile->content(); //Set the password feild to new password @@ -105,7 +105,7 @@ protected function serve() // Create user object and save it using oldData (with updated password) $user = new User($oldData); - $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('user://accounts/' . $username . YAML_EXT, true, true)); + $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('account://' . $username . YAML_EXT, true, true)); $user->file($file); $user->save(); @@ -137,7 +137,7 @@ protected function validate($type, $value, $extra = '') if (!preg_match('/^[a-z0-9_-]{3,16}$/', $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'); } - if (!file_exists(self::getGrav()['locator']->findResource('user://accounts/' . $value . YAML_EXT))) { + if (!file_exists(self::getGrav()['locator']->findResource('account://' . $value . YAML_EXT))) { throw new \RuntimeException('Username "' . $value . '" does not exist, please pick another username'); } diff --git a/cli/ChangeUserStateCommand.php b/cli/ChangeUserStateCommand.php index a14b639..40f9dc6 100644 --- a/cli/ChangeUserStateCommand.php +++ b/cli/ChangeUserStateCommand.php @@ -96,7 +96,7 @@ protected function serve() $username = strtolower($username); // Grab the account file and read in the information before setting the file (prevent setting erase) - $oldUserFile = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('user://accounts/' . $username . YAML_EXT, true, true)); + $oldUserFile = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('account://' . $username . YAML_EXT, true, true)); $oldData = $oldUserFile->content(); //Set the state feild to new state @@ -104,7 +104,7 @@ protected function serve() // Create user object and save it using oldData (with updated state) $user = new User($oldData); - $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('user://accounts/' . $username . YAML_EXT, true, true)); + $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('account://' . $username . YAML_EXT, true, true)); $user->file($file); $user->save(); @@ -136,7 +136,7 @@ protected function validate($type, $value, $extra = '') if (!preg_match('/^[a-z0-9_-]{3,16}$/', $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'); } - if (!file_exists(self::getGrav()['locator']->findResource('user://accounts/' . $value . YAML_EXT))) { + if (!file_exists(self::getGrav()['locator']->findResource('account://' . $value . YAML_EXT))) { throw new \RuntimeException('Username "' . $value . '" does not exist, please pick another username'); } diff --git a/cli/NewUserCommand.php b/cli/NewUserCommand.php index 1b595ef..17dbf48 100644 --- a/cli/NewUserCommand.php +++ b/cli/NewUserCommand.php @@ -4,6 +4,7 @@ use Grav\Console\ConsoleCommand; use Grav\Common\File\CompiledYamlFile; use Grav\Common\User\User; +use Grav\Common\Grav; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Question\ChoiceQuestion; @@ -207,7 +208,7 @@ protected function serve() // Create user object and save it $user = new User($data); - $file = CompiledYamlFile::instance(self::getGrav()['locator']->findResource('user://accounts/' . $username . YAML_EXT, true, true)); + $file = CompiledYamlFile::instance(Grav::instance()['locator']->findResource('account://' . $username . YAML_EXT, true, true)); $user->file($file); $user->save(); @@ -239,7 +240,7 @@ protected function validate($type, $value, $extra = '') if (!preg_match('/^[a-z0-9_-]{3,16}$/', $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'); } - if (file_exists(self::getGrav()['locator']->findResource('user://accounts/' . $value . YAML_EXT))) { + if (file_exists(Grav::instance()['locator']->findResource('account://' . $value . YAML_EXT))) { throw new \RuntimeException('Username "' . $value . '" already exists, please pick another username'); } diff --git a/composer.json b/composer.json index 181805a..78751cc 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,6 @@ }, "require": { "php": ">=5.4.0", - "lusitanian/oauth": "0.8.*", "birke/rememberme": "1.*" }, "autoload": { diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..b4eee9e --- /dev/null +++ b/composer.lock @@ -0,0 +1,63 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "468b7385e7f02c35396a205864c36ceb", + "content-hash": "540c2e320c9819de1b7c90b59f17263d", + "packages": [ + { + "name": "birke/rememberme", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/gbirke/rememberme.git", + "reference": "4e71b0d9c692db28ae78c7eea752c4599b87dc2c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/gbirke/rememberme/zipball/4e71b0d9c692db28ae78c7eea752c4599b87dc2c", + "reference": "4e71b0d9c692db28ae78c7eea752c4599b87dc2c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Birke\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gabriel Birke", + "email": "gb@birke-software.de" + } + ], + "description": "Secure \"Remember Me\" functionality", + "homepage": "https://github.com/gbirke/rememberme", + "keywords": [ + "cookie", + "remember", + "security" + ], + "time": "2015-07-22 18:26:14" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.4.0" + }, + "platform-dev": [] +} diff --git a/css/login.css b/css/login.css index 0e2ca53..6b7f3c4 100644 --- a/css/login.css +++ b/css/login.css @@ -31,77 +31,9 @@ #grav-login .form-actions p { margin-bottom: 0; } -#grav-login .form-oauth .oauth-provider, -#grav-login .form-oauth .oauth-provider-extra { - list-style: outside none none; - margin: 0.5rem 0; - padding: 0; -} -#grav-login .oauth-provider li { - display: inline-block; - padding: 0.25rem; -} -#grav-login .oauth-provider .button { - height: 5rem; - width: 5rem; - opacity: 1; -} - -#grav-login .form-oauth .button:hover { - opacity: 0.75; -} - -#grav-login .form-oauth button.facebook { - background-color: #4c699e; -} -#grav-login .form-oauth button.google { - background-color: #da573b; -} -#grav-login .form-oauth button.twitter { - background-color: #1daee3; -} -#grav-login .form-oauth button.github { - background-color: #5c5853; -} -#grav-login .form-oauth button.bitbucket { - background-color: #205081; -} -#grav-login .form-oauth button.flickr { - background-color: #400090; -} -#grav-login .form-oauth button.microsoft { - background-color: #2672ec; -} -#grav-login .form-oauth > input { - display: none; -} - -#grav-login .oauth-provider .button { - display: block; - font-size: 0.8rem; - font-weight: bold; - height: 5rem; - line-height: 8rem; - margin: 0; - padding: 0; - position: relative; - text-align: center; - width: 5rem; - color: white; - border: none; -} - -#grav-login .oauth-provider .button i { - bottom: 0; - content: " "; - display: block; - font-size: 3rem; - left: 0; - margin-top: .5rem; - position: absolute; - right: 0; - top: 0; +#grav-login .button { + vertical-align: middle; } #grav-login .delimiter { @@ -132,86 +64,6 @@ right: 0; } -#grav-login .form-oauth { - position: relative; -} - -#grav-login .oauth-provider-extra { - background: #eee; - border: 4px solid #eee; - border-radius: 4px; - opacity: 0; - position: absolute; - z-index: 1; - right: -9999px; - top: 1.6rem; - - -webkit-transition: opacity 0.3s ease-in-out 0s, right 0s linear 0.3s; - -moz-transition: opacity 0.3s ease-in-out 0s, right 0s linear 0.3s; - -ms-transition: opacity 0.3s ease-in-out 0s, right 0s linear 0.3s; - -o-transition: opacity 0.3s ease-in-out 0s, right 0s linear 0.3s; - transition: opacity 0.3s ease-in-out 0s, right 0s linear 0.3s; -} - -#grav-login .oauth-provider-extra::before { - background-color: ; - border: 1em solid transparent; - border-bottom-color: #eee; - bottom: 100%; - content: " "; - display: block; - height: 0; - left: 0; - margin: 0 auto; - position: absolute; - right: 0; - width: 0; -} - -#grav-login .oauth-provider-extra .button { - border: none; - border-radius: 0; - display: inline-block; - text-align: left; - width: 100%; - color: white; - font-weight: bold; - font-size: 0.9rem; -} - -#grav-login .oauth-provider-extra i[class~="fa"] { - display: inline-block; - font-size: 2rem; - margin-right: 0.5rem; - vertical-align: middle; -} - -#grav-login .form-oauth label { - color: #1bb3e9; - cursor: pointer; - display: inline; - font-weight: inherit; - position: relative; - z-index: 2; - - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - - -#grav-login #oauth-input:checked + .oauth-provider-extra { - opacity: 1; - right: 0; - - -webkit-transition: opacity 0.3s ease-in-out 0s; - -moz-transition: opacity 0.3s ease-in-out 0s; - -ms-transition: opacity 0.3s ease-in-out 0s; - -o-transition: opacity 0.3s ease-in-out 0s; - transition: opacity 0.3s ease-in-out 0s; -} - #grav-login .rememberme { display: inline-block; float: left; diff --git a/languages.yaml b/languages.yaml old mode 100644 new mode 100755 index 4d3bd2c..7752415 --- a/languages.yaml +++ b/languages.yaml @@ -2,80 +2,56 @@ en: PLUGIN_LOGIN: USERNAME: Username PASSWORD: Password - ACCESS_DENIED: Access denied... LOGIN_FAILED: Login failed... LOGIN_SUCCESSFUL: You have been successfully logged in. - BTN_LOGIN: Login BTN_LOGOUT: Logout BTN_FORGOT: Forgot BTN_REGISTER: Register - + BTN_RESET: "Reset Password" + BTN_SEND_INSTRUCTIONS: "Send Reset Instructions" + RESET_LINK_EXPIRED: "Reset link has expired, please try again" + RESET_PASSWORD_RESET: "Password has been reset" + RESET_INVALID_LINK: "Invalid reset link used, please try again" + FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Instructions to reset your password have been sent via email to %s" + FORGOT_FAILED_TO_EMAIL: "Failed to email instructions, please try again later" + FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Cannot reset password for %s, no email address is set" + FORGOT_USERNAME_DOES_NOT_EXIST: "User with username %s does not exist" + FORGOT_EMAIL_NOT_CONFIGURED: "Cannot reset password. This site is not configured to send emails" + FORGOT_EMAIL_SUBJECT: "%s Password Reset Request" + FORGOT_EMAIL_BODY: "
Dear %1$s,
A request was made on %4$s to reset your password.
Click this to reset your password
Alternatively, copy the following URL into your browser's address bar:
%2$s
Kind regards,
%3$s
%1$s,
Une demande a été faite sur %4$s pour la réinitialisation de votre mot de passe.
Cliquez ici pour réinitialiser votre mot de passe
Vous pouvez également copier l’URL suivante dans la barre d’adresse de votre navigateur :
%2$s
Cordialement,
%3$s