diff --git a/dev/Boots/RainLoopApp.js b/dev/Boots/RainLoopApp.js index b770b1d44..8bf471fc3 100644 --- a/dev/Boots/RainLoopApp.js +++ b/dev/Boots/RainLoopApp.js @@ -1101,12 +1101,14 @@ RainLoopApp.prototype.bootstart = function () Plugins.runHook('rl-start-user-screens'); RL.pub('rl.bootstart-user-screens'); - if (!!RL.settingsGet('AccountSignMe')) + if (!!RL.settingsGet('AccountSignMe') && window.navigator.registerProtocolHandler) { _.delay(function () { - window.navigator.registerProtocolHandler('mailto', - window.location.protocol + '//' + window.location.host + window.location.pathname + '?mailto&to=%s', - '' + (RL.settingsGet('Title') || 'RainLoop')); + try { + window.navigator.registerProtocolHandler('mailto', + window.location.protocol + '//' + window.location.host + window.location.pathname + '?mailto&to=%s', + '' + (RL.settingsGet('Title') || 'RainLoop')); + } catch(e) {} if (RL.settingsGet('MailToEmail')) { diff --git a/dev/Storages/WebMailAjaxRemote.js b/dev/Storages/WebMailAjaxRemote.js index 1142b8542..5c09736e7 100644 --- a/dev/Storages/WebMailAjaxRemote.js +++ b/dev/Storages/WebMailAjaxRemote.js @@ -35,8 +35,9 @@ WebMailAjaxRemoteStorage.prototype.folders = function (fCallback) * @param {boolean} bSignMe * @param {string=} sLanguage * @param {string=} sAdditionalCode + * @param {boolean=} bAdditionalCodeSignMe */ -WebMailAjaxRemoteStorage.prototype.login = function (fCallback, sEmail, sLogin, sPassword, bSignMe, sLanguage, sAdditionalCode) +WebMailAjaxRemoteStorage.prototype.login = function (fCallback, sEmail, sLogin, sPassword, bSignMe, sLanguage, sAdditionalCode, bAdditionalCodeSignMe) { this.defaultRequest(fCallback, 'Login', { 'Email': sEmail, @@ -44,6 +45,7 @@ WebMailAjaxRemoteStorage.prototype.login = function (fCallback, sEmail, sLogin, 'Password': sPassword, 'Language': sLanguage || '', 'AdditionalCode': sAdditionalCode || '', + 'AdditionalCodeSignMe': bAdditionalCodeSignMe ? '1' : '0', 'SignMe': bSignMe ? '1' : '0' }); }; diff --git a/dev/Styles/Login.less b/dev/Styles/Login.less index 8f27c86c2..c247d2bfa 100644 --- a/dev/Styles/Login.less +++ b/dev/Styles/Login.less @@ -58,7 +58,7 @@ .border-radius(3px); } - .inputLoginForm, .inputEmail, .inputLogin, .inputPassword { + .inputLoginForm, .inputEmail, .inputLogin, .inputPassword, .inputAdditionalCode { font-size: 18px; height: 30px; line-height: 29px; diff --git a/dev/ViewModels/LoginViewModel.js b/dev/ViewModels/LoginViewModel.js index 2766e0256..117f58d68 100644 --- a/dev/ViewModels/LoginViewModel.js +++ b/dev/ViewModels/LoginViewModel.js @@ -19,6 +19,7 @@ function LoginViewModel() this.additionalCode.error = ko.observable(false); this.additionalCode.focused = ko.observable(false); this.additionalCode.visibility = ko.observable(false); + this.additionalCodeSignMe = ko.observable(false); this.logoImg = Utils.trim(RL.settingsGet('LoginLogo')); this.loginDescription = Utils.trim(RL.settingsGet('LoginDescription')); @@ -137,7 +138,9 @@ function LoginViewModel() }, this), this.email(), this.login(), this.password(), !!this.signMe(), this.bSendLanguage ? this.mainLanguage() : '', - this.additionalCode.visibility() ? this.additionalCode() : ''); + this.additionalCode.visibility() ? this.additionalCode() : '', + this.additionalCode.visibility() ? !!this.additionalCodeSignMe() : false + ); return true; diff --git a/package.json b/package.json index b9c24c16b..bd676dc3b 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "RainLoop", "title": "RainLoop Webmail", "version": "1.6.4", - "release": "893", + "release": "895", "description": "Simple, modern & fast web-based email client", "homepage": "http://rainloop.net", "main": "Gruntfile.js", diff --git a/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php b/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php index 538be38e1..e26036dc0 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -928,7 +928,7 @@ class Actions $sSignMeToken = \RainLoop\Utils::GetCookie(\RainLoop\Actions::AUTH_SIGN_ME_TOKEN_KEY, ''); if (!empty($sSignMeToken)) { - $oAccount = $this->oActions->GetAccountFromCustomToken($this->StorageProvider()->Get(null, + $oAccount = $this->GetAccountFromCustomToken($this->StorageProvider()->Get(null, \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, 'SignMe/UserToken/'.$sSignMeToken ), false, false); @@ -1375,12 +1375,14 @@ class Actions * @param string $sLogin * @param string $sPassword * @param string $sSignMeToken = '' - * @param string $sTwoFactorAuthCode = '' + * @param string $sAdditionalCode = '' + * @param string $bAdditionalCodeSignMeSignMe = false * * @return \RainLoop\Account * @throws \RainLoop\Exceptions\ClientException */ - public function LoginProcess(&$sEmail, &$sLogin, &$sPassword, $sSignMeToken = '', $sTwoFactorAuthCode = '') + public function LoginProcess(&$sEmail, &$sLogin, &$sPassword, $sSignMeToken = '', + $sAdditionalCode = '', $bAdditionalCodeSignMeSignMe = false) { if (false === \strpos($sEmail, '@') && 0 < \strlen(\trim($this->Config()->Get('login', 'default_domain', '')))) { @@ -1424,32 +1426,43 @@ class Actions $aData = $this->getTwoFactorInfo($oAccount->ParentEmailHelper()); if ($aData && isset($aData['IsSet'], $aData['Enable']) && !empty($aData['Secret']) && $aData['IsSet'] && $aData['Enable']) { - $sTwoFactorAuthCode = \trim($sTwoFactorAuthCode); - if (empty($sTwoFactorAuthCode)) + $iAdditionalCodeTimeoutLimit = 60 * 60 * 24 * 14; // two weeks + $iAdditionalCodeTimeout = isset($aData['Timeout']) && \is_numeric($aData['Timeout']) ? (int) $aData['Timeout'] : 0; + + if (0 === $iAdditionalCodeTimeout || $iAdditionalCodeTimeout + $iAdditionalCodeTimeoutLimit < \time()) { - $this->Logger()->Write('TwoFactorAuth: Required Code for '.$oAccount->ParentEmailHelper().' account.'); - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthRequired); - } - else - { - $this->Logger()->Write('TwoFactorAuth: Verify Code for '.$oAccount->ParentEmailHelper().' account.'); - - $bGood = false; - if (6 < \strlen($sTwoFactorAuthCode) && !empty($aData['BackupCodes'])) + $sAdditionalCode = \trim($sAdditionalCode); + if (empty($sAdditionalCode)) { - $aBackupCodes = \explode(' ', \trim(\preg_replace('/[^\d]+/', ' ', $aData['BackupCodes']))); - $bGood = \in_array($sTwoFactorAuthCode, $aBackupCodes); - - if ($bGood) - { - $this->removeBackupCodeFromTwoFactorInfo($oAccount->ParentEmailHelper(), $sTwoFactorAuthCode); - } + $this->Logger()->Write('TwoFactorAuth: Required Code for '.$oAccount->ParentEmailHelper().' account.'); + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthRequired); } - - if (!$bGood && !$this->TwoFactorAuthProvider()->VerifyCode($aData['Secret'], $sTwoFactorAuthCode)) + else { - $this->loginErrorDelay(); - throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthError); + $this->Logger()->Write('TwoFactorAuth: Verify Code for '.$oAccount->ParentEmailHelper().' account.'); + + $bGood = false; + if (6 < \strlen($sAdditionalCode) && !empty($aData['BackupCodes'])) + { + $aBackupCodes = \explode(' ', \trim(\preg_replace('/[^\d]+/', ' ', $aData['BackupCodes']))); + $bGood = \in_array($sAdditionalCode, $aBackupCodes); + + if ($bGood) + { + $this->removeBackupCodeFromTwoFactorInfo($oAccount->ParentEmailHelper(), $sAdditionalCode); + } + } + + if ($bAdditionalCodeSignMeSignMe) + { + $this->setSkipTimeoutForTwoFactor($oAccount->ParentEmailHelper()); + } + + if (!$bGood && !$this->TwoFactorAuthProvider()->VerifyCode($aData['Secret'], $sAdditionalCode)) + { + $this->loginErrorDelay(); + throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::AccountTwoFactorAuthError); + } } } } @@ -1492,9 +1505,10 @@ class Actions $sLogin = \trim($this->GetActionParam('Login', '')); $sPassword = $this->GetActionParam('Password', ''); $sLanguage = $this->GetActionParam('Language', ''); - $bSignMe = '1' === $this->GetActionParam('SignMe', '0'); + $bSignMe = '1' === (string) $this->GetActionParam('SignMe', '0'); $sAdditionalCode = $this->GetActionParam('AdditionalCode', ''); + $bAdditionalCodeSignMe = '1' === (string) $this->GetActionParam('AdditionalCodeSignMe', '0'); $this->Logger()->AddSecret($sPassword); @@ -1504,7 +1518,7 @@ class Actions { $oAccount = $this->LoginProcess($sEmail, $sLogin, $sPassword, $bSignMe ? \md5(\microtime(true).APP_SALT.\rand(10000, 99999).$sEmail) : '', - $sAdditionalCode); + $sAdditionalCode, $bAdditionalCodeSignMe); } catch (\RainLoop\Exceptions\ClientException $oException) { @@ -4631,7 +4645,8 @@ class Actions 'Enable' => false, 'Secret' => '', 'Url' => '', - 'BackupCodes' => '' + 'BackupCodes' => '', + 'Timeout' => 0 ); if (!empty($sEmail)) @@ -4657,6 +4672,7 @@ class Actions $aResult['Enable'] = isset($mData['Enable']) ? !!$mData['Enable'] : false; $aResult['Secret'] = $mData['Secret']; $aResult['BackupCodes'] = $mData['BackupCodes']; + $aResult['Timeout'] = isset($mData['Timeout']) ? (int) $mData['Timeout'] : 0; $aResult['Url'] = $this->TwoFactorAuthProvider()->GetQRCodeGoogleUrl( $aResult['User'], $aResult['Secret'], $this->Config()->Get('webmail', 'title', '')); @@ -4723,6 +4739,41 @@ class Actions return false; } + /** + * @param string $sEmail + * + * @return bool + */ + private function setSkipTimeoutForTwoFactor($sEmail) + { + if (empty($sEmail)) + { + return false; + } + + $sData = $this->StorageProvider()->Get(null, + \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, + 'TwoFactorAuth/User/'.$sEmail.'/Data/' + ); + + if ($sData) + { + $mData = \RainLoop\Utils::DecodeKeyValues($sData); + if (\is_array($mData)) + { + $mData['Timeout'] = \time(); + + return $this->StorageProvider()->Put(null, + \RainLoop\Providers\Storage\Enumerations\StorageType::NOBODY, + 'TwoFactorAuth/User/'.$sEmail.'/Data/', + \RainLoop\Utils::EncodeKeyValues($mData) + ); + } + } + + return false; + } + /** * @return array */ @@ -4767,6 +4818,7 @@ class Actions 'User' => $sEmail, 'Enable' => false, 'Secret' => $sSecret, + 'Timeout' => 0, 'BackupCodes' => \implode(' ', $aCodes) )) ); @@ -4822,6 +4874,7 @@ class Actions 'User' => $sEmail, 'Enable' => '1' === \trim($this->GetActionParam('Enable', '0')), 'Secret' => $mData['Secret'], + 'Timeout' => isset($mData['Timeout']) ? $mData['Timeout'] : 0, 'BackupCodes' => $mData['BackupCodes'] )) ); diff --git a/rainloop/v/0.0.0/app/templates/Themes/template.less b/rainloop/v/0.0.0/app/templates/Themes/template.less index 49e340e07..17f7d5093 100644 --- a/rainloop/v/0.0.0/app/templates/Themes/template.less +++ b/rainloop/v/0.0.0/app/templates/Themes/template.less @@ -66,7 +66,7 @@ .thm-border-radius(@login-border-radius); .thm-box-shadow(@login-box-shadow); - .legend, .checkboxSignMe, g-ui-link { + .legend, .checkboxSignMe, .checkboxAdditionalCodeSignMe, .g-ui-link { color: @login-color !important; } } diff --git a/rainloop/v/0.0.0/app/templates/Views/AdminSettingsSecurity.html b/rainloop/v/0.0.0/app/templates/Views/AdminSettingsSecurity.html index c00076898..17fb3e129 100644 --- a/rainloop/v/0.0.0/app/templates/Views/AdminSettingsSecurity.html +++ b/rainloop/v/0.0.0/app/templates/Views/AdminSettingsSecurity.html @@ -17,7 +17,7 @@ @@ -26,7 +26,7 @@ diff --git a/rainloop/v/0.0.0/app/templates/Views/Login.html b/rainloop/v/0.0.0/app/templates/Views/Login.html index 25d20cd4a..9451f0071 100644 --- a/rainloop/v/0.0.0/app/templates/Views/Login.html +++ b/rainloop/v/0.0.0/app/templates/Views/Login.html @@ -47,13 +47,20 @@
-
+
+ +
{{INCLUDE/BottomControlGroup/PLACE}}