diff options
author | Peter Zhang <peter@innocraft.com> | 2022-07-08 14:24:54 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-08 14:24:54 +0300 |
commit | 5df7397b4276a8f23e5537b7ba84394f4238dbed (patch) | |
tree | f38e8706090a4fb6d687415a89b29377d9bfda11 /plugins/Login | |
parent | 49adc21534740eeb62a700562cdde095c7fa9589 (diff) |
update invite user (#19366)4.11.0-rc2
* update invite
update invite
* update tests and VUE error
update tests and VUE error
* built vue files
* update ui
update ui
* update vue
update vue
* update vue
update vue
* built vue files
* Update UsersManager_spec.js
update tests
* update clean
update clean
* Update UsersManager_spec.js
update success
* Update UsersManager_spec.js
remove popup error
* update tests order
update tests order
* Update UsersManager_next_click.png
updateui
* revert screenshot
revert screenshot
* delete if user if declined
delete use if declined
add privacy
split templated into 3 parts
* remove decline
remove decline
* add invited by column
add invited by column
* Revert "add invited by column"
This reverts commit e2358cb493684dc26fdf9a436d00a29981365861.
* Update UserRepository.php
add view user display
* add email tests and update admin email
add email tests and update admin email
* update php cs
update php cs
* add expire task and default setting
add expire task and default setting
* add api tests
add api tests
* add filter
add filter
* update ui tests
update ui tests
* built vue files
* update tests
update tests
* update signup and tests
update signup and tests
* update tests
update tests
* update screenshots
update screenshots
* update screenshots
update screenshots
* update ui
update ui
* update typo
update typo
* built vue files
* Update core/Updates/4.12.0-b1.php
Co-authored-by: Ben Burgess <88810029+bx80@users.noreply.github.com>
* Update plugins/UsersManager/API.php
Co-authored-by: Ben Burgess <88810029+bx80@users.noreply.github.com>
* Update plugins/UsersManager/UsersManager.php
Co-authored-by: Ben Burgess <88810029+bx80@users.noreply.github.com>
* Update plugins/UsersManager/lang/en.json
Co-authored-by: Ben Burgess <88810029+bx80@users.noreply.github.com>
* Update config/global.ini.php
Co-authored-by: Ben Burgess <88810029+bx80@users.noreply.github.com>
* update email date
update email date
* update typo
update typo
* update UI
update UI
* Updated UI screenshot
* Update plugins/Login/lang/en.json
Co-authored-by: Stefan Giehl <stefan@matomo.org>
* Update plugins/Login/templates/invitationDecline.twig
Co-authored-by: Stefan Giehl <stefan@matomo.org>
* update some feedbacks
update some feedbacks
* built vue files
* update feedbacks
update feedbacks
* update filter
update filter
* pending your can't reset password
pending your can't reset password
* fix php cs
fix php cs
* update column
update column
* built vue files
* set up terms in tests
set up terms in tests
* Update PendingUsers.php
setup website
* update tests and broken template
update tests
* update password
update password
* update screenshots
update screenshots
* add delete to admin
add delete to admin
* update invite success notification
update invite success notification
* add tests
add tests
* built vue files
* update tests
update tests
* update UI and checkbox
update UI and checkbox
* Update Invite_spec.js
show error screen
* update tests
update tests
* update lang
update lang
* Update OmniFixture-dump.sql
revert OmniFixture
* update wording
update wording
* update version
update version
* fix change column
fix change column
* Update UsersManagerTest.php
update tests
* Update Model.php
update tests
* Update Model.php
update tests
* remove fixes
remove fixes
* Revert "remove fixes"
This reverts commit f8fe33706513a32bf3386bc6b52800d68ec58562.
* update tests
update tests
* revert password reset
revert password reset
* update tests
update tests
* update resend
update resend
* update tests
update tests
* Update PasswordResetter.php
update tests
* correct pending user from last time
correct pending user from last time
* apply some fixes /improvements
* Apply suggestions from code review
Co-authored-by: Ben Burgess <88810029+bx80@users.noreply.github.com>
* Allow composer plugins for dev dependency codesniffer to fix travis builds. (#19468)
Co-authored-by: peterhashair <peterhashair@users.noreply.github.com>
Co-authored-by: Ben Burgess <88810029+bx80@users.noreply.github.com>
Co-authored-by: Ben <ben.burgess@innocraft.com>
Co-authored-by: Stefan Giehl <stefan@matomo.org>
Co-authored-by: dizzy <diosmosis@users.noreply.github.com>
Diffstat (limited to 'plugins/Login')
17 files changed, 366 insertions, 263 deletions
diff --git a/plugins/Login/Controller.php b/plugins/Login/Controller.php index 98b0dd23e9..08c657827a 100644 --- a/plugins/Login/Controller.php +++ b/plugins/Login/Controller.php @@ -1,4 +1,5 @@ <?php + /** * Matomo - free/libre analytics platform * @@ -6,6 +7,7 @@ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * */ + namespace Piwik\Plugins\Login; use Exception; @@ -13,12 +15,14 @@ use Piwik\Auth\Password; use Piwik\Common; use Piwik\Config; use Piwik\Container\StaticContainer; +use Piwik\Date; use Piwik\Log; use Piwik\Nonce; use Piwik\Piwik; use Piwik\Plugins\CoreAdminHome\Emails\UserAcceptInvitationEmail; use Piwik\Plugins\CoreAdminHome\Emails\UserDeclinedInvitationEmail; use Piwik\Plugins\Login\Security\BruteForceDetection; +use Piwik\Plugins\PrivacyManager\SystemSettings; use Piwik\Plugins\UsersManager\Model as UsersModel; use Piwik\Plugins\UsersManager\UsersManager; use Piwik\QuickForm2; @@ -70,19 +74,19 @@ class Controller extends \Piwik\Plugin\ControllerAdmin * Constructor. * * @param PasswordResetter $passwordResetter - * @param AuthInterface $auth + * @param \Piwik\Auth $auth * @param SessionInitializer $sessionInitializer * @param PasswordVerifier $passwordVerify * @param BruteForceDetection $bruteForceDetection * @param SystemSettings $systemSettings */ public function __construct( - $passwordResetter = null, - $auth = null, - $sessionInitializer = null, - $passwordVerify = null, - $bruteForceDetection = null, - $systemSettings = null + $passwordResetter = null, + $auth = null, + $sessionInitializer = null, + $passwordVerify = null, + $bruteForceDetection = null, + $systemSettings = null ) { parent::__construct(); @@ -122,7 +126,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin * * @return string */ - function index() + public function index() { return $this->login(); } @@ -135,7 +139,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin * @return string * @internal param string $currentUrl Current URL */ - function login($messageNoAccess = null, $infoMessage = false) + public function login($messageNoAccess = null, $infoMessage = false) { $form = new FormLogin(); if ($form->validate()) { @@ -231,20 +235,20 @@ class Controller extends \Piwik\Plugin\ControllerAdmin } } - return $this->renderTemplate('@Login/confirmPassword', array( + return $this->renderTemplate('@Login/confirmPassword', [ 'nonce' => Nonce::getNonce($nonceKey), 'AccessErrorString' => $messageNoAccess, 'loginPlugin' => Piwik::getLoginPluginName(), - )); + ]); } /** * Form-less login - * @see how to use it on http://piwik.org/faq/how-to/#faq_30 + * @see how to use it on https://matomo.org/faq/how-to/faq_30 * @throws Exception * @return void */ - function logme() + public function logme() { if (Config::getInstance()->General['login_allow_logme'] == 0) { throw new Exception('This functionality has been disabled in config'); @@ -254,8 +258,9 @@ class Controller extends \Piwik\Plugin\ControllerAdmin $login = Common::getRequestVar('login', null, 'string'); if (Piwik::hasTheUserSuperUserAccess($login)) { - throw new Exception(Piwik::translate('Login_ExceptionInvalidSuperUserAccessAuthenticationMethod', - array("logme"))); + throw new Exception( + Piwik::translate('Login_ExceptionInvalidSuperUserAccessAuthenticationMethod', ["logme"]) + ); } $currentUrl = 'index.php'; @@ -274,10 +279,10 @@ class Controller extends \Piwik\Plugin\ControllerAdmin { Piwik::checkUserHasSuperUserAccess(); - return $this->renderTemplate('bruteForceLog', array( + return $this->renderTemplate('bruteForceLog', [ 'blockedIps' => $this->bruteForceDetection->getCurrentlyBlockedIps(), 'blacklistedIps' => $this->systemSettings->blacklistedBruteForceIps->getValue() - )); + ]); } /** @@ -289,14 +294,14 @@ class Controller extends \Piwik\Plugin\ControllerAdmin public function ajaxNoAccess($errorMessage) { return sprintf( - '<div class="alert alert-danger"> + '<div class="alert alert-danger"> <p><strong>%s:</strong> %s</p> <p><a href="%s">%s</a></p> </div>', - Piwik::translate('General_Error'), - htmlentities($errorMessage, Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8', $doubleEncode = false), - 'index.php?module=' . Piwik::getLoginPluginName(), - Piwik::translate('Login_LogIn') + Piwik::translate('General_Error'), + htmlentities($errorMessage, Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8', $doubleEncode = false), + 'index.php?module=' . Piwik::getLoginPluginName(), + Piwik::translate('Login_LogIn') ); } @@ -345,7 +350,8 @@ class Controller extends \Piwik\Plugin\ControllerAdmin $currentHost = explode(':', $currentHost, 2)[0]; // we only redirect to a trusted host - if (!empty($host) && !empty($currentHost) && $host == $currentHost && Url::isValidHost($host) + if ( + !empty($host) && !empty($currentHost) && $host == $currentHost && Url::isValidHost($host) ) { $urlToRedirect = $redirect; } @@ -362,9 +368,8 @@ class Controller extends \Piwik\Plugin\ControllerAdmin /** * Reset password action. Stores new password as hash and sends email * to confirm use. - * */ - function resetPassword() + public function resetPassword() { $infoMessage = null; $formErrors = null; @@ -379,7 +384,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin $infoMessage = Piwik::translate('Login_ConfirmationLinkSent'); } } else { - $formErrors = array($errorMessage); + $formErrors = [$errorMessage]; } } else { // if invalid, display error @@ -410,7 +415,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin } catch (Exception $ex) { Log::debug($ex); - return array($ex->getMessage()); + return [$ex->getMessage()]; } return null; @@ -444,15 +449,21 @@ class Controller extends \Piwik\Plugin\ControllerAdmin return $this->login($errorMessage); } - if (!empty($_POST['nonce']) - && !empty($_POST['mtmpasswordconfirm']) - && !empty($resetToken) - && !empty($login) - && !empty($passwordHash) - && empty($errorMessage)) { + if ( + !empty($_POST['nonce']) + && !empty($_POST['mtmpasswordconfirm']) + && !empty($resetToken) + && !empty($login) + && !empty($passwordHash) + && empty($errorMessage) + ) { Nonce::checkNonce(self::NONCE_CONFIRMRESETPASSWORD, $_POST['nonce']); - if ($this->passwordResetter->doesResetPasswordHashMatchesPassword($_POST['mtmpasswordconfirm'], - $passwordHash)) { + if ( + $this->passwordResetter->doesResetPasswordHashMatchesPassword( + $_POST['mtmpasswordconfirm'], + $passwordHash + ) + ) { $this->passwordResetter->setHashedPasswordForLogin($login, $passwordHash); return $this->resetPasswordSuccess(); } else { @@ -462,10 +473,10 @@ class Controller extends \Piwik\Plugin\ControllerAdmin $nonce = Nonce::getNonce(self::NONCE_CONFIRMRESETPASSWORD); - return $this->renderTemplateAs('confirmResetPassword', array( + return $this->renderTemplateAs('confirmResetPassword', [ 'nonce' => $nonce, 'errorMessage' => $errorMessage - ), 'basic'); + ], 'basic'); } /** @@ -475,7 +486,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin */ public function resetPasswordSuccess() { - $_POST = array(); // prevent showing error message username and password is missing + $_POST = []; // prevent showing error message username and password is missing return $this->login($errorMessage = null, $infoMessage = Piwik::translate('Login_PasswordChanged')); } @@ -499,7 +510,7 @@ class Controller extends \Piwik\Plugin\ControllerAdmin */ public function logout() { - Piwik::postEvent('Login.logout', array(Piwik::getCurrentUserLogin())); + Piwik::postEvent('Login.logout', [Piwik::getCurrentUserLogin()]); self::clearSession(); @@ -524,79 +535,99 @@ class Controller extends \Piwik\Plugin\ControllerAdmin $token = Common::getRequestVar('token', null, 'string'); $form = Common::getRequestVar('invitation_form', false, 'string'); - //check token is valid - $user = $model->getUserByTokenAuth($token); - if ($user['invite_status'] !== 'pending') { - throw new Exception(Piwik::translate('Login_InvalidOrExpiredToken')); - } + $settings = new SystemSettings(); + $termsAndConditionUrl = $settings->termsAndConditionUrl->getValue(); + $privacyPolicyUrl = $settings->privacyPolicyUrl->getValue(); + $user = $model->getUserByInviteToken($token); - //if user not match the invite user + // if no user matches the invite token if (!$user) { throw new Exception(Piwik::translate('Login_InvalidUsernameEmail')); } - //if form is blank + if (!empty($user['invite_expired_at']) && Date::factory($user['invite_expired_at'])->isEarlier(Date::now())) { + throw new Exception(Piwik::translate('Login_InvalidOrExpiredToken')); + } + + // if form was sent if (!empty($form)) { $error = null; $password = Common::getRequestVar('password', false, 'string'); $passwordConfirmation = Common::getRequestVar('passwordConfirmation', false, 'string'); - $terms = Common::getRequestVar('terms', false, 'string'); + $conditionCheck = Common::getRequestVar('conditionCheck', false, 'string'); + if (!$password) { $error = Piwik::translate('Login_PasswordRequired'); } - //not accept terms - if (!$terms) { - $error = Piwik::translate('Login_TermsRequired'); + // check if terms accepted and privacy + if (!$conditionCheck && ($privacyPolicyUrl || $termsAndConditionUrl)) { + if ($privacyPolicyUrl && $termsAndConditionUrl) { + $error = Piwik::translate('Login_AcceptPrivacyPolicyAndTermsAndCondition'); + } elseif ($privacyPolicyUrl) { + $error = Piwik::translate('Login_AcceptPrivacyPolicy'); + } elseif ($termsAndConditionUrl) { + $error = Piwik::translate('Login_AcceptTermsAndCondition'); + } } - //valid password + // validate password if (!UsersManager::isValidPasswordString($password)) { - $error = Piwik::translate('UsersManager_ExceptionInvalidPassword', - array(UsersManager::PASSWORD_MIN_LENGTH)); + $error = Piwik::translate('UsersManager_ExceptionInvalidPassword', [UsersManager::PASSWORD_MIN_LENGTH]); } - //confirm matching password + + // confirm matching passwords if ($password !== $passwordConfirmation) { $error = Piwik::translate('Login_PasswordsDoNotMatch'); } if (!$error) { $password = UsersManager::getPasswordHash($password); - $passwordInfo = $passwordHelper->info($password); - - if (!isset($passwordInfo['algo']) || 0 >= $passwordInfo['algo']) { - // password may have already been fully hashed - $password = $passwordHelper->hash($password); + $password = $passwordHelper->hash($password); + + // update pending user to active user + $model->updateUserFields( + $user['login'], + [ + 'password' => $password, + 'invite_token' => null, + 'invite_accept_at' => Date::now()->getDatetime(), + 'invite_expired_at' => null, + ] + ); + + // send e-mail to inviter + if (!empty($user['invited_by'])) { + $invitedBy = $model->getUser($user['invited_by']); + if ($invitedBy) { + $mail = StaticContainer::getContainer()->make(UserAcceptInvitationEmail::class, [ + 'login' => $user['invited_by'], + 'emailAddress' => $invitedBy['email'], + 'userLogin' => $user['login'], + ]); + $mail->safeSend(); + } } - //update pending user to active user - $model->updateUserFields($user['login'], ['password' => $password, 'invite_status' => 'accept']); - $sessionInitializer = new SessionInitializer(); - $auth = StaticContainer::get('Piwik\Auth'); - $auth->setTokenAuth(null); // ensure authenticated through password - $auth->setLogin($user['login']); - $auth->setPassword($passwordConfirmation); - $sessionInitializer->initSession($auth); - - //send Admin Email - try { - $mail = StaticContainer::getContainer()->make(UserAcceptInvitationEmail::class, array( - 'login' => $user['login'], - 'emailAddress' => $user['email'], - 'userLogin' => $user['login'], - )); - $mail->safeSend(); - } catch (\Exception $e) { - - } + /** + * Triggered after a user accepted an invite + * + * @param string $userLogin The invited user's login. + * @param string $email The invited user's e-mail. + * @param string $inviterLogin The login of the user, who invited this user + */ + Piwik::postEvent('UsersManager.inviteUser.accepted', [$user['login'], $user['email'], $user['invited_by']]); - $this->redirectToIndex('CoreHome', 'index'); + $this->authenticateAndRedirect($user['login'], $passwordConfirmation); } + $view->AccessErrorString = $error; } + $view->user = $user; + $view->termsAndCondition = $termsAndConditionUrl; + $view->privacyPolicyUrl = $privacyPolicyUrl; $view->token = $token; - $view->declined = false; $this->configureView($view); self::setHostValidationVariablesView($view); return $view->render(); @@ -609,42 +640,56 @@ class Controller extends \Piwik\Plugin\ControllerAdmin $token = Common::getRequestVar('token', null, 'string'); $form = Common::getRequestVar('invitation_form', false, 'string'); - $user = $model->getUserByTokenAuth($token); - if ($user['invite_status'] !== 'pending') { + $user = $model->getUserByInviteToken($token); + + // if no user matches the invite token + if (!$user) { throw new Exception(Piwik::translate('Login_InvalidOrExpiredToken')); } - //if user not match the invite user - if (!$user) { - throw new Exception(Piwik::translate('Login_InvalidUsernameEmail')); + + if (!empty($user['invite_expired_at']) && Date::factory($user['invite_expired_at'])->isEarlier(Date::now())) { + throw new Exception(Piwik::translate('Login_InvalidOrExpiredToken')); } - if ($form) { - $model->deleteAllTokensForUser($user['login']); - $model->updateUserFields($user['login'], ['invite_status' => 'decline']); - $this->redirectToIndex('Login', 'index'); + $view = new View('@Login/invitationDecline'); - } + if ($form) { + // remove user + try { + $model->deleteUser($user['login']); + } catch (\Exception $e) { + // deleting the user triggers an event, which might call methods that require a user to be logged in + // as those operations might not be needed for a pending user, we simply ignore any errors here + } - $view = new View('@Login/invitation'); - $view->declined = true; - $view->token = $token; + // send e-mail to inviter + if (!empty($user['invited_by'])) { + $invitedBy = $model->getUser($user['invited_by']); + if ($invitedBy) { + $mail = StaticContainer::getContainer()->make(UserDeclinedInvitationEmail::class, [ + 'login' => $user['invited_by'], + 'emailAddress' => $invitedBy['email'], + 'userLogin' => $user['login'], + ]); + $mail->safeSend(); + } + } - //send Admin Email - try { - $mail = StaticContainer::getContainer()->make(UserDeclinedInvitationEmail::class, array( - 'login' => $user['login'], - 'emailAddress' => $user['email'], - 'userLogin' => $user['login'], - )); - $mail->safeSend(); - } catch (\Exception $e) { + $view = new View('@Login/invitationDeclineSuccess'); + /** + * Triggered after a user accepted an invite + * + * @param string $userLogin The invited user's login. + * @param string $email The invited user's e-mail. + * @param string $inviterLogin The login of the user, who invited this user + */ + Piwik::postEvent('UsersManager.inviteUser.declined', [$user['login'], $user['email'], $user['invited_by']]); } + + $view->token = $token; $this->configureView($view); self::setHostValidationVariablesView($view); return $view->render(); - - } - } diff --git a/plugins/Login/PasswordResetter.php b/plugins/Login/PasswordResetter.php index 45854f4601..3d0039554c 100644 --- a/plugins/Login/PasswordResetter.php +++ b/plugins/Login/PasswordResetter.php @@ -15,9 +15,9 @@ use Piwik\IP; use Piwik\Option; use Piwik\Piwik; use Piwik\Plugins\Login\Emails\PasswordResetEmail; +use Piwik\Plugins\UsersManager\API as UsersManagerAPI; use Piwik\Plugins\UsersManager\Model; use Piwik\Plugins\UsersManager\UsersManager; -use Piwik\Plugins\UsersManager\API as UsersManagerAPI; use Piwik\Plugins\UsersManager\UserUpdater; use Piwik\SettingsPiwik; use Piwik\Url; @@ -379,6 +379,8 @@ class PasswordResetter /** * Returns user information based on a login or email. * + * If user is pending, return null + * * Derived classes can override this method to provide custom user querying logic. * * @param string $loginMail user login or email address @@ -388,7 +390,12 @@ class PasswordResetter { $userModel = new Model(); + if ($userModel->isPendingUser($loginOrMail)) { + return null; + } + $user = null; + if ($userModel->userExists($loginOrMail)) { $user = $userModel->getUser($loginOrMail); } else if ($userModel->userEmailExists($loginOrMail)) { diff --git a/plugins/Login/lang/en.json b/plugins/Login/lang/en.json index 9f97cad22b..92ff4bc518 100644 --- a/plugins/Login/lang/en.json +++ b/plugins/Login/lang/en.json @@ -55,14 +55,18 @@ "SuspiciousLoginAttemptsInLastHourEmail4": "Setup two-factor auth so attackers will need more information than just your password in order to login.", "SuspiciousLoginAttemptsInLastHourEmail5": "Additionally, if your Matomo has a limited set of users or IPs through which users will access it, it may be beneficial to setup a IP address allowlist. %1$sRead our docs for more information.%2$s", "LoginNotAllowedBecauseUserLoginBlocked": "Login functionality is temporarily disabled since we've a suspicious amount of failed login attempts in the last hour.", - "InvitationTitle": "Accept Invitation", - "InvitationDeclineTitle": "Decline Invitation", - "InvitationDeclineBody": "Are you sure you want to decline this Invitation?", + "InvitationTitle": "Accept invitation", + "InvitationDeclineTitle": "Decline invitation", + "InvitationDeclineBody": "Are you sure you want to decline this invitation?", "InvitationHints": "(to cancel this action, just leave the page)", "Accept": "Accept", - "Yes": "Yes", - "TermsRequired": "Please Accept the terms and conditions", "PasswordRequired": "Please enter password to continue", - "declineInvitationInfo": "Your invitation has been decline." + "DeclineInvitationInfo": "Your invitation was successfully declined.", + "BySigningUpPrivacyPolicy": "By signing up, I accept the %1$sprivacy policy%2$s", + "BySigningUpTermsAndCondition": "By signing up, I accept the %1$sterms & conditions%2$s", + "BySigningUpPrivacyPolicyAndTermsAndCondition": "By signing up, I accept the %1$sprivacy policy%2$s and the %3$sterms & conditions%4$s", + "AcceptPrivacyPolicy": "You need to accept the privacy policy.", + "AcceptTermsAndCondition": "You need to accept the terms & conditions.", + "AcceptPrivacyPolicyAndTermsAndCondition": "You need to accept the privacy policy and the terms & conditions." } } diff --git a/plugins/Login/templates/invitation.twig b/plugins/Login/templates/invitation.twig index adcba7d505..a559a9aab4 100644 --- a/plugins/Login/templates/invitation.twig +++ b/plugins/Login/templates/invitation.twig @@ -1,83 +1,83 @@ -{% extends '@Login/inviteLayout.twig' %} +{% extends '@Login/loginLayout.twig' %} {% block loginContent %} - {% if not declined %} - <div class="contentForm invitationForm"> - <div class="card"> - <div class="card-content"> - <div class="card-title"> - {{ "Login_InvitationTitle"|translate }} - </div> + <div class="contentForm invitationForm"> + <div class="card"> + <div class="card-content"> + <div class="card-title"> + {{ "Login_InvitationTitle"|translate }} + </div> - {% if AccessErrorString is defined %} - <div piwik-notification - noclear="true" - context="error"> - <strong>{{ 'General_Error'|translate }}</strong>: {{ AccessErrorString|raw }}<br/> - </div> - {% endif %} - {% block content %} - <form method="post" action="?module=Login&action=acceptInvitation"> - <input type="hidden" name="token" value="{{ token }}"/> - <div class="row"> - <div class="col s12 input-field"> - <input type="text" name="login" value="{{ user.login }}" size="20" readonly - tabindex="0"/> - <label><i class="icon-user icon"></i> {{ 'Login_LoginOrEmail'|translate }}</label> - </div> - <div class="col s12 input-field"> - <input type="password" placeholder="" name="password" id="password" class="input" value="" size="20" - autocorrect="off" autocapitalize="none" - tabindex="1" required/> - <label for="password"><i class="icon-locked icon"></i> {{ 'Login_NewPassword'|translate }}</label> - </div> - <div class="col s12 input-field"> - <input type="password" placeholder="" name="passwordConfirmation" id="password_confirm" class="input" value="" size="20" - autocorrect="off" autocapitalize="none" - tabindex="2"/> - <label for="password_confirm"><i class="icon-locked icon"></i> {{ 'Login_NewPasswordRepeat'|translate }}</label> - </div> + {% if AccessErrorString is defined %} + <div piwik-notification + noclear="true" + context="error"> + <strong>{{ 'General_Error'|translate }}</strong>: {{ AccessErrorString|raw }}<br/> + </div> + {% endif %} + {% block content %} + <form method="post" action="?module=Login&action=acceptInvitation"> + <input type="hidden" name="token" value="{{ token }}"/> + <div class="row"> + <div class="col s12 input-field"> + <input type="text" name="login" value="{{ user.login }}" size="20" readonly + tabindex="0"/> + <label><i class="icon-user icon"></i> {{ 'Login_LoginOrEmail'|translate }}</label> + </div> + <div class="col s12 input-field"> + <input type="password" placeholder="" name="password" id="password" class="input" value="" size="20" + autocorrect="off" autocapitalize="none" + tabindex="1" required/> + <label for="password"><i class="icon-locked icon"></i> {{ 'Login_NewPassword'|translate }}</label> + </div> + <div class="col s12 input-field"> + <input type="password" placeholder="" name="passwordConfirmation" id="password_confirm" class="input" value="" size="20" + autocorrect="off" autocapitalize="none" + tabindex="2" required/> + <label for="password_confirm"><i class="icon-locked icon"></i> {{ 'Login_NewPasswordRepeat'|translate }}</label> </div> - <div class="row actions"> + </div> + <div class="row actions"> + {% if privacyPolicyUrl|default('') is not empty or termsAndCondition|default('') is not empty %} <div class="col s12"> <label> - <input name="terms" type="checkbox" id="terms" value="1" tabindex="90"/> - <span>{{ 'PrivacyManager_TermsAndConditions'|translate }}</span> + <input name="conditionCheck" type="checkbox" id="conditionCheck" value="1" tabindex="89"/> + <span> + {% if privacyPolicyUrl|default('') is not empty and termsAndCondition|default('') is empty %} + {{ 'Login_BySigningUpPrivacyPolicy'|translate( + '<a target="_blank" rel="noreferrer noopener" href="' ~ privacyPolicyUrl|safelink|e('html_attr') ~ '">', + '</a>' + )|raw }} + {% elseif privacyPolicyUrl|default('') is empty and termsAndCondition|default('') is not empty %} + {{ 'Login_BySigningUpTermsAndCondition'|translate( + '<a target="_blank" rel="noreferrer noopener" href="' ~ termsAndCondition|safelink|e('html_attr') ~ '">', + '</a>' + )|raw }} + {% elseif privacyPolicyUrl|default('') is not empty and termsAndCondition|default('') is not empty %} + {{ 'Login_BySigningUpPrivacyPolicyAndTermsAndCondition'|translate( + '<a target="_blank" rel="noreferrer noopener" href="' ~ privacyPolicyUrl|safelink|e('html_attr') ~ '">', + '</a>', + '<a target="_blank" rel="noreferrer noopener" href="' ~ termsAndCondition|safelink|e('html_attr') ~ '">', + '</a>' + )|raw }} + {% endif %} + </span> </label> - <input class="submit btn" name="invitation_form" id="login_form_submit" type="submit" value="{{ 'Login_Accept'|translate }}" - tabindex="100"/> </div> - </div> - </form> - {% if isCustomLogo %} - <p id="piwik"> - <i><a href="https://matomo.org/" rel="noreferrer noopener" target="_blank">{{ linkTitle }}</a></i> - </p> - {% endif %} - {% endblock %} - </div> - </div> - </div> - {% else %} - <div class="contentForm invitationForm"> - <div class="card"> - <div class="card-content"> - <div class="card-title"> - {{ "Login_InvitationDeclineTitle"|translate }} - </div> - <p> {{ "Login_InvitationDeclineBody"|translate }}</p> - <p class="hints"> {{ "Login_InvitationHints"|translate }}</p> - <form method="post" action="?module=Login&action=declineInvitation"> - <input type="hidden" name="token" value="{{ token }}"/> - <div class="row actions"> - <div class="col s12"> - <input class="submit btn" name="invitation_form" id="login_form_submit" type="submit" value="{{ 'Login_Yes'|translate }}" + {% endif %} + <div style="margin-top:10px" class="col s12"> + <input class="submit btn" name="invitation_form" id="login_form_submit" type="submit" value="{{ 'Login_Accept'|translate }}" tabindex="100"/> </div> </div> </form> - </div> + {% if isCustomLogo %} + <p id="piwik"> + <i><a href="https://matomo.org/" rel="noreferrer noopener" target="_blank">{{ linkTitle }}</a></i> + </p> + {% endif %} + {% endblock %} </div> </div> - {% endif %} + </div> {% endblock %}
\ No newline at end of file diff --git a/plugins/Login/templates/invitationDecline.twig b/plugins/Login/templates/invitationDecline.twig new file mode 100644 index 0000000000..0e72c6d874 --- /dev/null +++ b/plugins/Login/templates/invitationDecline.twig @@ -0,0 +1,23 @@ +{% extends '@Login/loginLayout.twig' %} + +{% block loginContent %} + <div class="contentForm invitationForm"> + <div class="card"> + <div class="card-content"> + <div class="card-title"> + {{ "Login_InvitationDeclineTitle"|translate }} + </div> + <p> {{ "Login_InvitationDeclineBody"|translate }}</p> + <p class="hints"> {{ "Login_InvitationHints"|translate }}</p> + <form method="post" action="?module=Login&action=declineInvitation"> + <input type="hidden" name="token" value="{{ token }}"/> + <div class="row actions"> + <div class="col s12"> + <input class="submit btn" name="invitation_form" id="login_form_submit" type="submit" value="{{ 'General_Yes'|translate }}" + </div> + </div> + </form> + </div> + </div> + </div> +{% endblock %}
\ No newline at end of file diff --git a/plugins/Login/templates/invitationDeclineSuccess.twig b/plugins/Login/templates/invitationDeclineSuccess.twig new file mode 100644 index 0000000000..cf4f94dbce --- /dev/null +++ b/plugins/Login/templates/invitationDeclineSuccess.twig @@ -0,0 +1,11 @@ +{% extends '@Login/loginLayout.twig' %} + +{% block loginContent %} + <div class="contentForm invitationForm"> + <div class="card"> + <div class="notification system notification-success"> + <p> {{ "Login_DeclineInvitationInfo"|translate }}</p> + </div> + </div> + </div> +{% endblock %}
\ No newline at end of file diff --git a/plugins/Login/templates/inviteLayout.twig b/plugins/Login/templates/inviteLayout.twig deleted file mode 100644 index 3db4ed5be6..0000000000 --- a/plugins/Login/templates/inviteLayout.twig +++ /dev/null @@ -1,46 +0,0 @@ -{% extends '@Morpheus/layout.twig' %} - -{% block meta %} - <meta name="robots" content="noindex,nofollow"> -{% endblock %} - -{% block head %} - {{ parent() }} -{% endblock %} - -{% set title %}{{ 'Login_InvitationTitle'|translate }}{% endset %} - -{% block pageDescription %}{{ 'General_OpenSourceWebAnalytics'|translate }}{% endblock %} - -{% set bodyId = 'loginPage' %} - -{% block body %} - - {{ postEvent("Template.beforeTopBar", "login") }} - {{ postEvent("Template.beforeContent", "login") }} - - {% include "_iframeBuster.twig" %} - - <div id="notificationContainer"> - </div> - <nav> - <div class="nav-wrapper"> - {% include "@CoreHome/_logo.twig" with { 'logoLink': 'https://matomo.org', 'centeredLogo': true, 'useLargeLogo': false } %} - </div> - </nav> - - <section class="loginSection row"> - <div class="col s12 m6 push-m3 l4 push-l4"> - - {# untrusted host warning #} - {% if (isValidHost is defined and invalidHostMessage is defined and isValidHost == false) %} - {% include '@CoreHome/_warningInvalidHost.twig' %} - {% else %} - {% block loginContent %} - {% endblock %} - {% endif %} - - </div> - </section> - -{% endblock %} diff --git a/plugins/Login/tests/Fixtures/PendingUsers.php b/plugins/Login/tests/Fixtures/PendingUsers.php index 4be64378ac..fea4f24000 100644 --- a/plugins/Login/tests/Fixtures/PendingUsers.php +++ b/plugins/Login/tests/Fixtures/PendingUsers.php @@ -1,39 +1,40 @@ <?php + /** * Matomo - free/libre analytics platform * * @link https://matomo.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ + namespace Piwik\Plugins\Login\tests\Fixtures; -use Piwik\Date; +use Piwik\Plugins\PrivacyManager\SystemSettings; use Piwik\Plugins\UsersManager\Model; use Piwik\Tests\Framework\Fixture; /** - * Generates tracker testing data for our APITest - * - * This Simple fixture adds one website and tracks one visit with couple pageviews and an ecommerce conversion + * Simple fixture that creates a user with pending invitation */ class PendingUsers extends Fixture { - public $dateTime = '2013-01-23 01:23:45'; public $idSite = 1; - public $users = array(); + public $users = []; - public $pendingUser = array( + public $pendingUser = [ 'login' => '000pendingUser', 'email' => 'pendinguser2light@example.com' - ); + ]; public $token = "13cb9dcef6cc70b02a640cee30dc8ce9"; public function setUp(): void { + $this->setUpWebsite(); $this->setUpUser(); + $this->setUpTermsAndPrivacy(); } public function tearDown(): void @@ -45,10 +46,23 @@ class PendingUsers extends Fixture { $model = new Model(); $model->addUser($this->pendingUser['login'], '', $this->pendingUser['email'], $this->dateTime, 1); + $model->attachInviteToken($this->pendingUser['login'], $this->token, 7); + } - $model->addTokenAuth($this->pendingUser['login'], $this->token, "Invite Token", - Date::now()->getDatetime(), - Date::now()->addDay(7)->getDatetime()); + private function setUpWebsite() + { + if (!self::siteCreated($this->idSite)) { + $idSite = self::createWebsite($this->dateTime, $ecommerce = 1); + $this->assertSame($this->idSite, $idSite); + } + } + + private function setUpTermsAndPrivacy() + { + $settings = new SystemSettings(); + $settings->termsAndConditionUrl->setValue('matomo.org'); + $settings->privacyPolicyUrl->setValue('matomo.org'); + $settings->save(); } -}
\ No newline at end of file +} diff --git a/plugins/Login/tests/Integration/LoginTest.php b/plugins/Login/tests/Integration/LoginTest.php index 39f2cc3e76..533a5747d4 100644 --- a/plugins/Login/tests/Integration/LoginTest.php +++ b/plugins/Login/tests/Integration/LoginTest.php @@ -341,14 +341,16 @@ class LoginTest extends IntegrationTestCase protected function _setUpUser() { - $user = array('login' => 'user', - 'password' => 'geqgeagae', - 'email' => 'test@test.com', - 'superuser_access' => 0); + $user = array( + 'login' => 'user', + 'password' => 'geqgeagae', + 'email' => 'test@test.com', + 'superuser_access' => 0 + ); API::getInstance()->addUser($user['login'], $user['password'], $user['email']); - $model = new \Piwik\Plugins\UsersManager\Model(); + $model = new \Piwik\Plugins\UsersManager\Model(); $tokenAuth = $model->generateRandomTokenAuth(); $model->addTokenAuth($user['login'], $tokenAuth, 'many users test', Date::now()->getDatetime()); @@ -393,7 +395,7 @@ class LoginTest extends IntegrationTestCase public function provideContainerConfig() { return array( - 'Piwik\Access' => new FakeAccess() + 'Piwik\Access' => new FakeAccess() ); } } diff --git a/plugins/Login/tests/Integration/PasswordResetterTest.php b/plugins/Login/tests/Integration/PasswordResetterTest.php index b6014aecff..b8f8af336e 100644 --- a/plugins/Login/tests/Integration/PasswordResetterTest.php +++ b/plugins/Login/tests/Integration/PasswordResetterTest.php @@ -11,6 +11,7 @@ namespace Piwik\Plugins\Login\tests\Integration; use PHPMailer\PHPMailer\PHPMailer; use Piwik\Access; +use Piwik\API\Request; use Piwik\Auth; use Piwik\Container\StaticContainer; use Piwik\Option; @@ -166,6 +167,26 @@ class PasswordResetterTest extends IntegrationTestCase $this->passwordResetter->checkValidConfirmPasswordToken('superUserLogin', $oldCapturedToken); } + public function testPasswordResetShouldNotWorkForPendingUser() + { + self::expectException(\Exception::class); + self::expectExceptionMessage('Invalid username or e-mail address.'); + + Request::processRequest( + 'UsersManager.inviteUser', + [ + 'userLogin' => 'pendingUser', + 'email' => 'pending@user.io', + 'idSite' => 1, + 'expiryInDays' => 7 + ] + ); + + self::assertTrue($this->userModel->isPendingUser('pendingUser')); + + $this->passwordResetter->initiatePasswordResetProcess('pendingUser', self::NEWPASSWORD); + } + /** * @param Fixture $fixture */ diff --git a/plugins/Login/tests/UI/Decline_spec.js b/plugins/Login/tests/UI/Decline_spec.js index b2b6893784..515a44a6a3 100644 --- a/plugins/Login/tests/UI/Decline_spec.js +++ b/plugins/Login/tests/UI/Decline_spec.js @@ -1,7 +1,7 @@ /*! * Matomo - free/libre analytics platform * - * login & password reset screenshot tests. + * Decline invitation UI tests * * @link https://matomo.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later @@ -10,16 +10,22 @@ describe('Decline', function () { this.timeout(0); this.fixture = 'Piwik\\Plugins\\Login\\tests\\Fixtures\\PendingUsers'; + this.optionsOverride = { + 'persist-fixture-data': false + }; var pendingUserUrl = '?module=Login&action=declineInvitation&token=13cb9dcef6cc70b02a640cee30dc8ce9'; - - it('should display decline invite page', async function () { await page.goto(pendingUserUrl); expect(await page.screenshot({ fullPage: true })).to.matchImage('default'); }); - - -});
\ No newline at end of file + it('should display decline success page', async function () { + await page.evaluate(function(){ + $('#login_form_submit').click(); + }); + await page.waitForNetworkIdle(); + expect(await page.screenshot({ fullPage: true })).to.matchImage('success'); + }); +}); diff --git a/plugins/Login/tests/UI/Invite_spec.js b/plugins/Login/tests/UI/Invite_spec.js index d937f2c891..decb080ff0 100644 --- a/plugins/Login/tests/UI/Invite_spec.js +++ b/plugins/Login/tests/UI/Invite_spec.js @@ -1,7 +1,7 @@ /*! * Matomo - free/libre analytics platform * - * login & password reset screenshot tests. + * Accept invitation UI tests * * @link https://matomo.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later @@ -10,11 +10,13 @@ describe('Invite', function () { this.timeout(0); this.fixture = 'Piwik\\Plugins\\Login\\tests\\Fixtures\\PendingUsers'; + this.optionsOverride = { + 'persist-fixture-data': false + }; var pendingUserUrl = '?module=Login&action=acceptInvitation&token=13cb9dcef6cc70b02a640cee30dc8ce9'; var wrongUserUrl = '?module=Login&action=acceptInvitation&token=123'; - it('should display error page', async function (){ await page.goto(wrongUserUrl); expect(await page.screenshot({ fullPage: true })).to.matchImage('error'); @@ -31,8 +33,19 @@ describe('Invite', function () { await page.evaluate(function(){ $('#login_form_submit').click(); }); + await page.waitForNetworkIdle(); expect(await page.screenshot({ fullPage: true })).to.matchImage('wrong_password'); - }); -});
\ No newline at end of file + it('it should login success', async function () { + await page.type('#password', 'abcd1234'); + await page.type('#password_confirm', 'abcd1234'); + await page.evaluate(function(){ + $('#conditionCheck').prop('checked', true); + $('#login_form_submit').click(); + }); + // should show site without data page + await page.waitForNetworkIdle(); + await page.waitForSelector('.site-without-data'); + }); +}); diff --git a/plugins/Login/tests/UI/expected-screenshots/Decline_default.png b/plugins/Login/tests/UI/expected-screenshots/Decline_default.png index fa9acf274f..a2e9c2522b 100644 --- a/plugins/Login/tests/UI/expected-screenshots/Decline_default.png +++ b/plugins/Login/tests/UI/expected-screenshots/Decline_default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4f71261c0c6e74cae66aba9d6749eb2e0852acb802349e573d35f38ad6a7861 -size 24210 +oid sha256:41aedf3327800c766eb323de2239e78a8854f73ed18bacad5a0e3c243aa4407f +size 24267 diff --git a/plugins/Login/tests/UI/expected-screenshots/Decline_success.png b/plugins/Login/tests/UI/expected-screenshots/Decline_success.png new file mode 100644 index 0000000000..e88135cd2e --- /dev/null +++ b/plugins/Login/tests/UI/expected-screenshots/Decline_success.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d92554f925821c014c4ec38647342c14595d07d060dc761256983838dd426518 +size 15961 diff --git a/plugins/Login/tests/UI/expected-screenshots/Invite_error.png b/plugins/Login/tests/UI/expected-screenshots/Invite_error.png index 5d5cc8fcd4..e9775cca1e 100644 --- a/plugins/Login/tests/UI/expected-screenshots/Invite_error.png +++ b/plugins/Login/tests/UI/expected-screenshots/Invite_error.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c273f68c05b5c8398aa2248a92633bc83b5209188b859d173d76be5a3d96f630 -size 40102 +oid sha256:6510477474c2b1bd247bb61dedb0b5d381306e1f647772628a712b1ef5361dd3 +size 40268 diff --git a/plugins/Login/tests/UI/expected-screenshots/Invite_set_password.png b/plugins/Login/tests/UI/expected-screenshots/Invite_set_password.png index 4979ed5066..cf6230982b 100644 --- a/plugins/Login/tests/UI/expected-screenshots/Invite_set_password.png +++ b/plugins/Login/tests/UI/expected-screenshots/Invite_set_password.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d59911d79ef8cf203efa7a7d64024fe66c9c655dcf6ec32900954be7d9e7b494 -size 27166 +oid sha256:2928cffb6651bf7304f3fe2b99f53570a7cba38655df2b24eac99b3a4e720c36 +size 33403 diff --git a/plugins/Login/tests/UI/expected-screenshots/Invite_wrong_password.png b/plugins/Login/tests/UI/expected-screenshots/Invite_wrong_password.png index ac1265c02a..9563f4704c 100644 --- a/plugins/Login/tests/UI/expected-screenshots/Invite_wrong_password.png +++ b/plugins/Login/tests/UI/expected-screenshots/Invite_wrong_password.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a8c371e544e1964269b1d6200e933a719a47a6b3dcba01549588dc67bb2f3ea -size 32681 +oid sha256:e984c2c1337416b377b6c1195696ad126028e9b2aa5d6ddbad4d2c3a8d2bbb8a +size 39188 |