diff --git a/dev/App/User.js b/dev/App/User.js index 6cf839026..a67032d2c 100644 --- a/dev/App/User.js +++ b/dev/App/User.js @@ -1219,6 +1219,11 @@ kn.showScreenPopup(require('View/Popup/TwoFactorConfiguration'), [true]); }; + AppUser.prototype.bootstartWelcomePopup = function (sUrl) + { + kn.showScreenPopup(require('View/Popup/WelcomePage'), [sUrl]); + }; + AppUser.prototype.bootstartLoginScreen = function () { Globals.$html.removeClass('rl-user-auth').addClass('rl-user-no-auth'); @@ -1416,6 +1421,13 @@ Plugins.runHook('rl-start-user-screens'); Events.pub('rl.bootstart-user-screens'); + if (Settings.settingsGet('WelcomePageUrl')) + { + _.delay(function () { + self.bootstartWelcomePopup(Settings.settingsGet('WelcomePageUrl')); + }, 1000); + } + if (!!Settings.settingsGet('AccountSignMe') && window.navigator.registerProtocolHandler) { _.delay(function () { diff --git a/dev/Component/Script.js b/dev/Component/Script.js index 3e4ab8c75..9a1b01d33 100644 --- a/dev/Component/Script.js +++ b/dev/Component/Script.js @@ -5,6 +5,7 @@ var _ = require('_'), + $ = require('$'), AbstractComponent = require('Component/Abstract') ; @@ -20,13 +21,26 @@ { AbstractComponent.call(this); - if (oParams.component && oParams.component.templateNodes && oParams.element) + if (oParams.component && oParams.component.templateNodes && oParams.element && + oParams.element[0] && oParams.element[0].outerHTML) { - oParams.element.text(''); - if (oParams.component.templateNodes[0] && oParams.component.templateNodes[0].nodeValue) + var sScript = oParams.element[0].outerHTML; + sScript = sScript + .replace(/<\/b><\/x-script>/i, '') + ; + + if (sScript) { + oParams.element.text(''); oParams.element.replaceWith( - $('').text(oParams.component.templateNodes[0].nodeValue)); + $(sScript).text(oParams.component.templateNodes[0] && + oParams.component.templateNodes[0].nodeValue ? + oParams.component.templateNodes[0].nodeValue : '')); + } + else + { + oParams.element.remove(); } } } diff --git a/dev/External/ko.js b/dev/External/ko.js index 189d7fb98..612f0b1e3 100644 --- a/dev/External/ko.js +++ b/dev/External/ko.js @@ -57,16 +57,16 @@ 'init': function (oElement, fValueAccessor) { var - $oEl = null, bi18n = true, sValue = '', Translator = null, + $oEl = $(oElement), + bMobile = 'on' === ($oEl.data('tooltip-mobile') || 'off'), Globals = require('Common/Globals') ; - if (!Globals.bMobileDevice) + if (!Globals.bMobileDevice || bMobile) { - $oEl = $(oElement); bi18n = 'on' === ($oEl.data('tooltip-i18n') || 'on'); sValue = bi18n ? ko.unwrap(fValueAccessor()) : fValueAccessor()(); @@ -74,6 +74,7 @@ { oElement.__opentip = new Opentip(oElement, { 'style': 'rainloopTip', + 'showOn': 'mouseover click', 'element': oElement, 'tipJoint': $oEl.data('tooltip-join') || 'bottom' }); @@ -120,12 +121,14 @@ var bi18n = true, sValue = '', + $oEl = $(oElement), + bMobile = 'on' === ($oEl.data('tooltip-mobile') || 'off'), Globals = require('Common/Globals') ; - if (!Globals.bMobileDevice && oElement.__opentip) + if ((!Globals.bMobileDevice || bMobile) && oElement.__opentip) { - bi18n = 'on' === ($(oElement).data('tooltip-i18n') || 'on'); + bi18n = 'on' === ($oEl.data('tooltip-i18n') || 'on'); sValue = bi18n ? ko.unwrap(fValueAccessor()) : fValueAccessor()(); if (sValue) @@ -150,6 +153,8 @@ oElement.__opentip = new Opentip(oElement, { 'style': 'rainloopTestTip', + 'showOn': 'mouseover click', + 'hideOn': 'mouseout click', 'element': oElement, 'tipJoint': $oEl.data('tooltip-join') || 'top' }); @@ -183,21 +188,20 @@ } else { - if ($oEl.is(':visible')) - { - oOpenTips.activate(); - oOpenTips.setContent(sValue); - - _.delay(function () { + _.delay(function () { + if ($oEl.is(':visible')) + { + oOpenTips.activate(); + oOpenTips.setContent(sValue); oOpenTips.show(); - }, 100); - } - else - { - oOpenTips.hide(); - oOpenTips.setContent(''); - oOpenTips.deactivate(); - } + } + else + { + oOpenTips.hide(); + oOpenTips.setContent(''); + oOpenTips.deactivate(); + } + }, 100); } } } diff --git a/dev/Promises/User/Ajax.js b/dev/Promises/User/Ajax.js index 3f566c05e..4051aa901 100644 --- a/dev/Promises/User/Ajax.js +++ b/dev/Promises/User/Ajax.js @@ -89,7 +89,6 @@ }, 500); }; - UserAjaxUserPromises.prototype.folderDelete = function (sFolderFullNameRaw, fTrigger) { return this.postRequest('FolderDelete', fTrigger, { @@ -113,6 +112,11 @@ }); }; + UserAjaxUserPromises.prototype.welcomeClose = function () + { + return this.postRequest('WelcomeClose'); + }; + // UserAjaxUserPromises.prototype.messageList = function (sFolderFullNameRaw, iOffset, iLimit, sSearch, fTrigger) // { // sFolderFullNameRaw = Utils.pString(sFolderFullNameRaw); diff --git a/dev/Settings/Admin/Branding.js b/dev/Settings/Admin/Branding.js index 91c0a4ee1..a5f99570a 100644 --- a/dev/Settings/Admin/Branding.js +++ b/dev/Settings/Admin/Branding.js @@ -7,7 +7,8 @@ _ = require('_'), ko = require('ko'), - Utils = require('Common/Utils') + Utils = require('Common/Utils'), + Translator = require('Common/Translator') ; /** @@ -47,6 +48,21 @@ this.userCss = ko.observable(Settings.settingsGet('UserCss')); this.userCss.trigger = ko.observable(Enums.SaveSettingsStep.Idle); + this.welcomePageUrl = ko.observable(Settings.settingsGet('WelcomePageUrl')); + this.welcomePageUrl.trigger = ko.observable(Enums.SaveSettingsStep.Idle); + + this.welcomePageDisplay = ko.observable(Settings.settingsGet('WelcomePageDisplay')); + this.welcomePageDisplay.trigger = ko.observable(Enums.SaveSettingsStep.Idle); + + this.welcomePageDisplay.options = ko.computed(function () { + Translator.trigger(); + return [ + {'optValue': 'none', 'optText': Translator.i18n('TAB_BRANDING/OPTION_WELCOME_PAGE_DISPLAY_NONE')}, + {'optValue': 'once', 'optText': Translator.i18n('TAB_BRANDING/OPTION_WELCOME_PAGE_DISPLAY_ONCE')}, + {'optValue': 'always', 'optText': Translator.i18n('TAB_BRANDING/OPTION_WELCOME_PAGE_DISPLAY_ALWAYS')} + ]; + }); + this.loginPowered = ko.observable(!!Settings.settingsGet('LoginPowered')); } @@ -69,7 +85,9 @@ f5 = Utils.settingsSaveHelperSimpleFunction(self.loginCss.trigger, self), f6 = Utils.settingsSaveHelperSimpleFunction(self.userLogo.trigger, self), f7 = Utils.settingsSaveHelperSimpleFunction(self.loginBackground.trigger, self), - f8 = Utils.settingsSaveHelperSimpleFunction(self.userCss.trigger, self) + f8 = Utils.settingsSaveHelperSimpleFunction(self.userCss.trigger, self), + f9 = Utils.settingsSaveHelperSimpleFunction(self.welcomePageUrl.trigger, self), + f10 = Utils.settingsSaveHelperSimpleFunction(self.welcomePageDisplay.trigger, self) ; self.title.subscribe(function (sValue) { @@ -120,6 +138,18 @@ }); }); + self.welcomePageUrl.subscribe(function (sValue) { + Remote.saveAdminConfig(f9, { + 'WelcomePageUrl': Utils.trim(sValue) + }); + }); + + self.welcomePageDisplay.subscribe(function (sValue) { + Remote.saveAdminConfig(f10, { + 'WelcomePageDisplay': Utils.trim(sValue) + }); + }); + self.loginPowered.subscribe(function (bValue) { Remote.saveAdminConfig(null, { 'LoginPowered': bValue ? '1' : '0' diff --git a/dev/Storage/Settings.js b/dev/Storage/Settings.js index 3fecebfc2..ef12c2a8f 100644 --- a/dev/Storage/Settings.js +++ b/dev/Storage/Settings.js @@ -16,6 +16,7 @@ { this.oSettings = window['rainloopAppData'] || {}; this.oSettings = Utils.isNormal(this.oSettings) ? this.oSettings : {}; +// window.console.log(this.oSettings); } SettingsStorage.prototype.oSettings = null; diff --git a/dev/Styles/@Main.less b/dev/Styles/@Main.less index e6fe214f2..9e94d2306 100644 --- a/dev/Styles/@Main.less +++ b/dev/Styles/@Main.less @@ -54,6 +54,7 @@ @import "Components.less"; @import "SystemDropDown.less"; @import "Login.less"; +@import "WelcomPage.less"; @import "Ask.less"; @import "Shortcuts.less"; @import "FolderList.less"; diff --git a/dev/Styles/Admin.less b/dev/Styles/Admin.less index d2eb87986..12a30c2b4 100644 --- a/dev/Styles/Admin.less +++ b/dev/Styles/Admin.less @@ -103,5 +103,9 @@ .legend { width: 670px; } + + .nav-tabs { + width: 670px; + } } } diff --git a/dev/Styles/WelcomPage.less b/dev/Styles/WelcomPage.less new file mode 100644 index 000000000..731faab8b --- /dev/null +++ b/dev/Styles/WelcomPage.less @@ -0,0 +1,9 @@ +.popups { + .b-welcom-page-content { + + .modal-header { + background-color: #fff; + } + + } +} diff --git a/dev/View/Popup/Domain.js b/dev/View/Popup/Domain.js index 92b6da99f..b981f7949 100644 --- a/dev/View/Popup/Domain.js +++ b/dev/View/Popup/Domain.js @@ -162,6 +162,7 @@ this.testingDone(false); this.testingImapError(false); + this.testingSieveError(false); this.testingSmtpError(false); this.testing(true); @@ -295,26 +296,29 @@ this.testingDone(true); this.testingImapError(true !== oData.Result.Imap); - this.testingSmtpError(true !== oData.Result.Smtp); this.testingSieveError(true !== oData.Result.Sieve); + this.testingSmtpError(true !== oData.Result.Smtp); if (this.testingImapError() && oData.Result.Imap) { bImap = true; + this.testingImapErrorDesc(''); this.testingImapErrorDesc(oData.Result.Imap); } - if (this.testingSmtpError() && oData.Result.Smtp) - { - this.testingSmtpErrorDesc(oData.Result.Smtp); - } - if (this.testingSieveError() && oData.Result.Sieve) { bSieve = true; + this.testingSieveErrorDesc(''); this.testingSieveErrorDesc(oData.Result.Sieve); } + if (this.testingSmtpError() && oData.Result.Smtp) + { + this.testingSmtpErrorDesc(''); + this.testingSmtpErrorDesc(oData.Result.Smtp); + } + if (this.sieveSettings()) { if (!bSieve && bImap) @@ -333,8 +337,8 @@ else { this.testingImapError(true); - this.testingSmtpError(true); this.testingSieveError(true); + this.testingSmtpError(true); this.sieveSettings(false); } }; @@ -365,8 +369,8 @@ this.testing(false); this.testingDone(false); this.testingImapError(false); - this.testingSmtpError(false); this.testingSieveError(false); + this.testingSmtpError(false); }; DomainPopupView.prototype.onHide = function () diff --git a/dev/View/Popup/WelcomePage.js b/dev/View/Popup/WelcomePage.js new file mode 100644 index 000000000..3dd18a686 --- /dev/null +++ b/dev/View/Popup/WelcomePage.js @@ -0,0 +1,62 @@ + +(function () { + + 'use strict'; + + var + _ = require('_'), + ko = require('ko'), + + Promises = require('Promises/User/Ajax'), + + kn = require('Knoin/Knoin'), + AbstractView = require('Knoin/AbstractView') + ; + + /** + * @constructor + * @extends AbstractView + */ + function WelcomePagePopupView() + { + AbstractView.call(this, 'Popups', 'PopupsWelcomePage'); + + this.welcomePageURL = ko.observable(''); + + this.closeFocused = ko.observable(false); + + kn.constructorEnd(this); + } + + kn.extendAsViewModel(['View/Popup/WelcomePage', 'WelcomePagePopupViewModel'], WelcomePagePopupView); + _.extend(WelcomePagePopupView.prototype, AbstractView.prototype); + + WelcomePagePopupView.prototype.clearPopup = function () + { + this.welcomePageURL(''); + this.closeFocused(false); + }; + + /** + * @param {string} sUrl + */ + WelcomePagePopupView.prototype.onShow = function (sUrl) + { + this.clearPopup(); + + this.welcomePageURL(sUrl); + }; + + WelcomePagePopupView.prototype.onShowWithDelay = function () + { + this.closeFocused(true); + }; + + WelcomePagePopupView.prototype.onHide = function () + { + Promises.welcomeClose(); + }; + + module.exports = WelcomePagePopupView; + +}()); \ No newline at end of file diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index 2fffa593d..011e66dfc 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -843,7 +843,7 @@ ; // exit fullscreen, back - key('esc', Enums.KeyState.MessageView, _.bind(this.escShortcuts, this)); + key('esc, backspace', Enums.KeyState.MessageView, _.bind(this.escShortcuts, this)); // fullscreen key('enter', Enums.KeyState.MessageView, function () { diff --git a/gulpfile.js b/gulpfile.js index d60eeb45d..9f4902d65 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -121,7 +121,7 @@ cfg.paths.js = { openpgp: { name: 'openpgp.js', src: [ - 'vendors/openpgp/openpgp-0.10.1.min.js' + 'vendors/openpgp/openpgp-0.7.2.min.js' ] }, encrypt: { diff --git a/package.json b/package.json index bb35f6ad7..26a11b6fc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "RainLoop", "title": "RainLoop Webmail", "version": "1.8.3", - "release": "295", + "release": "297", "description": "Simple, modern & fast web-based email client", "homepage": "http://rainloop.net", "main": "gulpfile.js", diff --git a/rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php b/rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php index ab2601e51..24c91605c 100644 --- a/rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php +++ b/rainloop/v/0.0.0/app/libraries/MailSo/Base/HtmlUtils.php @@ -232,7 +232,7 @@ class HtmlUtils { if ($oElement) { - if (\preg_match('/http[s]?:\/\//i', $sUrl)) + if (\preg_match('/http[s]?:\/\//i', $sUrl) || '//' === \substr($sUrl, 0, 2)) { $bHasExternals = true; if (!$bDoNotReplaceExternalUrl) @@ -452,7 +452,7 @@ class HtmlUtils foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) { if (\in_array(\strtolower($oElement->tagName), array('svg', 'head', 'link', - 'base', 'meta', 'title', 'style', 'script', 'bgsound', 'keygen', 'source', + 'base', 'meta', 'title', 'style', 'x-script', 'script', 'bgsound', 'keygen', 'source', 'object', 'embed', 'applet', 'mocha', 'iframe', 'frame', 'frameset', 'video', 'audio')) && isset($oElement->parentNode)) { @$oElement->parentNode->removeChild($oElement); @@ -546,8 +546,8 @@ class HtmlUtils // } foreach (array( - 'id', 'class', 'contenteditable', 'designmode', 'formaction', 'data-bind', 'xmlns', - 'srcset' + 'id', 'class', 'contenteditable', 'designmode', 'formaction', + 'data-bind', 'xmlns', 'srcset' ) as $sAttr) { @$oElement->removeAttribute($sAttr); @@ -566,7 +566,8 @@ class HtmlUtils if ($oElement->hasAttribute('href')) { $sHref = \trim($oElement->getAttribute('href')); - if (!\preg_match('/^(http[s]?|ftp|skype|mailto):/i', $sHref)) + if (!\preg_match('/^(http[s]?|ftp|skype|mailto):/i', $sHref) || + '//' !== \substr($sHref, 0, 2)) { $oElement->setAttribute('data-x-broken-href', $sHref); $oElement->setAttribute('href', 'javascript:false'); @@ -594,7 +595,8 @@ class HtmlUtils } else { - if (\preg_match('/http[s]?:\/\//i', $sSrc)) + if (\preg_match('/^http[s]?:\/\//i', $sSrc) || + '//' === \substr($sSrc, 0, 2)) { if ($bDoNotReplaceExternalUrl) { @@ -615,7 +617,7 @@ class HtmlUtils $bHasExternals = true; } - else if ('data:image/' === \strtolower(\substr(\trim($sSrc), 0, 11))) + else if ('data:image/' === \strtolower(\substr($sSrc, 0, 11))) { $oElement->setAttribute('src', $sSrc); } @@ -745,7 +747,7 @@ class HtmlUtils { $oElement->removeAttribute('data-x-additional-src'); } - + if ($oElement->hasAttribute('data-x-additional-style-url')) { $oElement->removeAttribute('data-x-additional-style-url'); 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 3c02a4bf1..db71ce86b 100644 --- a/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/rainloop/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -1286,6 +1286,8 @@ class Actions 'LoginCss' => '', 'UserLogo' => '', 'UserCss' => '', + 'WelcomePageUrl' => '', + 'WelcomePageDisplay' => 'none', 'IncludeCss' => '', 'IncludeBackground' => '', 'Token' => $oConfig->Get('security', 'csrf_protection', false) ? \RainLoop\Utils::GetCsrfToken() : '', @@ -1352,6 +1354,8 @@ class Actions $aResult['LoginPowered'] = !!$oConfig->Get('branding', 'login_powered', true); $aResult['UserLogo'] = $oConfig->Get('branding', 'user_logo', ''); $aResult['UserCss'] = $oConfig->Get('branding', 'user_css', ''); + $aResult['WelcomePageUrl'] = $oConfig->Get('branding', 'welcome_page_url', ''); + $aResult['WelcomePageDisplay'] = \strtolower($oConfig->Get('branding', 'welcome_page_display', 'none')); } $aResult['LoadingDescriptionEsc'] = \htmlspecialchars($aResult['LoadingDescription'], ENT_QUOTES|ENT_IGNORE, 'UTF-8'); @@ -1414,6 +1418,24 @@ class Actions $oSettings = $this->SettingsProvider()->Load($oAccount); $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); + + if (!$oAccount->IsAdditionalAccount() && !empty($aResult['WelcomePageUrl']) && + ('once' === $aResult['WelcomePageDisplay'] || 'always' === $aResult['WelcomePageDisplay'])) + { + if ('once' === $aResult['WelcomePageDisplay']) + { + if ($aResult['WelcomePageUrl'] === $oSettings->GetConf('LastWelcomePage', '')) + { + $aResult['WelcomePageUrl'] = ''; + $aResult['WelcomePageDisplay'] = ''; + } + } + } + else + { + $aResult['WelcomePageUrl'] = ''; + $aResult['WelcomePageDisplay'] = ''; + } } else { @@ -1424,6 +1446,9 @@ class Actions $aResult['DevEmail'] = $oConfig->Get('labs', 'dev_email', ''); $aResult['DevPassword'] = $oConfig->Get('labs', 'dev_password', ''); + + $aResult['WelcomePageUrl'] = ''; + $aResult['WelcomePageDisplay'] = ''; } $aResult['AllowGoogleSocial'] = (bool) $oConfig->Get('social', 'google_enable', false); @@ -3247,7 +3272,11 @@ class Actions $this->setConfigFromParams($oConfig, 'Title', 'webmail', 'title', 'string'); $this->setConfigFromParams($oConfig, 'LoadingDescription', 'webmail', 'loading_description', 'string'); - if ($this->HasOneOfActionParams(array('LoginLogo', 'LoginBackground', 'LoginDescription', 'LoginCss', 'LoginPowered', 'UserLogo', 'UserCss')) && $this->PremType()) + if ($this->HasOneOfActionParams(array( + 'LoginLogo', 'LoginBackground', 'LoginDescription', 'LoginCss', 'LoginPowered', + 'UserLogo', 'UserCss', + 'WelcomePageUrl', 'WelcomePageDisplay' + )) && $this->PremType()) { $this->setConfigFromParams($oConfig, 'LoginLogo', 'branding', 'login_logo', 'string'); $this->setConfigFromParams($oConfig, 'LoginBackground', 'branding', 'login_background', 'string'); @@ -3257,6 +3286,9 @@ class Actions $this->setConfigFromParams($oConfig, 'UserLogo', 'branding', 'user_logo', 'string'); $this->setConfigFromParams($oConfig, 'UserCss', 'branding', 'user_css', 'string'); + + $this->setConfigFromParams($oConfig, 'WelcomePageUrl', 'branding', 'welcome_page_url', 'string'); + $this->setConfigFromParams($oConfig, 'WelcomePageDisplay', 'branding', 'welcome_page_display', 'string'); } $this->setConfigFromParams($oConfig, 'TokenProtection', 'security', 'csrf_protection', 'bool'); @@ -4810,6 +4842,25 @@ class Actions return $this->DefaultResponse(__FUNCTION__, true); } + /** + * @return array + */ + public function DoWelcomeClose() + { + $oAccount = $this->getAccountFromToken(); + if ($oAccount && !$oAccount->IsAdditionalAccount()) + { + $oSettings = $this->SettingsProvider()->Load($oAccount); + $oSettings->SetConf('LastWelcomePage', + $this->Config()->Get('branding', 'welcome_page_url', '')); + + return $this->DefaultResponse(__FUNCTION__, + $this->SettingsProvider()->Save($oAccount, $oSettings)); + } + + return $this->FalseResponse(__FUNCTION__); + } + /** * @return array */ @@ -8509,6 +8560,9 @@ class Actions $sHtml = $this->Plugins()->ProcessTemplate($sName, $sHtml); $sHtml = \preg_replace('/\{\{INCLUDE\/([a-zA-Z]+)\/PLACE\}\}/', '', $sHtml); + $sHtml = \preg_replace('/