diff --git a/.editorconfig b/.editorconfig index b986ce55f..91575c032 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,7 +13,7 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false -[*.{yml,js,json,css,scss,eslintrc}] +[*.{yml,js,json,css,scss,eslintrc,feature}] indent_size = 2 indent_style = space diff --git a/lang/en.yml b/lang/en.yml index 15cf4f57a..ce663fad6 100644 --- a/lang/en.yml +++ b/lang/en.yml @@ -207,19 +207,19 @@ en: ENTERINFO: 'Please enter a username and password.' ERRORNOTADMIN: 'That user is not an administrator.' ERRORNOTREC: 'That username / password isn''t recognised' - SilverStripe\Security\CMSMemberLoginForm: + SilverStripe\Security\MemberAuthenticator\CMSMemberLoginForm: AUTHENTICATORNAME: 'CMS Member Login Form' - BUTTONFORGOTPASSWORD: 'Forgot password?' - BUTTONLOGIN: 'Log back in' + BUTTONFORGOTPASSWORD: 'Forgot password' + BUTTONLOGIN: 'Let me back in' BUTTONLOGOUT: 'Log out' PASSWORDEXPIRED: '

Your password has expired. Please choose a new one.

' SilverStripe\Security\CMSSecurity: INVALIDUSER: '

Invalid user. Please re-authenticate here to continue.

' - LoginMessage: '

If you have any unsaved work you can return to where you left off by logging back in below.

' + LOGIN_MESSAGE: '

Your session has timed out due to inactivity.

' SUCCESS: Success SUCCESSCONTENT: '

Login success. If you are not automatically redirected click here

' - TimedOutTitleAnonymous: 'Your session has timed out.' - TimedOutTitleMember: 'Hey {name}!
Your session has timed out.' + LOGIN_TITLE: 'Return to where you left off by logging back in' + SUCCESS_TITLE: Success SilverStripe\Security\Group: AddRole: 'Add a role for this group' Code: 'Group Code' @@ -285,7 +285,7 @@ en: PLURALS: one: 'A Member' other: '{count} Members' - REMEMBERME: 'Remember me next time? (for %d days on this device)' + REMEMBERME: 'Remember me next time? (for {count} days on this device)' SINGULARNAME: Member SUBJECTPASSWORDCHANGED: 'Your password has been changed' SUBJECTPASSWORDRESET: 'Your password reset link' diff --git a/lang/fi.yml b/lang/fi.yml index 30e0c3b6b..04dfdcf27 100644 --- a/lang/fi.yml +++ b/lang/fi.yml @@ -234,7 +234,7 @@ fi: PLURALS: one: 'Käyttäjä' other: '{count} Käyttäjää' - REMEMBERME: 'Muista minut? (%d päivän ajan tällä koneella)' + REMEMBERME: 'Muista minut? ({count} päivän ajan tällä koneella)' SINGULARNAME: Käyttäjä SUBJECTPASSWORDCHANGED: 'Salasanasi on vaihdettu' SUBJECTPASSWORDRESET: 'Salasanasi palautuslinkki' diff --git a/src/Security/CMSSecurity.php b/src/Security/CMSSecurity.php index 826da9a89..16cdcb6cf 100644 --- a/src/Security/CMSSecurity.php +++ b/src/Security/CMSSecurity.php @@ -5,22 +5,21 @@ namespace SilverStripe\Security; use SilverStripe\Admin\AdminRootController; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; +use SilverStripe\Admin\LeftAndMain; use SilverStripe\Control\HTTPResponse; use SilverStripe\Control\Session; use SilverStripe\Core\Convert; +use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\ORM\FieldType\DBField; +use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\View\Requirements; +use SilverStripe\View\SSViewer; /** * Provides a security interface functionality within the cms */ class CMSSecurity extends Security { - - private static $casting = array( - 'Title' => 'HTMLFragment' - ); - private static $allowed_actions = array( 'login', 'LoginForm', @@ -39,7 +38,13 @@ class CMSSecurity extends Security { parent::init(); - Requirements::javascript(FRAMEWORK_ADMIN_DIR . '/client/dist/js/vendor.js'); + // Assign default cms theme and replace user-specified themes + SSViewer::set_themes(LeftAndMain::config()->uninherited('admin_themes')); + + // Core styles / vendor scripts + $admin = ModuleLoader::getModule('silverstripe/admin'); + Requirements::javascript($admin->getResourcePath('client/dist/js/vendor.js')); + Requirements::css($admin->getResourcePath('client/dist/styles/bundle.css')); } public function login($request = null, $service = Authenticator::CMS_LOGIN) @@ -70,7 +75,8 @@ class CMSSecurity extends Security */ public function getTargetMember() { - if ($tempid = $this->getRequest()->requestVar('tempid')) { + $tempid = $this->getRequest()->requestVar('tempid'); + if ($tempid) { return Member::member_from_tempid($tempid); } @@ -85,36 +91,26 @@ class CMSSecurity extends Security protected function getLoginMessage(&$messageType = null) { - return parent::getLoginMessage($messageType) - ?: _t( - 'SilverStripe\\Security\\CMSSecurity.LoginMessage', - '

If you have any unsaved work you can return to where you left off by logging back in below.

' - ); + $message = parent::getLoginMessage($messageType); + if ($message) { + return $message; + } + + // Format + return _t( + __CLASS__.'.LOGIN_MESSAGE', + '

Your session has timed out due to inactivity

' + ); } - public function getTitle() + /** + * Check if there is a logged in member + * + * @return bool + */ + public function getIsloggedIn() { - // Check if logged in already - if (Security::getCurrentUser()) { - return _t('SilverStripe\\Security\\CMSSecurity.SUCCESS', 'Success'); - } - - // Display logged-out message - $member = $this->getTargetMember(); - if ($member) { - return _t( - 'SilverStripe\\Security\\CMSSecurity.TimedOutTitleMember', - 'Hey {name}!
Your session has timed out.', - 'Title for CMS popup login form for a known user', - array('name' => $member->FirstName) - ); - } else { - return _t( - 'SilverStripe\\Security\\CMSSecurity.TimedOutTitleAnonymous', - 'Your session has timed out.', - 'Title for CMS popup login form without a known user' - ); - } + return !!Security::getCurrentUser(); } /** @@ -128,7 +124,7 @@ class CMSSecurity extends Security $loginURLATT = Convert::raw2att($loginURL); $loginURLJS = Convert::raw2js($loginURL); $message = _t( - 'SilverStripe\\Security\\CMSSecurity.INVALIDUSER', + __CLASS__.'.INVALIDUSER', '

Invalid user. Please re-authenticate here to continue.

', 'Message displayed to user if their session cannot be restored', array('link' => $loginURLATT) @@ -188,7 +184,7 @@ PHP } // Get redirect url - $controller = $this->getResponseController(_t('SilverStripe\\Security\\CMSSecurity.SUCCESS', 'Success')); + $controller = $this->getResponseController(_t(__CLASS__.'.SUCCESS', 'Success')); $backURLs = array( $this->getRequest()->requestVar('BackURL'), Session::get('BackURL'), @@ -203,13 +199,13 @@ PHP // Show login $controller = $controller->customise(array( - 'Content' => _t( - 'SilverStripe\\Security\\CMSSecurity.SUCCESSCONTENT', - '

Login success. If you are not automatically redirected ' . + 'Content' => DBField::create_field(DBHTMLText::class, _t( + __CLASS__.'.SUCCESSCONTENT', + '

Login success. If you are not automatically redirected '. 'click here

', 'Login message displayed in the cms popup once a user has re-authenticated themselves', array('link' => Convert::raw2att($backURL)) - ) + )) )); return $controller->renderWith($this->getTemplatesFor('success')); diff --git a/src/Security/MemberAuthenticator/CMSLoginHandler.php b/src/Security/MemberAuthenticator/CMSLoginHandler.php index f30cdb7db..e976d1e5d 100644 --- a/src/Security/MemberAuthenticator/CMSLoginHandler.php +++ b/src/Security/MemberAuthenticator/CMSLoginHandler.php @@ -2,6 +2,7 @@ namespace SilverStripe\Security\MemberAuthenticator; +use SilverStripe\Control\Director; use SilverStripe\Control\HTTPResponse; use SilverStripe\Core\Convert; use SilverStripe\Security\CMSSecurity; @@ -28,10 +29,20 @@ class CMSLoginHandler extends LoginHandler public function redirectBackToForm() { // Redirect back to form - $url = $this->addBackURLParam(CMSSecurity::singleton()->Link('login')); + $url = $this->addBackURLParam($this->getReturnReferer()); return $this->redirect($url); } + public function getReturnReferer() + { + // Try to retain referer (includes tempid param) + $referer = $this->getReferer(); + if ($referer && Director::is_site_url($referer)) { + return $referer; + } + return CMSSecurity::singleton()->Link('login'); + } + /** * Redirect the user to the change password form. * diff --git a/src/Security/CMSMemberLoginForm.php b/src/Security/MemberAuthenticator/CMSMemberLoginForm.php similarity index 66% rename from src/Security/CMSMemberLoginForm.php rename to src/Security/MemberAuthenticator/CMSMemberLoginForm.php index 197240037..8e5c8d944 100644 --- a/src/Security/CMSMemberLoginForm.php +++ b/src/Security/MemberAuthenticator/CMSMemberLoginForm.php @@ -4,12 +4,14 @@ namespace SilverStripe\Security\MemberAuthenticator; use SilverStripe\Control\Controller; use SilverStripe\Control\RequestHandler; +use SilverStripe\Core\Convert; use SilverStripe\Forms\CheckboxField; use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FormAction; use SilverStripe\Forms\HiddenField; use SilverStripe\Forms\LiteralField; use SilverStripe\Forms\PasswordField; +use SilverStripe\Security\RememberLoginHash; use SilverStripe\Security\Security; /** @@ -35,6 +37,8 @@ class CMSMemberLoginForm extends MemberLoginForm $actions = $this->getFormActions(); parent::__construct($controller, $authenticatorClass, $name, $fields, $actions); + + $this->addExtraClass('form--no-dividers'); } /** @@ -46,22 +50,24 @@ class CMSMemberLoginForm extends MemberLoginForm $fields = FieldList::create([ HiddenField::create("AuthenticationMethod", null, $this->authenticator_class, $this), HiddenField::create('tempid', null, $this->controller->getRequest()->requestVar('tempid')), - PasswordField::create("Password", _t('SilverStripe\\Security\\Member.PASSWORD', 'Password')), - LiteralField::create( - 'forgotPassword', - sprintf( - '

%s

', - $this->getExternalLink('lostpassword'), - _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONFORGOTPASSWORD', "Forgot password?") - ) - ) + PasswordField::create("Password", _t('SilverStripe\\Security\\Member.PASSWORD', 'Password')) ]); if (Security::config()->get('autologin_enabled')) { - $fields->push(CheckboxField::create( - "Remember", - _t('SilverStripe\\Security\\Member.REMEMBERME', "Remember me next time?") - )); + $fields->insertAfter( + 'Password', + CheckboxField::create( + "Remember", + _t('SilverStripe\\Security\\Member.KEEPMESIGNEDIN', "Keep me signed in") + )->setAttribute( + 'title', + _t( + 'SilverStripe\\Security\\Member.REMEMBERME', + "Remember me next time? (for {count} days on this device)", + [ 'count' => RememberLoginHash::config()->uninherited('token_expiry_days') ] + ) + ) + ); } return $fields; @@ -72,7 +78,6 @@ class CMSMemberLoginForm extends MemberLoginForm */ public function getFormActions() { - // Determine returnurl to redirect to parent page $logoutLink = $this->getExternalLink('logout'); if ($returnURL = $this->controller->getRequest()->requestVar('BackURL')) { @@ -81,13 +86,22 @@ class CMSMemberLoginForm extends MemberLoginForm // Make actions $actions = FieldList::create([ - FormAction::create('doLogin', _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGIN', "Log back in")), + FormAction::create('doLogin', _t(__CLASS__.'.BUTTONLOGIN', "Let me back in")) + ->addExtraClass('btn-primary'), LiteralField::create( 'doLogout', sprintf( - '

%s

', - $logoutLink, - _t('SilverStripe\\Security\\CMSMemberLoginForm.BUTTONLOGOUT', "Log out") + '%s', + Convert::raw2att($logoutLink), + _t(__CLASS__.'.BUTTONLOGOUT', "Log out") + ) + ), + LiteralField::create( + 'forgotPassword', + sprintf( + '

%s

', + $this->getExternalLink('lostpassword'), + _t(__CLASS__.'.BUTTONFORGOTPASSWORD', "Forgot password") ) ) ]); @@ -111,6 +125,6 @@ class CMSMemberLoginForm extends MemberLoginForm */ public function getAuthenticatorName() { - return _t('SilverStripe\\Security\\CMSMemberLoginForm.AUTHENTICATORNAME', 'CMS Member Login Form'); + return _t(__CLASS__.'.AUTHENTICATORNAME', 'CMS Member Login Form'); } } diff --git a/src/Security/MemberAuthenticator/MemberLoginForm.php b/src/Security/MemberAuthenticator/MemberLoginForm.php index b6b7e8809..b56fe8f25 100644 --- a/src/Security/MemberAuthenticator/MemberLoginForm.php +++ b/src/Security/MemberAuthenticator/MemberLoginForm.php @@ -156,9 +156,10 @@ class MemberLoginForm extends BaseLoginForm _t('SilverStripe\\Security\\Member.KEEPMESIGNEDIN', "Keep me signed in") )->setAttribute( 'title', - sprintf( - _t('SilverStripe\\Security\\Member.REMEMBERME', "Remember me next time? (for %d days on this device)"), - RememberLoginHash::config()->uninherited('token_expiry_days') + _t( + 'SilverStripe\\Security\\Member.REMEMBERME', + "Remember me next time? (for {count} days on this device)", + [ 'count' => RememberLoginHash::config()->uninherited('token_expiry_days') ] ) ) ); diff --git a/src/Security/Security.php b/src/Security/Security.php index 2c39d18c0..3d1bff936 100644 --- a/src/Security/Security.php +++ b/src/Security/Security.php @@ -25,7 +25,6 @@ use SilverStripe\ORM\DB; use SilverStripe\ORM\FieldType\DBField; use SilverStripe\ORM\FieldType\DBHTMLText; use SilverStripe\ORM\ValidationResult; -use SilverStripe\Security\DefaultAdminService; use SilverStripe\View\ArrayData; use SilverStripe\View\SSViewer; use SilverStripe\View\TemplateGlobalProvider; @@ -241,7 +240,7 @@ class Security extends Controller implements TemplateGlobalProvider public function index() { - return $this->httpError(404); // no-op + $this->httpError(404); // no-op } /** @@ -358,7 +357,7 @@ class Security extends Controller implements TemplateGlobalProvider $messageSet = $configMessageSet; } else { $messageSet = array( - 'default' => _t( + 'default' => _t( 'SilverStripe\\Security\\Security.NOTEPAGESECURED', "That page is secured. Enter your credentials below and we will send " . "you right along." @@ -608,6 +607,10 @@ class Security extends Controller implements TemplateGlobalProvider */ protected function generateLoginFormSet($forms) { + if (count($forms) === 1) { + return $forms; + } + $viewData = new ArrayData(array( 'Forms' => new ArrayList($forms), )); @@ -773,6 +776,7 @@ class Security extends Controller implements TemplateGlobalProvider return $this->renderWrappedController( $title, [ + 'Forms' => ArrayList::create($forms), 'Form' => $this->generateLoginFormSet($forms), ], $templates @@ -1086,12 +1090,12 @@ class Security extends Controller implements TemplateGlobalProvider // New salts will only need to be generated if the password is hashed for the first time $salt = ($salt) ? $salt : $encryptor->salt($password); - return array( + return [ 'password' => $encryptor->encrypt($password, $salt, $member), - 'salt' => $salt, + 'salt' => $salt, 'algorithm' => $algorithm, 'encryptor' => $encryptor - ); + ]; } /** @@ -1238,12 +1242,12 @@ class Security extends Controller implements TemplateGlobalProvider */ public static function get_template_global_variables() { - return array( - "LoginURL" => "login_url", - "LogoutURL" => "logout_url", + return [ + "LoginURL" => "login_url", + "LogoutURL" => "logout_url", "LostPasswordURL" => "lost_password_url", - "CurrentMember" => "getCurrentUser", - "currentUser" => "getCurrentUser" - ); + "CurrentMember" => "getCurrentUser", + "currentUser" => "getCurrentUser" + ]; } } diff --git a/templates/SilverStripe/Security/CMSSecurity_login.ss b/templates/SilverStripe/Security/CMSSecurity_login.ss new file mode 100644 index 000000000..a4834f0b6 --- /dev/null +++ b/templates/SilverStripe/Security/CMSSecurity_login.ss @@ -0,0 +1,34 @@ + + + + <% base_tag %> + <%t SilverStripe\\Security\\CMSSecurity.LOGIN_TITLE 'Return to where you left off by logging back in' %> + + + <% with $Form %> + <% if $Message %> +
+

$Message

+
+ <% end_if %> + <% end_with %> +
+
+

+ + <%t SilverStripe\\Security\\CMSSecurity.LOGIN_TITLE 'Return to where you left off by logging back in' %> +

+
+ <% if $Content %> +
+
$Content
+
+ <% end_if %> +
+
+ $Form +
+
+
+ + diff --git a/templates/SilverStripe/Security/CMSSecurity_success.ss b/templates/SilverStripe/Security/CMSSecurity_success.ss new file mode 100644 index 000000000..46deba316 --- /dev/null +++ b/templates/SilverStripe/Security/CMSSecurity_success.ss @@ -0,0 +1,55 @@ + + + + <% base_tag %> + <%t SilverStripe\\Security\\CMSSecurity.SUCCESS_TITLE 'Login successful' %> + + +
+
+

+ + <%t SilverStripe\\Security\\CMSSecurity.SUCCESS_TITLE 'Login successful' %> +

+
+ <% if $Content %> +
+
$Content
+
+ <% end_if %> +
+
+ $Form +
+
+
+ + + + diff --git a/templates/SilverStripe/Security/MemberAuthenticator/Includes/CMSMemberLoginForm.ss b/templates/SilverStripe/Security/MemberAuthenticator/Includes/CMSMemberLoginForm.ss new file mode 100644 index 000000000..319a9bf50 --- /dev/null +++ b/templates/SilverStripe/Security/MemberAuthenticator/Includes/CMSMemberLoginForm.ss @@ -0,0 +1,21 @@ +<% if $IncludeFormTag %> +
+<% end_if %> +
+ <% if $Legend %>$Legend<% end_if %> + <% loop $Fields %> + $FieldHolder + <% end_loop %> +
+
+ + <% if $Actions %> +
+ <% loop $Actions %> + $Field + <% end_loop %> +
+ <% end_if %> +<% if $IncludeFormTag %> +
+<% end_if %> diff --git a/tests/behat/features/reauthenticate.feature b/tests/behat/features/reauthenticate.feature new file mode 100644 index 000000000..15b648e9e --- /dev/null +++ b/tests/behat/features/reauthenticate.feature @@ -0,0 +1,35 @@ +@modal @retry +Feature: Reauthenticate + As a content editor + I want to be able to log in through a CMS popup when my session expires + So that I can avoid losing unsaved work + + Background: + And I am logged in with "ADMIN" permissions + And I go to "/admin/security" + And I am not in an iframe + And I click the "Users" CMS tab + And my session expires + + Scenario: Reauthenticate with correct login + When I press the "Add Member" button + And I switch to the "login-dialog-iframe" iframe + Then I should see "Your session has timed out due to inactivity" in the ".cms-security__container" element + When I fill in "Password" with "Secret!123" + And I press the "Let me back in" button + And I am not in an iframe + And I click "ADMIN" in the "#Root_Users" element + Then I should see "Save" in the "#Form_ItemEditForm_action_doSave" element + + Scenario: Reauthenticate with wrong login + When I press the "Add Member" button + And I switch to the "login-dialog-iframe" iframe + Then I should see "Your session has timed out due to inactivity" in the ".cms-security__container" element + When I fill in "Password" with "wrong password" + And I press the "Let me back in" button + Then I should see "The provided details don't seem to be correct. Please try again." + When I fill in "Password" with "Secret!123" + And I press the "Let me back in" button + And I am not in an iframe + And I click "ADMIN" in the "#Root_Users" element + Then I should see "Save" in the "#Form_ItemEditForm_action_doSave" element diff --git a/tests/behat/src/CmsFormsContext.php b/tests/behat/src/CmsFormsContext.php index 6fad73bba..5dee3efa4 100644 --- a/tests/behat/src/CmsFormsContext.php +++ b/tests/behat/src/CmsFormsContext.php @@ -293,4 +293,31 @@ JS; throw new BadMethodCallException("Invalid condition"); } } + + /** + * @When /^I switch to the "([^"]*)" iframe$/ + * @param string $id iframe id property + */ + public function stepSwitchToTheFrame($id) + { + $this->getMainContext()->getSession()->getDriver()->switchToIFrame($id); + } + + /** + * @When /^I am not in an iframe$/ + */ + public function stepSwitchToParentFrame() + { + $this->getMainContext()->getSession()->getDriver()->switchToIFrame(null); + } + + /** + * @When /^my session expires$/ + */ + public function stepMySessionExpires() + { + // Destroy cookie to detach session + $this->getMainContext()->getSession()->setCookie('PHPSESSID', null); + } + }