mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 10:09:20 +03:00
Improvements of message displaying (internal styles)
This commit is contained in:
parent
0bba7146a6
commit
4457cdbc23
46 changed files with 1967 additions and 359 deletions
7
CONTRIBUTING.md
Normal file
7
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Thanks for contributing to RainLoop Webmail!
|
||||
|
||||
1. Sign the CLA (individual or corporate) at <http://www.rainloop.net/contribute/>.
|
||||
2. Fork the repo, do work in a feature branch.
|
||||
3. Issue a pull request.
|
||||
|
||||
If you have any questions, open an issue or email [support@rainloop.net](mailto:support@rainloop.net).
|
||||
|
|
@ -5,9 +5,9 @@ Simple, modern & fast web-based email client.
|
|||
|
||||
Modest system requirements, decent performance, simple installation and upgrade, no database required - all these make RainLoop Webmail a perfect choice for your email solution.
|
||||
|
||||
For more information about the product, check [http://rainloop.net](http://rainloop.net).
|
||||
For more information about the product, check [rainloop.net](http://www.rainloop.net/).
|
||||
|
||||
Information about installing the product, check the [documentation page](http://rainloop.net/docs/installation/).
|
||||
Information about installing the product, check the [documentation page](http://www.rainloop.net/docs/installation/).
|
||||
|
||||
## Attention
|
||||
|
||||
|
|
|
|||
|
|
@ -1511,7 +1511,7 @@
|
|||
self.initVerticalLayoutResizer(Enums.ClientSideKeyName.FolderListSize);
|
||||
});
|
||||
|
||||
if (Tinycon)
|
||||
if (Tinycon && !Settings.settingsGet('Filtered'))
|
||||
{
|
||||
Tinycon.setOptions({
|
||||
fallback: false
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@
|
|||
{
|
||||
this.modeToggle(true);
|
||||
|
||||
sHtml = sHtml.replace(/<[pP][^>]*><\/[pP]>/g, '');
|
||||
sHtml = sHtml.replace(/<p[^>]*><\/p>/ig, '');
|
||||
|
||||
try {
|
||||
this.editor.setData(sHtml);
|
||||
|
|
|
|||
|
|
@ -867,13 +867,16 @@
|
|||
|
||||
sText = sHtml
|
||||
.replace(/\u0002([\s\S]*)\u0002/gm, '\u200C$1\u200C')
|
||||
.replace(/<[pP][^>]*><\/[pP]>/g, '')
|
||||
.replace(/<p[^>]*><\/p>/gi, '')
|
||||
.replace(/<pre[^>]*>([\s\S\r\n]*)<\/pre>/gmi, convertPre)
|
||||
.replace(/[\s]+/gm, ' ')
|
||||
.replace(/((?:href|data)\s?=\s?)("[^"]+?"|'[^']+?')/gmi, fixAttibuteValue)
|
||||
.replace(/<br[^>]*>/gmi, '\n')
|
||||
.replace(/<\/h[\d]>/gi, '\n')
|
||||
.replace(/<\/p>/gi, '\n\n')
|
||||
.replace(/<ul[^>]*>/gmi, '\n')
|
||||
.replace(/<\/ul>/gi, '\n')
|
||||
.replace(/<li[^>]*>/gmi, ' * ')
|
||||
.replace(/<\/li>/gi, '\n')
|
||||
.replace(/<\/td>/gi, '\n')
|
||||
.replace(/<\/tr>/gi, '\n')
|
||||
|
|
|
|||
|
|
@ -55,6 +55,11 @@
|
|||
nFoldersInboxUnreadCount = FolderStore.foldersInboxUnreadCount()
|
||||
;
|
||||
|
||||
if (Settings.settingsGet('Filtered'))
|
||||
{
|
||||
nFoldersInboxUnreadCount = 0;
|
||||
}
|
||||
|
||||
require('App/User').setWindowTitle(('' === sEmail ? '' : '' +
|
||||
(0 < nFoldersInboxUnreadCount ? '(' + nFoldersInboxUnreadCount + ') ' : ' ') +
|
||||
sEmail + ' - ') + Translator.i18n('TITLES/MAILBOX'));
|
||||
|
|
@ -82,7 +87,6 @@
|
|||
* @param {string} sFolderHash
|
||||
* @param {number} iPage
|
||||
* @param {string} sSearch
|
||||
* @param {boolean=} bPreview = false
|
||||
*/
|
||||
MailBoxUserScreen.prototype.onRoute = function (sFolderHash, iPage, sSearch)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
this.googleEnable = SocialStore.google.enabled;
|
||||
this.googleEnableAuth = SocialStore.google.capa.auth;
|
||||
this.googleEnableAuthFast = SocialStore.google.capa.authFast;
|
||||
this.googleEnableDrive = SocialStore.google.capa.drive;
|
||||
this.googleEnablePreview = SocialStore.google.capa.preview;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
this.googleEnable = SocialStore.google.enabled;
|
||||
this.googleEnableAuth = SocialStore.google.capa.auth;
|
||||
this.googleEnableAuthFast = SocialStore.google.capa.authFast;
|
||||
this.googleEnableDrive = SocialStore.google.capa.drive;
|
||||
this.googleEnablePreview = SocialStore.google.capa.preview;
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
|
||||
this.google.capa = {};
|
||||
this.google.capa.auth = ko.observable(false);
|
||||
this.google.capa.authFast = ko.observable(false);
|
||||
this.google.capa.drive = ko.observable(false);
|
||||
this.google.capa.preview = ko.observable(false);
|
||||
|
||||
|
|
@ -88,6 +89,7 @@
|
|||
this.google.apiKey(Settings.settingsGet('GoogleApiKey'));
|
||||
|
||||
this.google.capa.auth(!!Settings.settingsGet('AllowGoogleSocialAuth'));
|
||||
this.google.capa.authFast(!!Settings.settingsGet('AllowGoogleSocialAuthFast'));
|
||||
this.google.capa.drive(!!Settings.settingsGet('AllowGoogleSocialDrive'));
|
||||
this.google.capa.preview(!!Settings.settingsGet('AllowGoogleSocialPreview'));
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@
|
|||
max-width: 450px;
|
||||
}
|
||||
|
||||
.wrapper-parent {
|
||||
}
|
||||
|
||||
.loginForm {
|
||||
background-color: #efefef;
|
||||
text-align: left;
|
||||
|
|
@ -34,6 +37,18 @@
|
|||
width: 303px;
|
||||
}
|
||||
|
||||
.loginWelcomeForm {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.welcome-on {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.welcome-off {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* .control-label {
|
||||
font-size: 16px;
|
||||
line-height: 30px;
|
||||
|
|
|
|||
|
|
@ -331,6 +331,10 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
}
|
||||
|
||||
.bodySubHeader {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.bodyText {
|
||||
|
||||
color: #000;
|
||||
|
|
@ -338,9 +342,11 @@ html.rl-no-preview-pane {
|
|||
|
||||
.b-text-part {
|
||||
|
||||
height: 100%;
|
||||
|
||||
div[data-x-div-type=html] {
|
||||
height: 100%;
|
||||
div[data-x-div-type=html] {
|
||||
div[data-x-div-type=body] {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
|
@ -357,14 +363,10 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid #000;
|
||||
margin: 0;
|
||||
padding: 0px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rl-bq-switcher.hidden-bq {
|
||||
|
|
@ -372,8 +374,10 @@ html.rl-no-preview-pane {
|
|||
}
|
||||
|
||||
&.html {
|
||||
|
||||
div[data-x-div-type=body] {
|
||||
margin: 15px;
|
||||
padding: 15px;
|
||||
// margin: 15px;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@
|
|||
min-height: 300px;
|
||||
}
|
||||
|
||||
.g-ui-100-proc-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.g-ui-absolute-reset {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
|||
|
|
@ -17,12 +17,19 @@
|
|||
width: 17px;
|
||||
}
|
||||
|
||||
.iconsize50 {
|
||||
line-height: 50px;
|
||||
font-size: 50px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.iconsize24 {
|
||||
line-height: 24px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.iconsize24 {
|
||||
.iconsize20 {
|
||||
line-height: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
{
|
||||
AbstractView.call(this, 'Center', 'Login');
|
||||
|
||||
this.welcome = ko.observable(!!Settings.settingsGet('UseLoginWelcomePage'));
|
||||
|
||||
this.email = ko.observable('');
|
||||
this.password = ko.observable('');
|
||||
this.signMe = ko.observable(false);
|
||||
|
|
@ -294,11 +296,12 @@
|
|||
});
|
||||
|
||||
this.googleLoginEnabled = ko.observable(false);
|
||||
this.googleFastLoginEnabled = ko.observable(false);
|
||||
|
||||
this.googleCommand = Utils.createCommand(this, function () {
|
||||
|
||||
window.open(Links.socialGoogle(), 'Google',
|
||||
'left=200,top=100,width=650,height=450,menubar=no,status=no,resizable=yes,scrollbars=yes');
|
||||
'left=200,top=100,width=650,height=500,menubar=no,status=no,resizable=yes,scrollbars=yes');
|
||||
|
||||
return true;
|
||||
|
||||
|
|
@ -306,15 +309,15 @@
|
|||
return !this.submitRequest() && this.googleLoginEnabled();
|
||||
});
|
||||
|
||||
this.googleXAuthCommand = Utils.createCommand(this, function () {
|
||||
this.googleFastCommand = Utils.createCommand(this, function () {
|
||||
|
||||
window.open(Links.socialGoogle(true), 'Google',
|
||||
'left=200,top=100,width=650,height=450,menubar=no,status=no,resizable=yes,scrollbars=yes');
|
||||
'left=200,top=100,width=650,height=500,menubar=no,status=no,resizable=yes,scrollbars=yes');
|
||||
|
||||
return true;
|
||||
|
||||
}, function () {
|
||||
return !this.submitRequest() && this.googleLoginEnabled();
|
||||
return !this.submitRequest() && this.googleFastLoginEnabled();
|
||||
});
|
||||
|
||||
this.twitterLoginEnabled = ko.observable(false);
|
||||
|
|
@ -347,6 +350,11 @@
|
|||
kn.extendAsViewModel(['View/User/Login', 'View/App/Login', 'LoginViewModel'], LoginUserView);
|
||||
_.extend(LoginUserView.prototype, AbstractView.prototype);
|
||||
|
||||
LoginUserView.prototype.displayMainForm = function ()
|
||||
{
|
||||
this.welcome(false);
|
||||
};
|
||||
|
||||
LoginUserView.prototype.onShow = function ()
|
||||
{
|
||||
kn.routeOff();
|
||||
|
|
@ -404,6 +412,8 @@
|
|||
this.twitterLoginEnabled(!!Settings.settingsGet('AllowTwitterSocial'));
|
||||
this.googleLoginEnabled(!!Settings.settingsGet('AllowGoogleSocial') &&
|
||||
!!Settings.settingsGet('AllowGoogleSocialAuth'));
|
||||
this.googleFastLoginEnabled(!!Settings.settingsGet('AllowGoogleSocial') &&
|
||||
!!Settings.settingsGet('AllowGoogleSocialAuthFast'));
|
||||
|
||||
switch (sSignMe)
|
||||
{
|
||||
|
|
@ -433,7 +443,7 @@
|
|||
this.email(AppStore.devEmail);
|
||||
this.password(AppStore.devPassword);
|
||||
|
||||
if (this.googleLoginEnabled())
|
||||
if (this.googleLoginEnabled() || this.googleFastLoginEnabled())
|
||||
{
|
||||
window['rl_' + sJsHash + '_google_login_service'] = fSocial;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@
|
|||
|
||||
Cache = require('Common/Cache'),
|
||||
|
||||
EmailModel = require('Model/Email'),
|
||||
|
||||
SocialStore = require('Stores/Social'),
|
||||
AppStore = require('Stores/User/App'),
|
||||
SettingsStore = require('Stores/User/Settings'),
|
||||
|
|
@ -66,6 +64,8 @@
|
|||
this.oHeaderDom = null;
|
||||
this.oMessageScrollerDom = null;
|
||||
|
||||
this.bodyBackgroundColor = ko.observable('');
|
||||
|
||||
this.pswp = null;
|
||||
|
||||
this.allowComposer = !!Settings.capa(Enums.Capa.Composer);
|
||||
|
|
@ -353,6 +353,10 @@
|
|||
|
||||
}, this);
|
||||
|
||||
this.messageActiveDom.subscribe(function (oDom) {
|
||||
this.bodyBackgroundColor(oDom ? this.detectDomBackgroundColor(oDom): '');
|
||||
}, this);
|
||||
|
||||
this.message.subscribe(function (oMessage) {
|
||||
|
||||
this.messageActiveDom(null);
|
||||
|
|
@ -466,6 +470,59 @@
|
|||
kn.extendAsViewModel(['View/User/MailBox/MessageView', 'View/App/MailBox/MessageView', 'MailBoxMessageViewViewModel'], MessageViewMailBoxUserView);
|
||||
_.extend(MessageViewMailBoxUserView.prototype, AbstractView.prototype);
|
||||
|
||||
MessageViewMailBoxUserView.prototype.detectDomBackgroundColor = function (oDom)
|
||||
{
|
||||
var
|
||||
iLimit = 5,
|
||||
sResult = '',
|
||||
aC = null,
|
||||
fFindDom = function (oDom) {
|
||||
var aC = oDom ? oDom.children() : null;
|
||||
return (aC && 1 === aC.length && aC.is('table,div,center')) ? aC : null;
|
||||
},
|
||||
fFindColor = function (oDom) {
|
||||
var sResult = '';
|
||||
if (oDom)
|
||||
{
|
||||
sResult = oDom.css('background-color') || '';
|
||||
if (!oDom.is('table'))
|
||||
{
|
||||
sResult = 'rgba(0, 0, 0, 0)' === sResult || 'transparent' === sResult ? '' : sResult;
|
||||
}
|
||||
}
|
||||
|
||||
return sResult;
|
||||
}
|
||||
;
|
||||
|
||||
if (oDom && 1 === oDom.length)
|
||||
{
|
||||
aC = oDom;
|
||||
while ('' === sResult)
|
||||
{
|
||||
iLimit--;
|
||||
if (0 >= iLimit)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
aC = fFindDom(aC);
|
||||
if (aC)
|
||||
{
|
||||
sResult = fFindColor(aC);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sResult = 'rgba(0, 0, 0, 0)' === sResult || 'transparent' === sResult ? '' : sResult;
|
||||
}
|
||||
|
||||
return sResult;
|
||||
};
|
||||
|
||||
MessageViewMailBoxUserView.prototype.isPgpActionVisible = function ()
|
||||
{
|
||||
return Enums.SignedVerifyStatus.Success !== this.viewPgpSignedVerifyStatus();
|
||||
|
|
@ -552,27 +609,28 @@
|
|||
* @todo
|
||||
* @param {string} sEmail
|
||||
*/
|
||||
MessageViewMailBoxUserView.prototype.displayMailToPopup = function (sMailToUrl)
|
||||
{
|
||||
sMailToUrl = sMailToUrl.replace(/\?.+$/, '');
|
||||
|
||||
var
|
||||
sResult = '',
|
||||
aTo = [],
|
||||
fParseEmailLine = function (sLine) {
|
||||
return sLine ? _.compact(_.map([window.decodeURIComponent(sLine)], function (sItem) {
|
||||
var oEmailModel = new EmailModel();
|
||||
oEmailModel.mailsoParse(sItem);
|
||||
return '' !== oEmailModel.email ? oEmailModel : null;
|
||||
})) : null;
|
||||
}
|
||||
;
|
||||
|
||||
aTo = fParseEmailLine(sMailToUrl);
|
||||
sResult = aTo && aTo[0] ? aTo[0].email : '';
|
||||
|
||||
window.console.log(sResult);
|
||||
};
|
||||
// MessageViewMailBoxUserView.prototype.displayMailToPopup = function (sMailToUrl)
|
||||
// {
|
||||
// sMailToUrl = sMailToUrl.replace(/\?.+$/, '');
|
||||
//
|
||||
// var
|
||||
// sResult = '',
|
||||
// aTo = [],
|
||||
// EmailModel = require('Model/Email'),
|
||||
// fParseEmailLine = function (sLine) {
|
||||
// return sLine ? _.compact(_.map([window.decodeURIComponent(sLine)], function (sItem) {
|
||||
// var oEmailModel = new EmailModel();
|
||||
// oEmailModel.mailsoParse(sItem);
|
||||
// return '' !== oEmailModel.email ? oEmailModel : null;
|
||||
// })) : null;
|
||||
// }
|
||||
// ;
|
||||
//
|
||||
// aTo = fParseEmailLine(sMailToUrl);
|
||||
// sResult = aTo && aTo[0] ? aTo[0].email : '';
|
||||
//
|
||||
// return sResult;
|
||||
// };
|
||||
|
||||
MessageViewMailBoxUserView.prototype.onBuild = function (oDom)
|
||||
{
|
||||
|
|
@ -592,6 +650,7 @@
|
|||
}
|
||||
}, this);
|
||||
|
||||
this.showAttachmnetControls.subscribe(fCheckHeaderHeight);
|
||||
this.fullScreenMode.subscribe(fCheckHeaderHeight);
|
||||
this.showFullInfo.subscribe(fCheckHeaderHeight);
|
||||
this.message.subscribe(fCheckHeaderHeight);
|
||||
|
|
@ -806,18 +865,6 @@
|
|||
this.initShortcuts();
|
||||
};
|
||||
|
||||
MessageViewMailBoxUserView.prototype.selectSelectedThreadMessage = function ()
|
||||
{
|
||||
var self = this;
|
||||
_.delay(function () {
|
||||
var oLast = self.oDom ? $('.thread-list .e-item.thread-list-message.real-msg.selected a.e-link', self.oDom) : null;
|
||||
if (oLast && oLast[0])
|
||||
{
|
||||
oLast.focus();
|
||||
}
|
||||
}, 30);
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {boolean}
|
||||
*/
|
||||
|
|
@ -1196,6 +1243,8 @@
|
|||
{
|
||||
oMessage.showExternalImages(true);
|
||||
}
|
||||
|
||||
this.checkHeaderHeight();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1217,6 +1266,8 @@
|
|||
{
|
||||
oMessage.verifyPgpSignedClearMessage();
|
||||
}
|
||||
|
||||
this.checkHeaderHeight();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -1248,6 +1299,8 @@
|
|||
|
||||
require('App/User').reloadFlagsCurrentMessageListAndMessageFromCache();
|
||||
}
|
||||
|
||||
this.checkHeaderHeight();
|
||||
};
|
||||
|
||||
module.exports = MessageViewMailBoxUserView;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "RainLoop",
|
||||
"title": "RainLoop Webmail",
|
||||
"version": "1.9.1",
|
||||
"release": "333",
|
||||
"release": "334",
|
||||
"description": "Simple, modern & fast web-based email client",
|
||||
"homepage": "http://rainloop.net",
|
||||
"main": "gulpfile.js",
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ if (!\defined('RAINLOOP_APP_LIBRARIES_PATH'))
|
|||
|
||||
if (\class_exists('RainLoop\Api'))
|
||||
{
|
||||
if (!\class_exists('MailSo\Version'))
|
||||
if (!\class_exists('MailSo\Version', false))
|
||||
{
|
||||
include APP_VERSION_ROOT_PATH.'app/libraries/MailSo/MailSo.php';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,23 +35,32 @@ class HtmlUtils
|
|||
*/
|
||||
public static function GetDomFromText($sText, $sHtmlAttrs = '', $sBodyAttrs = '')
|
||||
{
|
||||
static $bOnce = true;
|
||||
if ($bOnce)
|
||||
$bState = true;
|
||||
if (\MailSo\Base\Utils::FunctionExistsAndEnabled('libxml_use_internal_errors'))
|
||||
{
|
||||
$bOnce = false;
|
||||
if (\MailSo\Base\Utils::FunctionExistsAndEnabled('libxml_use_internal_errors'))
|
||||
{
|
||||
@\libxml_use_internal_errors(true);
|
||||
}
|
||||
$bState = \libxml_use_internal_errors(true);
|
||||
}
|
||||
|
||||
$oDom = new \DOMDocument('1.0', 'utf-8');
|
||||
$oDom->encoding = 'UTF-8';
|
||||
$oDom->strictErrorChecking = false;
|
||||
$oDom->formatOutput = false;
|
||||
|
||||
@$oDom->loadHTML('<'.'?xml version="1.0" encoding="utf-8"?'.'>'.
|
||||
'<html '.$sHtmlAttrs.'><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body '.$sBodyAttrs.'>'.$sText.'</body></html>');
|
||||
|
||||
@$oDom->normalizeDocument();
|
||||
|
||||
if (\MailSo\Base\Utils::FunctionExistsAndEnabled('libxml_use_internal_errors'))
|
||||
{
|
||||
@\libxml_clear_errors();
|
||||
}
|
||||
|
||||
if (\MailSo\Base\Utils::FunctionExistsAndEnabled('libxml_use_internal_errors'))
|
||||
{
|
||||
\libxml_use_internal_errors($bState);
|
||||
}
|
||||
|
||||
return $oDom;
|
||||
}
|
||||
|
||||
|
|
@ -65,13 +74,13 @@ class HtmlUtils
|
|||
public static function ClearBodyAndHtmlTag($sHtml, &$sHtmlAttrs = '', &$sBodyAttrs = '')
|
||||
{
|
||||
$aMatch = array();
|
||||
if (preg_match('/<html([^>]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1]))
|
||||
if (\preg_match('/<html([^>]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1]))
|
||||
{
|
||||
$sHtmlAttrs = $aMatch[1];
|
||||
}
|
||||
|
||||
$aMatch = array();
|
||||
if (preg_match('/<body([^>]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1]))
|
||||
if (\preg_match('/<body([^>]+)>/im', $sHtml, $aMatch) && !empty($aMatch[1]))
|
||||
{
|
||||
$sBodyAttrs = $aMatch[1];
|
||||
}
|
||||
|
|
@ -81,6 +90,29 @@ class HtmlUtils
|
|||
$sHtml = \preg_replace('/<html([^>]*)>/im', '', $sHtml);
|
||||
$sHtml = \preg_replace('/<\/html>/im', '', $sHtml);
|
||||
|
||||
$sHtmlAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/im', '', $sHtmlAttrs);
|
||||
$sHtmlAttrs = \preg_replace('/xmlns="[^"]*"/im', '', $sHtmlAttrs);
|
||||
$sBodyAttrs = \preg_replace('/xmlns:[a-z]="[^"]*"/im', '', $sBodyAttrs);
|
||||
|
||||
return $sHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sHtml
|
||||
* @param bool $bClearEmpty = true
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function FixSchemas($sHtml, $bClearEmpty = true)
|
||||
{
|
||||
if ($bClearEmpty)
|
||||
{
|
||||
$sHtml = \str_replace('<o:p></o:p>', '', $sHtml);
|
||||
}
|
||||
|
||||
$sHtml = \str_replace('<o:p>', '<span data-ms="o:p">', $sHtml);
|
||||
$sHtml = \str_replace('</o:p>', '</span>', $sHtml);
|
||||
|
||||
return $sHtml;
|
||||
}
|
||||
|
||||
|
|
@ -104,6 +136,7 @@ class HtmlUtils
|
|||
}
|
||||
|
||||
$aToRemove = array(
|
||||
'/<p[^>]*><\/p>/i',
|
||||
'/<!doctype[^>]*>/msi',
|
||||
'/<\?xml [^>]*\?>/msi'
|
||||
);
|
||||
|
|
@ -262,7 +295,7 @@ class HtmlUtils
|
|||
// $mResult = false;
|
||||
// $oCss = null;
|
||||
//
|
||||
// if (!\class_exists('\\Sabberworm\\CSS\\Parser'))
|
||||
// if (!\class_exists('Sabberworm\CSS\Parser'))
|
||||
// {
|
||||
// return $mResult;
|
||||
// }
|
||||
|
|
@ -637,15 +670,17 @@ class HtmlUtils
|
|||
* @param bool $bDoNotReplaceExternalUrl = false
|
||||
* @param bool $bFindLinksInHtml = false
|
||||
* @param callback|null $fAdditionalExternalFilter = null
|
||||
* @param callback|null $fAdditionalDomReader = null
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function ClearHtml($sHtml, &$bHasExternals = false, &$aFoundCIDs = array(),
|
||||
$aContentLocationUrls = array(), &$aFoundedContentLocationUrls = array(),
|
||||
$bDoNotReplaceExternalUrl = false, $bFindLinksInHtml = false, $fAdditionalExternalFilter = null)
|
||||
$bDoNotReplaceExternalUrl = false, $bFindLinksInHtml = false,
|
||||
$fAdditionalExternalFilter = null, $fAdditionalDomReader = false,
|
||||
$bTryToDetectHiddenImages = false)
|
||||
{
|
||||
$sResult = '';
|
||||
// $aBodyStyles = array();
|
||||
|
||||
$sHtml = null === $sHtml ? '' : (string) $sHtml;
|
||||
$sHtml = \trim($sHtml);
|
||||
|
|
@ -659,8 +694,15 @@ class HtmlUtils
|
|||
$fAdditionalExternalFilter = null;
|
||||
}
|
||||
|
||||
if ($fAdditionalDomReader && !\is_callable($fAdditionalDomReader))
|
||||
{
|
||||
$fAdditionalDomReader = null;
|
||||
}
|
||||
|
||||
$bHasExternals = false;
|
||||
|
||||
$sHtml = \MailSo\Base\HtmlUtils::FixSchemas($sHtml);
|
||||
|
||||
$sHtml = \MailSo\Base\HtmlUtils::ClearTags($sHtml, false);
|
||||
$sHtml = \MailSo\Base\HtmlUtils::ClearOn($sHtml);
|
||||
|
||||
|
|
@ -673,11 +715,37 @@ class HtmlUtils
|
|||
|
||||
if ($oDom)
|
||||
{
|
||||
if ($fAdditionalDomReader)
|
||||
{
|
||||
$oResDom = \call_user_func($fAdditionalDomReader, $oDom);
|
||||
if ($oResDom)
|
||||
{
|
||||
$oDom = $oResDom;
|
||||
}
|
||||
|
||||
unset($oResDom);
|
||||
}
|
||||
|
||||
if ($bFindLinksInHtml)
|
||||
{
|
||||
\MailSo\Base\HtmlUtils::FindLinksInDOM($oDom);
|
||||
}
|
||||
|
||||
$oXpath = new \DOMXpath($oDom);
|
||||
$oComments = $oXpath->query('//comment()');
|
||||
if ($oComments)
|
||||
{
|
||||
foreach ($oComments as $oComment)
|
||||
{
|
||||
if (isset($oComment->parentNode))
|
||||
{
|
||||
@$oComment->parentNode->removeChild($oComment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unset($oXpath, $oComments);
|
||||
|
||||
$aNodes = $oDom->getElementsByTagName('*');
|
||||
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement)
|
||||
{
|
||||
|
|
@ -685,19 +753,10 @@ class HtmlUtils
|
|||
{
|
||||
$sTagNameLower = \strtolower($oElement->tagName);
|
||||
|
||||
if (\in_array($sTagNameLower, array('svg', 'head', 'link',
|
||||
if ('' !== $sTagNameLower && \in_array($sTagNameLower, array('svg', 'head', 'link',
|
||||
'base', 'meta', 'title', 'style', 'x-script', 'script', 'bgsound', 'keygen', 'source',
|
||||
'object', 'embed', 'applet', 'mocha', 'iframe', 'frame', 'frameset', 'video', 'audio')))
|
||||
{
|
||||
// if ('style' === $sTagNameLower)
|
||||
// {
|
||||
// $sV = (string) $oElement->textContent;
|
||||
// if (!empty($sV))
|
||||
// {
|
||||
// $aBodyStyles[] = $sV;
|
||||
// }
|
||||
// }
|
||||
|
||||
if (isset($oElement->parentNode))
|
||||
{
|
||||
@$oElement->parentNode->removeChild($oElement);
|
||||
|
|
@ -706,6 +765,7 @@ class HtmlUtils
|
|||
}
|
||||
}
|
||||
|
||||
$sLinkColor = '';
|
||||
$aNodes = $oDom->getElementsByTagName('*');
|
||||
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement)
|
||||
{
|
||||
|
|
@ -715,6 +775,7 @@ class HtmlUtils
|
|||
if ('body' === $sTagNameLower)
|
||||
{
|
||||
$aAttrs = array(
|
||||
'link' => '',
|
||||
'text' => '',
|
||||
'topmargin' => '',
|
||||
'leftmargin' => '',
|
||||
|
|
@ -746,6 +807,13 @@ class HtmlUtils
|
|||
|
||||
switch ($sIndex)
|
||||
{
|
||||
case 'link':
|
||||
$sLinkColor = \trim($aItem[1]);
|
||||
if (!\preg_match('/^#[abcdef0-9]{3,6}$/i', $sLinkColor))
|
||||
{
|
||||
$sLinkColor = '';
|
||||
}
|
||||
break;
|
||||
case 'text':
|
||||
$aStyles[] = 'color: '.$aItem[1];
|
||||
break;
|
||||
|
|
@ -767,7 +835,7 @@ class HtmlUtils
|
|||
|
||||
if (0 < \count($aStyles))
|
||||
{
|
||||
$sStyles = $oElement->hasAttribute('style') ? $oElement->getAttribute('style') : '';
|
||||
$sStyles = $oElement->hasAttribute('style') ? \trim(\trim(\trim($oElement->getAttribute('style')), ';')) : '';
|
||||
$oElement->setAttribute('style', (empty($sStyles) ? '' : $sStyles.'; ').\implode('; ', $aStyles));
|
||||
}
|
||||
}
|
||||
|
|
@ -777,6 +845,15 @@ class HtmlUtils
|
|||
$oElement->setAttribute('src', 'javascript:false');
|
||||
}
|
||||
|
||||
if ('a' === $sTagNameLower && !empty($sLinkColor))
|
||||
{
|
||||
$sStyles = $oElement->hasAttribute('style')
|
||||
? \trim(\trim(\trim($oElement->getAttribute('style')), ';')) : '';
|
||||
|
||||
$oElement->setAttribute('style',
|
||||
'color: '.$sLinkColor.\trim((empty($sStyles) ? '' : '; '.$sStyles)));
|
||||
}
|
||||
|
||||
if (\in_array($sTagNameLower, array('a', 'form', 'area')))
|
||||
{
|
||||
$oElement->setAttribute('target', '_blank');
|
||||
|
|
@ -787,15 +864,10 @@ class HtmlUtils
|
|||
$oElement->setAttribute('tabindex', '-1');
|
||||
}
|
||||
|
||||
// if ('blockquote' === $sTagNameLower)
|
||||
// {
|
||||
// $oElement->removeAttribute('style');
|
||||
// }
|
||||
|
||||
foreach (array(
|
||||
'id', 'class',
|
||||
'contenteditable', 'designmode', 'formaction',
|
||||
'data-bind', 'xmlns', 'srcset'
|
||||
'data-bind', 'data-reactid', 'xmlns', 'srcset'
|
||||
) as $sAttr)
|
||||
{
|
||||
@$oElement->removeAttribute($sAttr);
|
||||
|
|
@ -826,6 +898,35 @@ class HtmlUtils
|
|||
}
|
||||
}
|
||||
|
||||
if ($bTryToDetectHiddenImages && 'img' === $sTagNameLower)
|
||||
{
|
||||
$sAlt = $oElement->hasAttribute('alt')
|
||||
? \trim($oElement->getAttribute('alt')) : '';
|
||||
|
||||
$sH = $oElement->hasAttribute('height')
|
||||
? \trim($oElement->getAttribute('height')) : '';
|
||||
|
||||
$sW = $oElement->hasAttribute('width')
|
||||
? \trim($oElement->getAttribute('width')) : '';
|
||||
|
||||
$sStyles = $oElement->hasAttribute('style')
|
||||
? \trim(\trim(\trim($oElement->getAttribute('style')), ';')) : '';
|
||||
|
||||
if ($oElement->hasAttribute('src'))
|
||||
{
|
||||
$aValues = array('4', '3', '2', '1', '0', '4px', '3px', '2px', '1px', '0px');
|
||||
if (('' === $sAlt && \in_array($sH, $aValues) && \in_array($sW, $aValues)) ||
|
||||
\preg_match('/(display:none|visibility:hidden)/i', \preg_replace('/[\s]+/', '', $sStyles)))
|
||||
{
|
||||
if (isset($oElement->parentNode))
|
||||
{
|
||||
@$oElement->parentNode->removeChild($oElement);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($oElement->hasAttribute('src'))
|
||||
{
|
||||
$sSrc = \trim($oElement->getAttribute('src'));
|
||||
|
|
@ -884,7 +985,7 @@ class HtmlUtils
|
|||
{
|
||||
$aStyles = array();
|
||||
$sStyles = $oElement->hasAttribute('style')
|
||||
? $oElement->getAttribute('style') : '';
|
||||
? \trim(\trim(\trim($oElement->getAttribute('style')), ';')) : '';
|
||||
|
||||
if (!empty($sBackground))
|
||||
{
|
||||
|
|
@ -922,6 +1023,7 @@ class HtmlUtils
|
|||
$sResult = '<div data-x-div-type="html" '.$sHtmlAttrs.'>'.$sResult.'</div>';
|
||||
|
||||
$sResult = \str_replace(\MailSo\Base\HtmlUtils::$KOS, ':', $sResult);
|
||||
$sResult = \preg_replace('/[\s]+/u', ' ', $sResult);
|
||||
|
||||
return \trim($sResult);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ class Config
|
|||
*/
|
||||
public static $PreferStartTlsIfAutoDetect = true;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public static $BoundaryPrefix = '_Part_';
|
||||
|
||||
/**
|
||||
* @var \MailSo\Log\Logger|null
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -756,7 +756,8 @@ class MailClient
|
|||
{
|
||||
$iUnseenCount = 0; // unneccessery
|
||||
return \md5('FolderHash/'.$sFolder.'-'.$iCount.'-'.$iUnseenCount.'-'.$sUidNext.'-'.
|
||||
$sHighestModSeq.'-'.$this->GenerateImapClientHash()
|
||||
$sHighestModSeq.'-'.$this->GenerateImapClientHash().'-'.
|
||||
\MailSo\Config::$MessageListPermanentFilter
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1882,6 +1883,7 @@ class MailClient
|
|||
$oMessageCollection->Limit = $iLimit;
|
||||
$oMessageCollection->Search = $sSearch;
|
||||
$oMessageCollection->ThreadUid = $sThreadUid;
|
||||
$oMessageCollection->Filtered = '' !== \MailSo\Config::$MessageListPermanentFilter;
|
||||
|
||||
$aUids = array();
|
||||
$mAllSortedUids = null;
|
||||
|
|
@ -1988,7 +1990,9 @@ class MailClient
|
|||
|
||||
if (0 < \strlen($sSearch) && \is_array($aUids))
|
||||
{
|
||||
$aSearchedUids = $this->GetUids($oCacher, $sSearch, $sFilter, $oMessageCollection->FolderName, $oMessageCollection->FolderHash);
|
||||
$aSearchedUids = $this->GetUids($oCacher, $sSearch, $sFilter,
|
||||
$oMessageCollection->FolderName, $oMessageCollection->FolderHash);
|
||||
|
||||
if (\is_array($aSearchedUids) && 0 < \count($aSearchedUids))
|
||||
{
|
||||
$aFlippedSearchedUids = \array_flip($aSearchedUids);
|
||||
|
|
|
|||
|
|
@ -73,9 +73,9 @@ class MessageCollection extends \MailSo\Base\Collection
|
|||
public $NewMessages;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @var bool
|
||||
*/
|
||||
public $LastCollapsedThreadUids;
|
||||
public $Filtered;
|
||||
|
||||
/**
|
||||
* @access protected
|
||||
|
|
@ -116,7 +116,7 @@ class MessageCollection extends \MailSo\Base\Collection
|
|||
$this->ThreadUid = '';
|
||||
$this->NewMessages = array();
|
||||
|
||||
$this->LastCollapsedThreadUids = array();
|
||||
$this->Filtered = false;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -511,7 +511,8 @@ class Message
|
|||
*/
|
||||
private function generateNewBoundary()
|
||||
{
|
||||
return '----=_Part_'.rand(100, 999).'_'.rand(100000000, 999999999).'.'.time();
|
||||
return '--='.\MailSo\Config::$BoundaryPrefix.
|
||||
\rand(100, 999).'_'.rand(100000000, 999999999).'.'.\time();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -411,16 +411,17 @@ class Actions
|
|||
/**
|
||||
* @param string $sLine
|
||||
* @param \RainLoop\Model\Account $oAccount = null
|
||||
* @param bool $bUrlEncode = false
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function compileLogParams($sLine, $oAccount = null)
|
||||
private function compileLogParams($sLine, $oAccount = null, $bUrlEncode = false)
|
||||
{
|
||||
if (false !== \strpos($sLine, '{date:'))
|
||||
{
|
||||
$iTimeOffset = (int) $this->Config()->Get('logs', 'time_offset', 0);
|
||||
$sLine = \preg_replace_callback('/\{date:([^}]+)\}/', function ($aMatch) use ($iTimeOffset) {
|
||||
return \MailSo\Log\Logger::DateHelper($aMatch[1], $iTimeOffset);
|
||||
$sLine = \preg_replace_callback('/\{date:([^}]+)\}/', function ($aMatch) use ($iTimeOffset, $bUrlEncode) {
|
||||
return \RainLoop\Utils::UrlEncode(\MailSo\Log\Logger::DateHelper($aMatch[1], $iTimeOffset), $bUrlEncode);
|
||||
}, $sLine);
|
||||
|
||||
$sLine = \preg_replace('/\{date:([^}]*)\}/', 'date', $sLine);
|
||||
|
|
@ -436,37 +437,38 @@ class Actions
|
|||
|
||||
if ($oAccount)
|
||||
{
|
||||
$sLine = \str_replace('{imap:login}', $oAccount->IncLogin(), $sLine);
|
||||
$sLine = \str_replace('{imap:host}', $oAccount->DomainIncHost(), $sLine);
|
||||
$sLine = \str_replace('{imap:port}', $oAccount->DomainIncPort(), $sLine);
|
||||
$sLine = \str_replace('{imap:login}', \RainLoop\Utils::UrlEncode($oAccount->IncLogin(), $bUrlEncode), $sLine);
|
||||
$sLine = \str_replace('{imap:host}', \RainLoop\Utils::UrlEncode($oAccount->DomainIncHost(), $bUrlEncode), $sLine);
|
||||
$sLine = \str_replace('{imap:port}', \RainLoop\Utils::UrlEncode($oAccount->DomainIncPort(), $bUrlEncode), $sLine);
|
||||
|
||||
$sLine = \str_replace('{smtp:login}', $oAccount->OutLogin(), $sLine);
|
||||
$sLine = \str_replace('{smtp:host}', $oAccount->DomainOutHost(), $sLine);
|
||||
$sLine = \str_replace('{smtp:port}', $oAccount->DomainOutPort(), $sLine);
|
||||
$sLine = \str_replace('{smtp:login}', \RainLoop\Utils::UrlEncode($oAccount->OutLogin(), $bUrlEncode), $sLine);
|
||||
$sLine = \str_replace('{smtp:host}', \RainLoop\Utils::UrlEncode($oAccount->DomainOutHost(), $bUrlEncode), $sLine);
|
||||
$sLine = \str_replace('{smtp:port}', \RainLoop\Utils::UrlEncode($oAccount->DomainOutPort(), $bUrlEncode), $sLine);
|
||||
}
|
||||
|
||||
$sLine = \preg_replace('/\{imap:([^}]*)\}/i', 'imap', $sLine);
|
||||
$sLine = \preg_replace('/\{smtp:([^}]*)\}/i', 'imap', $sLine);
|
||||
$sLine = \preg_replace('/\{smtp:([^}]*)\}/i', 'smtp', $sLine);
|
||||
}
|
||||
|
||||
if (false !== \strpos($sLine, '{request:'))
|
||||
{
|
||||
if (false !== \strpos($sLine, '{request:ip}'))
|
||||
{
|
||||
$sLine = \str_replace('{request:ip}', $this->Http()->GetClientIp(
|
||||
$this->Config()->Get('labs', 'http_client_ip_check_proxy', false)), $sLine);
|
||||
$sLine = \str_replace('{request:ip}', \RainLoop\Utils::UrlEncode($this->Http()->GetClientIp(
|
||||
$this->Config()->Get('labs', 'http_client_ip_check_proxy', false)), $bUrlEncode), $sLine);
|
||||
}
|
||||
|
||||
if (false !== \strpos($sLine, '{request:domain}'))
|
||||
{
|
||||
$sLine = \str_replace('{request:domain}',
|
||||
$this->Http()->GetHost(false, true, true), $sLine);
|
||||
\RainLoop\Utils::UrlEncode($this->Http()->GetHost(false, true, true), $bUrlEncode), $sLine);
|
||||
}
|
||||
|
||||
if (false !== \strpos($sLine, '{request:domain-clear}'))
|
||||
{
|
||||
$sLine = \str_replace('{request:domain-clear}',
|
||||
\MailSo\Base\Utils::GetClearDomainName($this->Http()->GetHost(false, true, true)), $sLine);
|
||||
\RainLoop\Utils::UrlEncode(
|
||||
\MailSo\Base\Utils::GetClearDomainName($this->Http()->GetHost(false, true, true)), $bUrlEncode), $sLine);
|
||||
}
|
||||
|
||||
$sLine = \preg_replace('/\{request:([^}]*)\}/i', 'request', $sLine);
|
||||
|
|
@ -477,15 +479,16 @@ class Actions
|
|||
if (false !== \strpos($sLine, '{user:uid}'))
|
||||
{
|
||||
$sLine = \str_replace('{user:uid}',
|
||||
\base_convert(\sprintf('%u', \crc32(\md5(\RainLoop\Utils::GetConnectionToken()))), 10, 32),
|
||||
\RainLoop\Utils::UrlEncode(\base_convert(\sprintf('%u',
|
||||
\crc32(\md5(\RainLoop\Utils::GetConnectionToken()))), 10, 32), $bUrlEncode),
|
||||
$sLine
|
||||
);
|
||||
}
|
||||
|
||||
if (false !== \strpos($sLine, '{user:ip}'))
|
||||
{
|
||||
$sLine = \str_replace('{user:ip}', $this->Http()->GetClientIp(
|
||||
$this->Config()->Get('labs', 'http_client_ip_check_proxy', false)), $sLine);
|
||||
$sLine = \str_replace('{user:ip}', \RainLoop\Utils::UrlEncode($this->Http()->GetClientIp(
|
||||
$this->Config()->Get('labs', 'http_client_ip_check_proxy', false)), $bUrlEncode), $sLine);
|
||||
}
|
||||
|
||||
if (\preg_match('/\{user:(email|login|domain)\}/i', $sLine))
|
||||
|
|
@ -499,13 +502,15 @@ class Actions
|
|||
if ($oAccount)
|
||||
{
|
||||
$sEmail = $oAccount->Email();
|
||||
$sLine = \str_replace('{user:email}', $sEmail, $sLine);
|
||||
$sLine = \str_replace('{user:login}',
|
||||
\MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), $sLine);
|
||||
$sLine = \str_replace('{user:domain}',
|
||||
\MailSo\Base\Utils::GetDomainFromEmail($sEmail), $sLine);
|
||||
$sLine = \str_replace('{user:domain-clear}',
|
||||
\MailSo\Base\Utils::GetClearDomainName(\MailSo\Base\Utils::GetDomainFromEmail($sEmail)), $sLine);
|
||||
|
||||
$sLine = \str_replace('{user:email}', \RainLoop\Utils::UrlEncode($sEmail, $bUrlEncode), $sLine);
|
||||
$sLine = \str_replace('{user:login}', \RainLoop\Utils::UrlEncode(
|
||||
\MailSo\Base\Utils::GetAccountNameFromEmail($sEmail), $bUrlEncode), $sLine);
|
||||
$sLine = \str_replace('{user:domain}', \RainLoop\Utils::UrlEncode(
|
||||
\MailSo\Base\Utils::GetDomainFromEmail($sEmail), $bUrlEncode), $sLine);
|
||||
$sLine = \str_replace('{user:domain-clear}', \RainLoop\Utils::UrlEncode(
|
||||
\MailSo\Base\Utils::GetClearDomainName(
|
||||
\MailSo\Base\Utils::GetDomainFromEmail($sEmail)), $bUrlEncode), $sLine);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -997,6 +1002,18 @@ class Actions
|
|||
|
||||
if (!!$this->Config()->Get('logs', 'enable', false))
|
||||
{
|
||||
$sSessionFilter = (string) $this->Config()->Get('logs', 'session_filter', '');
|
||||
if (!empty($sSessionFilter))
|
||||
{
|
||||
$aSessionParts = \explode(':', $sSessionFilter, 2);
|
||||
|
||||
if (empty($aSessionParts[0]) || empty($aSessionParts[1]) ||
|
||||
(string) $aSessionParts[1] !== (string) \RainLoop\Utils::GetCookie($aSessionParts[0], ''))
|
||||
{
|
||||
return $this->oLogger;
|
||||
}
|
||||
}
|
||||
|
||||
$iTimeOffset = (int) $this->Config()->Get('logs', 'time_offset', 0);
|
||||
|
||||
$this->oLogger->SetShowSecter(!$this->Config()->Get('logs', 'hide_passwords', true));
|
||||
|
|
@ -1370,6 +1387,7 @@ class Actions
|
|||
'LoginDefaultDomain' => $oConfig->Get('login', 'default_domain', ''),
|
||||
'DetermineUserLanguage' => (bool) $oConfig->Get('login', 'determine_user_language', true),
|
||||
'DetermineUserDomain' => (bool) $oConfig->Get('login', 'determine_user_domain', false),
|
||||
'UseLoginWelcomePage' => (bool) $oConfig->Get('login', 'welcome_page', false),
|
||||
'ForgotPasswordLinkUrl' => \trim($oConfig->Get('login', 'forgot_password_link_url', '')),
|
||||
'RegistrationLinkUrl' => \trim($oConfig->Get('login', 'registration_link_url', '')),
|
||||
'ContactsIsAllowed' => false,
|
||||
|
|
@ -1382,6 +1400,7 @@ class Actions
|
|||
'MaterialDesign' => (bool) $oConfig->Get('labs', 'use_material_design', true),
|
||||
'FolderSpecLimit' => (int) $oConfig->Get('labs', 'folders_spec_limit', 50),
|
||||
'StartupUrl' => \trim(\ltrim(\trim($oConfig->Get('labs', 'startup_url', '')), '#/')),
|
||||
'Filtered' => '' !== \trim(\RainLoop\Api::Config()->Get('labs', 'imap_message_list_permanent_filter', '')),
|
||||
'Community' => true,
|
||||
'PremType' => false,
|
||||
'Admin' => array(),
|
||||
|
|
@ -1397,7 +1416,7 @@ class Actions
|
|||
$aResult['AttachmentsActions'][] = 'zip';
|
||||
}
|
||||
|
||||
if (\RainLoop\Utils::IsOwnCloudLoggedIn() && \class_exists('\\OCP\\Files'))
|
||||
if (\RainLoop\Utils::IsOwnCloudLoggedIn() && \class_exists('OCP\Files'))
|
||||
{
|
||||
$aResult['AttachmentsActions'][] = 'owncloud';
|
||||
}
|
||||
|
|
@ -1523,7 +1542,15 @@ class Actions
|
|||
$aResult['WelcomePageDisplay'] = '';
|
||||
}
|
||||
|
||||
$aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount);
|
||||
if (!empty($aResult['StartupUrl']))
|
||||
{
|
||||
$aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount, true);
|
||||
}
|
||||
|
||||
if (!empty($aResult['UserIframeMessage']))
|
||||
{
|
||||
$aResult['UserIframeMessage'] = $this->compileLogParams($aResult['UserIframeMessage'], $oAccount, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1543,6 +1570,7 @@ class Actions
|
|||
|
||||
$aResult['AllowGoogleSocial'] = (bool) $oConfig->Get('social', 'google_enable', false);
|
||||
$aResult['AllowGoogleSocialAuth'] = (bool) $oConfig->Get('social', 'google_enable_auth', true);
|
||||
$aResult['AllowGoogleSocialAuthFast'] = (bool) $oConfig->Get('social', 'google_enable_auth_fast', true);
|
||||
$aResult['AllowGoogleSocialDrive'] = (bool) $oConfig->Get('social', 'google_enable_drive', true);
|
||||
$aResult['AllowGoogleSocialPreview'] = (bool) $oConfig->Get('social', 'google_enable_preview', true);
|
||||
|
||||
|
|
@ -1553,6 +1581,7 @@ class Actions
|
|||
'' === \trim($oConfig->Get('social', 'google_client_id', '')) || '' === \trim($oConfig->Get('social', 'google_client_secret', '')))))
|
||||
{
|
||||
$aResult['AllowGoogleSocialAuth'] = false;
|
||||
$aResult['AllowGoogleSocialAuthFast'] = false;
|
||||
$aResult['AllowGoogleSocialDrive'] = false;
|
||||
$aResult['GoogleClientID'] = '';
|
||||
$aResult['GoogleApiKey'] = '';
|
||||
|
|
@ -1563,7 +1592,9 @@ class Actions
|
|||
$aResult['AllowGoogleSocialPreview'] = false;
|
||||
}
|
||||
|
||||
if ($aResult['AllowGoogleSocial'] && !$aResult['AllowGoogleSocialAuth'] && !$aResult['AllowGoogleSocialDrive'] && !$aResult['AllowGoogleSocialPreview'])
|
||||
if ($aResult['AllowGoogleSocial'] &&
|
||||
!$aResult['AllowGoogleSocialAuth'] && !$aResult['AllowGoogleSocialAuthFast'] &&
|
||||
!$aResult['AllowGoogleSocialDrive'] && !$aResult['AllowGoogleSocialPreview'])
|
||||
{
|
||||
$aResult['AllowGoogleSocial'] = false;
|
||||
}
|
||||
|
|
@ -1639,6 +1670,7 @@ class Actions
|
|||
|
||||
$aResult['AllowGoogleSocial'] = (bool) $oConfig->Get('social', 'google_enable', false);
|
||||
$aResult['AllowGoogleSocialAuth'] = (bool) $oConfig->Get('social', 'google_enable_auth', true);
|
||||
$aResult['AllowGoogleSocialAuthFast'] = (bool) $oConfig->Get('social', 'google_enable_auth_fast', true);
|
||||
$aResult['AllowGoogleSocialDrive'] = (bool) $oConfig->Get('social', 'google_enable_drive', true);
|
||||
$aResult['AllowGoogleSocialPreview'] = (bool) $oConfig->Get('social', 'google_enable_preview', true);
|
||||
|
||||
|
|
@ -2882,7 +2914,7 @@ class Actions
|
|||
|
||||
$mResult = false;
|
||||
|
||||
if (\RainLoop\Utils::IsOwnCloudLoggedIn() && \class_exists('\\OCP\\Files'))
|
||||
if (\RainLoop\Utils::IsOwnCloudLoggedIn() && \class_exists('OCP\Files'))
|
||||
{
|
||||
$sSaveFolder = $this->Config()->Get('labs', 'owncloud_save_folder', '');
|
||||
if (empty($sSaveFolder))
|
||||
|
|
@ -3644,6 +3676,7 @@ class Actions
|
|||
|
||||
$this->setConfigFromParams($oConfig, 'GoogleEnable', 'social', 'google_enable', 'bool');
|
||||
$this->setConfigFromParams($oConfig, 'GoogleEnableAuth', 'social', 'google_enable_auth', 'bool');
|
||||
$this->setConfigFromParams($oConfig, 'GoogleEnableAuthFast', 'social', 'google_enable_auth_fast', 'bool');
|
||||
$this->setConfigFromParams($oConfig, 'GoogleEnableDrive', 'social', 'google_enable_drive', 'bool');
|
||||
$this->setConfigFromParams($oConfig, 'GoogleEnablePreview', 'social', 'google_enable_preview', 'bool');
|
||||
$this->setConfigFromParams($oConfig, 'GoogleClientID', 'social', 'google_client_id', 'string');
|
||||
|
|
@ -4092,7 +4125,7 @@ class Actions
|
|||
/**
|
||||
* @return string
|
||||
*/
|
||||
private function rainloopRepo()
|
||||
private function rainLoopRepo()
|
||||
{
|
||||
$sUrl = APP_REP_PATH;
|
||||
if ('' !== $sUrl)
|
||||
|
|
@ -4281,7 +4314,7 @@ class Actions
|
|||
{
|
||||
$bRainLoopUpdatable = $this->rainLoopUpdatable();
|
||||
|
||||
$aResult = $this->getRepositoryDataByUrl($this->rainloopRepo(), $bReal);
|
||||
$aResult = $this->getRepositoryDataByUrl($this->rainLoopRepo(), $bReal);
|
||||
|
||||
$aSub = array();
|
||||
if (\is_array($aResult))
|
||||
|
|
@ -4555,7 +4588,7 @@ class Actions
|
|||
$bResult = false;
|
||||
if ('' !== $sRealFile)
|
||||
{
|
||||
$sUrl = $this->rainloopRepo().$sRealFile;
|
||||
$sUrl = $this->rainLoopRepo().$sRealFile;
|
||||
$sTmp = $this->downloadRemotePackageByUrl($sUrl);
|
||||
}
|
||||
|
||||
|
|
@ -6424,7 +6457,12 @@ class Actions
|
|||
*/
|
||||
public function DoQuota()
|
||||
{
|
||||
$this->initMailClientConnection();
|
||||
$oAccount = $this->initMailClientConnection();
|
||||
|
||||
if (!$this->GetCapa(false, \RainLoop\Enumerations\Capa::QUOTA, $oAccount))
|
||||
{
|
||||
return $this->DefaultResponse(__FUNCTION__, array(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
|
@ -7960,6 +7998,11 @@ class Actions
|
|||
$aResult[] = \RainLoop\Enumerations\Capa::RELOAD;
|
||||
}
|
||||
|
||||
if ($oConfig->Get('capa', 'quota', true))
|
||||
{
|
||||
$aResult[] = \RainLoop\Enumerations\Capa::QUOTA;
|
||||
}
|
||||
|
||||
if ($oConfig->Get('capa', 'settings', true))
|
||||
{
|
||||
$aResult[] = \RainLoop\Enumerations\Capa::SETTINGS;
|
||||
|
|
@ -9453,6 +9496,24 @@ class Actions
|
|||
$mResult['InReplyTo'] = $mResponse->InReplyTo();
|
||||
$mResult['References'] = $mResponse->References();
|
||||
|
||||
$fAdditionalDomReader = null;
|
||||
if (0 < \strlen($sHtml) && $this->Config()->Get('labs', 'emogrifier', false))
|
||||
{
|
||||
if (!\class_exists('Pelago\Emogrifier', false))
|
||||
{
|
||||
include_once APP_VERSION_ROOT_PATH.'app/libraries/emogrifier/Emogrifier.php';
|
||||
}
|
||||
|
||||
if (\class_exists('Pelago\Emogrifier', false))
|
||||
{
|
||||
$fAdditionalDomReader = function ($oDom) {
|
||||
$oEmogrifier = new \Pelago\Emogrifier();
|
||||
$oEmogrifier->preserveEncoding = false;
|
||||
return $oEmogrifier->emogrify($oDom);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$fAdditionalExternalFilter = null;
|
||||
if (!!$this->Config()->Get('labs', 'use_local_proxy_for_external_images', false))
|
||||
{
|
||||
|
|
@ -9465,9 +9526,13 @@ class Actions
|
|||
};
|
||||
}
|
||||
|
||||
// $this->Logger()->Write('---');
|
||||
// $this->Logger()->Write($sHtml);
|
||||
|
||||
$mResult['Html'] = 0 === \strlen($sHtml) ? '' : \MailSo\Base\HtmlUtils::ClearHtml(
|
||||
$sHtml, $bHasExternals, $mFoundedCIDs, $aContentLocationUrls, $mFoundedContentLocationUrls, false, false,
|
||||
$fAdditionalExternalFilter
|
||||
$fAdditionalExternalFilter, $fAdditionalDomReader,
|
||||
!!$this->Config()->Get('labs', 'try_to_detect_hidden_images', false)
|
||||
);
|
||||
|
||||
$mResult['ExternalProxy'] = null !== $fAdditionalExternalFilter;
|
||||
|
|
@ -9669,6 +9734,7 @@ class Actions
|
|||
'UidNext' => $mResponse->UidNext,
|
||||
'ThreadUid' => $mResponse->ThreadUid,
|
||||
'NewMessages' => $this->responseObject($mResponse->NewMessages),
|
||||
'Filtered' => $mResponse->Filtered,
|
||||
'Offset' => $mResponse->Offset,
|
||||
'Limit' => $mResponse->Limit,
|
||||
'Search' => $mResponse->Search
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class Api
|
|||
if ($bOne)
|
||||
{
|
||||
\RainLoop\Api::SetupDefaultMailSoConfig();
|
||||
|
||||
$bOne = \RainLoop\Api::RunResult();
|
||||
}
|
||||
}
|
||||
|
|
@ -97,6 +98,7 @@ class Api
|
|||
\MailSo\Config::$LargeThreadLimit =
|
||||
(int) \RainLoop\Api::Config()->Get('labs', 'imap_large_thread_limit', 50);
|
||||
|
||||
\MailSo\Config::$BoundaryPrefix = '_RainLoop_';
|
||||
|
||||
\MailSo\Config::$SystemLogger = \RainLoop\Api::Logger();
|
||||
|
||||
|
|
|
|||
|
|
@ -142,6 +142,7 @@ class Application extends \RainLoop\Config\AbstractConfig
|
|||
'composer' => array(true),
|
||||
'contacts' => array(true),
|
||||
'settings' => array(true),
|
||||
'quota' => array(true),
|
||||
'help' => array(true),
|
||||
'reload' => array(true),
|
||||
'search' => array(true),
|
||||
|
|
@ -164,6 +165,8 @@ class Application extends \RainLoop\Config\AbstractConfig
|
|||
'determine_user_language' => array(true, ''),
|
||||
'determine_user_domain' => array(false, ''),
|
||||
|
||||
'welcome_page' => array(false, ''),
|
||||
|
||||
'forgot_password_link_url' => array('', ''),
|
||||
'registration_link_url' => array('', ''),
|
||||
|
||||
|
|
@ -184,7 +187,7 @@ Values:
|
|||
|
||||
'defaults' => array(
|
||||
'view_editor_type' => array('Html', 'Editor mode used by default (Plain, Html, HtmlForced or PlainForced)'),
|
||||
'view_layout' => array(1, 'layout: 0 - no preview, 1 - side preview, 3 - bottom preview'),
|
||||
'view_layout' => array(1, 'layout: 0 - no preview, 1 - side preview, 2 - bottom preview'),
|
||||
'view_use_checkboxes' => array(true),
|
||||
'autologout' => array(30),
|
||||
'show_images' => array(false),
|
||||
|
|
@ -205,6 +208,7 @@ Values:
|
|||
Disabling this option is not recommended.'),
|
||||
|
||||
'time_offset' => array(0),
|
||||
'session_filter' => array(''),
|
||||
|
||||
'filename' => array('log-{date:Y-m-d}.txt',
|
||||
'Log filename.
|
||||
|
|
@ -246,6 +250,7 @@ Examples:
|
|||
'social' => array(
|
||||
'google_enable' => array(false, 'Google'),
|
||||
'google_enable_auth' => array(false),
|
||||
'google_enable_auth_fast' => array(false),
|
||||
'google_enable_drive' => array(false),
|
||||
'google_enable_preview' => array(false),
|
||||
'google_client_id' => array(''),
|
||||
|
|
@ -298,6 +303,7 @@ Enables caching in the system'),
|
|||
'allow_html_editor_source_button' => array(false),
|
||||
'allow_html_editor_biti_buttons' => array(false),
|
||||
'allow_ctrl_enter_on_compose' => array(false),
|
||||
'try_to_detect_hidden_images' => array(false),
|
||||
'hide_dangerous_actions' => array(false),
|
||||
'use_app_debug_js' => array(false),
|
||||
'use_app_debug_css' => array(false),
|
||||
|
|
@ -340,6 +346,7 @@ Enables caching in the system'),
|
|||
'fast_cache_memcache_expire' => array(43200),
|
||||
'use_local_proxy_for_external_images' => array(false),
|
||||
'startup_url' => array(''),
|
||||
'emogrifier' => array(true),
|
||||
'dev_email' => array(''),
|
||||
'dev_password' => array('')
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class Capa
|
|||
const SEARCH = 'SEARCH';
|
||||
const SEARCH_ADV = 'SEARCH_ADV';
|
||||
const SETTINGS = 'SETTINGS';
|
||||
const QUOTA = 'QUOTA';
|
||||
const HELP = 'HELP';
|
||||
const TEMPLATES = 'TEMPLATES';
|
||||
const MESSAGE_ACTIONS = 'MESSAGE_ACTIONS';
|
||||
|
|
|
|||
|
|
@ -304,6 +304,22 @@ abstract class AbstractPlugin
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sFile
|
||||
* @param bool $bAdminScope = false
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
protected function replaceTemplate($sFile, $bAdminScope = false)
|
||||
{
|
||||
if ($this->oPluginManager)
|
||||
{
|
||||
$this->oPluginManager->AddTemplate($this->sPath.'/'.$sFile, $bAdminScope);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sActionName
|
||||
* @param string $sFunctionName
|
||||
|
|
|
|||
|
|
@ -274,27 +274,24 @@ class Manager
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $aList
|
||||
* @param bool $bAdminScope = false
|
||||
* @return string
|
||||
*/
|
||||
public function CompileTemplate($bAdminScope = false)
|
||||
public function CompileTemplate(&$aList, $bAdminScope = false)
|
||||
{
|
||||
$sResult = '';
|
||||
if ($this->bIsEnabled)
|
||||
{
|
||||
$aTemplates = $bAdminScope ? $this->aAdminTemplates : $this->aTemplates;
|
||||
foreach ($aTemplates as $sFile)
|
||||
{
|
||||
if (file_exists($sFile))
|
||||
if (\file_exists($sFile))
|
||||
{
|
||||
$sTemplateName = substr(basename($sFile), 0, -5);
|
||||
$sResult .= '<script id="'.preg_replace('/[^a-zA-Z0-9]/', '', $sTemplateName).'" type="text/html" data-cfasync="false">'.
|
||||
$this->Actions()->ProcessTemplate($sTemplateName, file_get_contents($sFile)).'</script>';
|
||||
$sTemplateName = \substr(\basename($sFile), 0, -5);
|
||||
$aList[$sTemplateName] = $sFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class OwnCloudSuggestions implements \RainLoop\Providers\Suggestions\ISuggestion
|
|||
try
|
||||
{
|
||||
if ('' === $sQuery || !$oAccount || !\RainLoop\Utils::IsOwnCloudLoggedIn() ||
|
||||
!\class_exists('\\OCP\\Contacts') || !\OCP\Contacts::isEnabled()
|
||||
!\class_exists('OCP\Contacts') || !\OCP\Contacts::isEnabled()
|
||||
)
|
||||
{
|
||||
return $aResult;
|
||||
|
|
|
|||
|
|
@ -1220,11 +1220,23 @@ class ServiceActions
|
|||
*/
|
||||
public function compileTemplates($bAdmin = false, $bJsOutput = true)
|
||||
{
|
||||
$sHtml =
|
||||
\RainLoop\Utils::CompileTemplates(APP_VERSION_ROOT_PATH.'app/templates/Views/Components', $this->oActions, 'Component').
|
||||
\RainLoop\Utils::CompileTemplates(APP_VERSION_ROOT_PATH.'app/templates/Views/'.($bAdmin ? 'Admin' : 'User'), $this->oActions).
|
||||
\RainLoop\Utils::CompileTemplates(APP_VERSION_ROOT_PATH.'app/templates/Views/Common', $this->oActions).
|
||||
$this->oActions->Plugins()->CompileTemplate($bAdmin);
|
||||
$aTemplates = array();
|
||||
|
||||
\RainLoop\Utils::CompileTemplates($aTemplates, APP_VERSION_ROOT_PATH.'app/templates/Views/Components', 'Component');
|
||||
\RainLoop\Utils::CompileTemplates($aTemplates, APP_VERSION_ROOT_PATH.'app/templates/Views/'.($bAdmin ? 'Admin' : 'User'));
|
||||
\RainLoop\Utils::CompileTemplates($aTemplates, APP_VERSION_ROOT_PATH.'app/templates/Views/Common');
|
||||
|
||||
$this->oActions->Plugins()->CompileTemplate($aTemplates, $bAdmin);
|
||||
|
||||
$sHtml = '';
|
||||
foreach ($aTemplates as $sName => $sFile)
|
||||
{
|
||||
$sName = \preg_replace('/[^a-zA-Z0-9]/', '', $sName);
|
||||
$sHtml .= '<script id="'.$sName.'" type="text/html" data-cfasync="false">'.
|
||||
$this->oActions->ProcessTemplate($sName, \file_get_contents($sFile)).'</script>';
|
||||
}
|
||||
|
||||
unset($aTemplates);
|
||||
|
||||
return $bJsOutput ? 'window.rainloopTEMPLATES='.\MailSo\Base\Utils::Php2js(array($sHtml), $this->Logger()).';' : $sHtml;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class Utils
|
|||
define('_phpseclib_', true);
|
||||
}
|
||||
|
||||
if (!\class_exists('Crypt_RSA'))
|
||||
if (!\class_exists('Crypt_RSA', false))
|
||||
{
|
||||
include_once 'Crypt/RSA.php';
|
||||
\defined('CRYPT_RSA_MODE') || \define('CRYPT_RSA_MODE', CRYPT_RSA_MODE_INTERNAL);
|
||||
|
|
@ -470,30 +470,22 @@ class Utils
|
|||
}
|
||||
|
||||
/**
|
||||
* @param array $aList
|
||||
* @param string $sDirName
|
||||
* @param \RainLoop\Actions $oAction
|
||||
* @param string $sNameSuffix = ''
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function CompileTemplates($sDirName, $oAction, $sNameSuffix = '')
|
||||
public static function CompileTemplates(&$aList, $sDirName, $sNameSuffix = '')
|
||||
{
|
||||
$sResult = '';
|
||||
if (\file_exists($sDirName))
|
||||
{
|
||||
$aList = \RainLoop\Utils::FolderFiles($sDirName, '.html');
|
||||
$aFileList = \RainLoop\Utils::FolderFiles($sDirName, '.html');
|
||||
|
||||
foreach ($aList as $sName)
|
||||
foreach ($aFileList as $sName)
|
||||
{
|
||||
$sTemplateName = \substr($sName, 0, -5).$sNameSuffix;
|
||||
$sResult .= '<script id="'.\preg_replace('/[^a-zA-Z0-9]/', '', $sTemplateName).'" type="text/html" data-cfasync="false">'.
|
||||
$oAction->ProcessTemplate($sTemplateName, \file_get_contents($sDirName.'/'.$sName)).'</script>';
|
||||
$aList[$sTemplateName] = $sDirName.'/'.$sName;
|
||||
}
|
||||
|
||||
$sResult = \trim($sResult);
|
||||
}
|
||||
|
||||
return $sResult;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -539,14 +531,25 @@ class Utils
|
|||
public static function IsOwnCloud()
|
||||
{
|
||||
return isset($_ENV['RAINLOOP_OWNCLOUD']) && $_ENV['RAINLOOP_OWNCLOUD'] &&
|
||||
\class_exists('\\OC');
|
||||
\class_exists('OC');
|
||||
}
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function IsOwnCloudLoggedIn()
|
||||
{
|
||||
return self::IsOwnCloud() && \class_exists('\\OCP\\User') && \OCP\User::isLoggedIn();
|
||||
return self::IsOwnCloud() && \class_exists('OCP\User') && \OCP\User::isLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $sV
|
||||
* @param bool $bEncode = false
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function UrlEncode($sV, $bEncode = false)
|
||||
{
|
||||
return $bEncode ? \urlencode($sV) : $sV;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -557,7 +560,7 @@ class Utils
|
|||
$sAppPath = '';
|
||||
if (\RainLoop\Utils::IsOwnCloud())
|
||||
{
|
||||
if (\class_exists('\\OC_App'))
|
||||
if (\class_exists('OC_App'))
|
||||
{
|
||||
$sAppPath = \rtrim(\trim(\OC_App::getAppWebPath('rainloop')), '\\/').'/app/';
|
||||
}
|
||||
|
|
|
|||
73
rainloop/v/0.0.0/app/libraries/emogrifier/CHANGELOG.TXT
Normal file
73
rainloop/v/0.0.0/app/libraries/emogrifier/CHANGELOG.TXT
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
2012-05-01
|
||||
Made removal of invisible nodes operate in a case-insensitive manner... Thanks Juha P.!
|
||||
|
||||
2012-02-07
|
||||
Fixed some recent code introductions to use class constants rather than global constants.
|
||||
Fixed some recent code introductions to make it cleaner to read.
|
||||
|
||||
2012-01-31
|
||||
Fixed a bug that was introduced with the 2011-12-22 revision... Thanks Sagi L. and M. Bąkowski!
|
||||
Added extraction of <style> blocks within the HTML due to popular demand.
|
||||
Added several new pseudo-selectors (first-child, last-child, nth-child, and nth-of-type).
|
||||
|
||||
2011-12-22
|
||||
Fixed a bug that was overwriting existing inline styles from the original HTML... Thanks Sagi L.!
|
||||
|
||||
2011-10-26
|
||||
Added an option to allow you to output emogrified code without extended characters being turned into HTML entities.
|
||||
Moved static references to class attributes so they can be manipulated.
|
||||
Added the ability to clear out the (formerly) static cache when CSS is reloaded.
|
||||
|
||||
2011-10-13
|
||||
Fully fixed a bug introduced in 2011-06-08 where selectors at the beginning of the CSS would be parsed incorrectly... Thanks Thomas A.!
|
||||
|
||||
2011-08-03
|
||||
Fixed an error where an empty selector at the beginning of the CSS would cause a parse error on the next selector... Thanks Alexei T.!
|
||||
|
||||
2011-06-08
|
||||
Fixed an error where CSS @media types weren't being parsed correctly... Thanks Will W.!
|
||||
|
||||
2011-04-08
|
||||
Fixed errors in CSS->XPath conversion for adjacent sibling selectors and id/class combinations... Thanks Bob V.!
|
||||
|
||||
2010-09-03
|
||||
Added checks to invisible node removal to ensure that we don't try to remove non-existent child nodes of parents that have already been deleted
|
||||
|
||||
2010-07-26
|
||||
Fixed bug where '0' values were getting discarded because of php's empty() function... Thanks Scott!
|
||||
|
||||
2010-06-16
|
||||
Added static caching for less processing overhead in situations where multiple emogrification takes place
|
||||
|
||||
2010-05-18
|
||||
Fixed bug where full url filenames with protocols wouldn't get split improperly when we explode on ':'... Thanks Mark!
|
||||
Added two new attribute selectors
|
||||
|
||||
2009-11-04
|
||||
Explicitly declared static functions static to get rid of E_STRICT notices.
|
||||
|
||||
2009-10-29
|
||||
Fixed so that selectors appearing later in the CSS will have precedence over identical selectors appearing earlier.
|
||||
|
||||
2009-08-17
|
||||
Fixed CSS selector processing so that selectors are processed by precedence/specificity, and not just in order.
|
||||
|
||||
2009-08-13
|
||||
Added support for subset class values (e.g. "p.class1.class2"). Added better protection for bad css attributes.
|
||||
Fixed support for HTML entities.
|
||||
|
||||
2009-06-03
|
||||
Normalize existing CSS (style) attributes in the HTML before we process the CSS.
|
||||
Made it so that the display:none stripper doesn't require a trailing semi-colon.
|
||||
|
||||
2008-03-02
|
||||
Added licensing terms under the MIT License; Only remove unprocessable HTML tags if they exist in the array
|
||||
|
||||
2008-10-20
|
||||
Fixed bug with bad variable name... Thanks Thomas!
|
||||
|
||||
2008-08-18
|
||||
Added lines instructing DOMDocument to attempt to normalize HTML before processing
|
||||
|
||||
2008-08-10
|
||||
Fixed CSS comment stripping regex to add PCRE_DOTALL (changed from '/\/\*.*\*\//U' to '/\/\*.*\*\//sU')
|
||||
981
rainloop/v/0.0.0/app/libraries/emogrifier/Emogrifier.php
Normal file
981
rainloop/v/0.0.0/app/libraries/emogrifier/Emogrifier.php
Normal file
|
|
@ -0,0 +1,981 @@
|
|||
<?php
|
||||
namespace Pelago;
|
||||
|
||||
/**
|
||||
* This class provides functions for converting CSS styles into inline style attributes in your HTML code.
|
||||
*
|
||||
* For more information, please see the README.md file.
|
||||
*
|
||||
* @author Cameron Brooks
|
||||
* @author Jaime Prado
|
||||
* @author Roman Ožana <ozana@omdesign.cz>
|
||||
*/
|
||||
class Emogrifier
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
const ENCODING = 'UTF-8';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_KEY_CSS = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_KEY_SELECTOR = 1;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_KEY_XPATH = 2;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
const CACHE_KEY_CSS_DECLARATION_BLOCK = 3;
|
||||
|
||||
/**
|
||||
* for calculating nth-of-type and nth-child selectors
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const INDEX = 0;
|
||||
|
||||
/**
|
||||
* for calculating nth-of-type and nth-child selectors
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MULTIPLIER = 1;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
const ID_ATTRIBUTE_MATCHER = '/(\\w+)?\\#([\\w\\-]+)/';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
const CLASS_ATTRIBUTE_MATCHER = '/(\\w+|[\\*\\]])?((\\.[\\w\\-]+)+)/';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $html = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $css = '';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $unprocessableHtmlTags = array('wbr');
|
||||
|
||||
/**
|
||||
* @var array[]
|
||||
*/
|
||||
private $caches = array(
|
||||
self::CACHE_KEY_CSS => array(),
|
||||
self::CACHE_KEY_SELECTOR => array(),
|
||||
self::CACHE_KEY_XPATH => array(),
|
||||
self::CACHE_KEY_CSS_DECLARATION_BLOCK => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* the visited nodes with the XPath paths as array keys
|
||||
*
|
||||
* @var \DOMNode[]
|
||||
*/
|
||||
private $visitedNodes = array();
|
||||
|
||||
/**
|
||||
* the styles to apply to the nodes with the XPath paths as array keys for the outer array
|
||||
* and the attribute names/values as key/value pairs for the inner array
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
private $styleAttributesForNodes = array();
|
||||
|
||||
/**
|
||||
* Determines whether the "style" attributes of tags in the the HTML passed to this class should be preserved.
|
||||
* If set to false, the value of the style attributes will be discarded.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $isInlineStyleAttributesParsingEnabled = true;
|
||||
|
||||
/**
|
||||
* Determines whether the <style> blocks in the HTML passed to this class should be parsed.
|
||||
*
|
||||
* If set to true, the <style> blocks will be removed from the HTML and their contents will be applied to the HTML
|
||||
* via inline styles.
|
||||
*
|
||||
* If set to false, the <style> blocks will be left as they are in the HTML.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $isStyleBlocksParsingEnabled = true;
|
||||
|
||||
/**
|
||||
* This attribute applies to the case where you want to preserve your original text encoding.
|
||||
*
|
||||
* By default, emogrifier translates your text into HTML entities for two reasons:
|
||||
*
|
||||
* 1. Because of client incompatibilities, it is better practice to send out HTML entities
|
||||
* rather than unicode over email.
|
||||
*
|
||||
* 2. It translates any illegal XML characters that DOMDocument cannot work with.
|
||||
*
|
||||
* If you would like to preserve your original encoding, set this attribute to true.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $preserveEncoding = false;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string $html the HTML to emogrify, must be UTF-8-encoded
|
||||
* @param string $css the CSS to merge, must be UTF-8-encoded
|
||||
*/
|
||||
public function __construct($html = '', $css = '')
|
||||
{
|
||||
$this->setHtml($html);
|
||||
$this->setCss($css);
|
||||
}
|
||||
|
||||
/**
|
||||
* The destructor.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->purgeVisitedNodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTML to emogrify.
|
||||
*
|
||||
* @param string $html the HTML to emogrify, must be UTF-8-encoded
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setHtml($html)
|
||||
{
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the CSS to merge with the HTML.
|
||||
*
|
||||
* @param string $css the CSS to merge, must be UTF-8-encoded
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setCss($css)
|
||||
{
|
||||
$this->css = $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the CSS you submit to the HTML you submit.
|
||||
*
|
||||
* This method places the CSS inline.
|
||||
*
|
||||
* @param \DOMDocument $dom = null
|
||||
*
|
||||
* @return string|\DOMDocument
|
||||
*
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
public function emogrify($dom = null)
|
||||
{
|
||||
if ($this->html === '' && !$dom) {
|
||||
throw new \BadMethodCallException('Please set some HTML first before calling emogrify.', 1390393096);
|
||||
}
|
||||
|
||||
$xmlDocument = $dom ? $dom : $this->createXmlDocument();
|
||||
$xpath = new \DOMXPath($xmlDocument);
|
||||
$this->clearAllCaches();
|
||||
|
||||
// Before be begin processing the CSS file, parse the document and normalize all existing CSS attributes.
|
||||
// This changes 'DISPLAY: none' to 'display: none'.
|
||||
// We wouldn't have to do this if DOMXPath supported XPath 2.0.
|
||||
// Also store a reference of nodes with existing inline styles so we don't overwrite them.
|
||||
$this->purgeVisitedNodes();
|
||||
|
||||
$nodesWithStyleAttributes = @$xpath->query('//*[@style]');
|
||||
if ($nodesWithStyleAttributes) {
|
||||
/** @var \DOMElement $node */
|
||||
foreach ($nodesWithStyleAttributes as $node) {
|
||||
if ($this->isInlineStyleAttributesParsingEnabled) {
|
||||
$this->normalizeStyleAttributes($node);
|
||||
} else {
|
||||
$node->removeAttribute('style');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// grab any existing style blocks from the html and append them to the existing CSS
|
||||
// (these blocks should be appended so as to have precedence over conflicting styles in the existing CSS)
|
||||
$allCss = $this->css;
|
||||
|
||||
if ($this->isStyleBlocksParsingEnabled) {
|
||||
$allCss .= $this->getCssFromAllStyleNodes($xpath);
|
||||
}
|
||||
|
||||
$cssParts = $this->splitCssAndMediaQuery($allCss);
|
||||
|
||||
$cssKey = md5($cssParts['css']);
|
||||
if (!isset($this->caches[self::CACHE_KEY_CSS][$cssKey])) {
|
||||
// process the CSS file for selectors and definitions
|
||||
preg_match_all('/(?:^|[\\s^{}]*)([^{]+){([^}]*)}/mis', $cssParts['css'], $matches, PREG_SET_ORDER);
|
||||
|
||||
$allSelectors = array();
|
||||
foreach ($matches as $key => $selectorString) {
|
||||
// if there is a blank definition, skip
|
||||
if (!strlen(trim($selectorString[2]))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// else split by commas and duplicate attributes so we can sort by selector precedence
|
||||
$selectors = explode(',', $selectorString[1]);
|
||||
foreach ($selectors as $selector) {
|
||||
// don't process pseudo-elements and behavioral (dynamic) pseudo-classes;
|
||||
// only allow structural pseudo-classes
|
||||
if (strpos($selector, ':') !== false && !preg_match('/:\\S+\\-(child|type)\\(/i', $selector)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allSelectors[] = array('selector' => trim($selector),
|
||||
'attributes' => trim($selectorString[2]),
|
||||
// keep track of where it appears in the file, since order is important
|
||||
'line' => $key,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// now sort the selectors by precedence
|
||||
usort($allSelectors, array($this,'sortBySelectorPrecedence'));
|
||||
|
||||
$this->caches[self::CACHE_KEY_CSS][$cssKey] = $allSelectors;
|
||||
}
|
||||
|
||||
foreach ($this->caches[self::CACHE_KEY_CSS][$cssKey] as $value) {
|
||||
// query the body for the xpath selector
|
||||
$nodesMatchingCssSelectors = @$xpath->query($this->translateCssToXpath($value['selector']));
|
||||
if (!$nodesMatchingCssSelectors)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var \DOMElement $node */
|
||||
foreach ($nodesMatchingCssSelectors as $node) {
|
||||
// if it has a style attribute, get it, process it, and append (overwrite) new stuff
|
||||
if ($node->hasAttribute('style')) {
|
||||
// break it up into an associative array
|
||||
$oldStyleDeclarations = $this->parseCssDeclarationBlock($node->getAttribute('style'));
|
||||
} else {
|
||||
$oldStyleDeclarations = array();
|
||||
}
|
||||
$newStyleDeclarations = $this->parseCssDeclarationBlock($value['attributes']);
|
||||
$node->setAttribute(
|
||||
'style',
|
||||
$this->generateStyleStringFromDeclarationsArrays($oldStyleDeclarations, $newStyleDeclarations)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isInlineStyleAttributesParsingEnabled) {
|
||||
$this->fillStyleAttributesWithMergedStyles();
|
||||
}
|
||||
|
||||
// This removes styles from your email that contain display:none.
|
||||
// We need to look for display:none, but we need to do a case-insensitive search. Since DOMDocument only
|
||||
// supports XPath 1.0, lower-case() isn't available to us. We've thus far only set attributes to lowercase,
|
||||
// not attribute values. Consequently, we need to translate() the letters that would be in 'NONE' ("NOE")
|
||||
// to lowercase.
|
||||
$nodesWithStyleDisplayNone = @$xpath->query(
|
||||
'//*[contains(translate(translate(@style," ",""),"NOE","noe"),"display:none")]'
|
||||
);
|
||||
// The checks on parentNode and is_callable below ensure that if we've deleted the parent node,
|
||||
// we don't try to call removeChild on a nonexistent child node
|
||||
if ($nodesWithStyleDisplayNone && $nodesWithStyleDisplayNone->length > 0) {
|
||||
/** @var \DOMNode $node */
|
||||
foreach ($nodesWithStyleDisplayNone as $node) {
|
||||
if ($node->parentNode && is_callable(array($node->parentNode,'removeChild'))) {
|
||||
$node->parentNode->removeChild($node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->copyCssWithMediaToStyleNode($cssParts, $xmlDocument);
|
||||
|
||||
if ($dom)
|
||||
{
|
||||
return $xmlDocument;
|
||||
}
|
||||
|
||||
if ($this->preserveEncoding) {
|
||||
return mb_convert_encoding($xmlDocument->saveHTML(), self::ENCODING, 'HTML-ENTITIES');
|
||||
} else {
|
||||
return $xmlDocument->saveHTML();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the parsing of inline styles.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disableInlineStyleAttributesParsing()
|
||||
{
|
||||
$this->isInlineStyleAttributesParsingEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the parsing of <style> blocks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disableStyleBlocksParsing()
|
||||
{
|
||||
$this->isStyleBlocksParsingEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all caches.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function clearAllCaches()
|
||||
{
|
||||
$this->clearCache(self::CACHE_KEY_CSS);
|
||||
$this->clearCache(self::CACHE_KEY_SELECTOR);
|
||||
$this->clearCache(self::CACHE_KEY_XPATH);
|
||||
$this->clearCache(self::CACHE_KEY_CSS_DECLARATION_BLOCK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears a single cache by key.
|
||||
*
|
||||
* @param int $key the cache key, must be CACHE_KEY_CSS, CACHE_KEY_SELECTOR, CACHE_KEY_XPATH
|
||||
* or CACHE_KEY_CSS_DECLARATION_BLOCK
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
private function clearCache($key)
|
||||
{
|
||||
$allowedCacheKeys = array(
|
||||
self::CACHE_KEY_CSS,
|
||||
self::CACHE_KEY_SELECTOR,
|
||||
self::CACHE_KEY_XPATH,
|
||||
self::CACHE_KEY_CSS_DECLARATION_BLOCK,
|
||||
);
|
||||
if (!in_array($key, $allowedCacheKeys, true)) {
|
||||
throw new \InvalidArgumentException('Invalid cache key: ' . $key, 1391822035);
|
||||
}
|
||||
|
||||
$this->caches[$key] = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges the visited nodes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function purgeVisitedNodes()
|
||||
{
|
||||
$this->visitedNodes = array();
|
||||
$this->styleAttributesForNodes = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a tag for removal.
|
||||
*
|
||||
* There are some HTML tags that DOMDocument cannot process, and it will throw an error if it encounters them.
|
||||
* In particular, DOMDocument will complain if you try to use HTML5 tags in an XHTML document.
|
||||
*
|
||||
* Note: The tags will not be removed if they have any content.
|
||||
*
|
||||
* @param string $tagName the tag name, e.g., "p"
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addUnprocessableHtmlTag($tagName)
|
||||
{
|
||||
$this->unprocessableHtmlTags[] = $tagName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a tag from the removal list.
|
||||
*
|
||||
* @param string $tagName the tag name, e.g., "p"
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function removeUnprocessableHtmlTag($tagName)
|
||||
{
|
||||
$key = array_search($tagName, $this->unprocessableHtmlTags, true);
|
||||
if ($key !== false) {
|
||||
unset($this->unprocessableHtmlTags[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the value of the "style" attribute and saves it.
|
||||
*
|
||||
* @param \DOMElement $node
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function normalizeStyleAttributes(\DOMElement $node)
|
||||
{
|
||||
$normalizedOriginalStyle = preg_replace_callback(
|
||||
'/[A-z\\-]+(?=\\:)/S',
|
||||
function (array $m) {
|
||||
return strtolower($m[0]);
|
||||
},
|
||||
$node->getAttribute('style')
|
||||
);
|
||||
|
||||
// in order to not overwrite existing style attributes in the HTML, we
|
||||
// have to save the original HTML styles
|
||||
$nodePath = $node->getNodePath();
|
||||
if (!isset($this->styleAttributesForNodes[$nodePath])) {
|
||||
$this->styleAttributesForNodes[$nodePath] = $this->parseCssDeclarationBlock($normalizedOriginalStyle);
|
||||
$this->visitedNodes[$nodePath] = $node;
|
||||
}
|
||||
|
||||
$node->setAttribute('style', $normalizedOriginalStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges styles from styles attributes and style nodes and applies them to the attribute nodes
|
||||
*
|
||||
* return @void
|
||||
*/
|
||||
private function fillStyleAttributesWithMergedStyles()
|
||||
{
|
||||
foreach ($this->styleAttributesForNodes as $nodePath => $styleAttributesForNode) {
|
||||
$node = $this->visitedNodes[$nodePath];
|
||||
$currentStyleAttributes = $this->parseCssDeclarationBlock($node->getAttribute('style'));
|
||||
$node->setAttribute(
|
||||
'style',
|
||||
$this->generateStyleStringFromDeclarationsArrays(
|
||||
$currentStyleAttributes,
|
||||
$styleAttributesForNode
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method merges old or existing name/value array with new name/value array
|
||||
* and then generates a string of the combined style suitable for placing inline.
|
||||
* This becomes the single point for CSS string generation allowing for consistent
|
||||
* CSS output no matter where the CSS originally came from.
|
||||
*
|
||||
* @param string[] $oldStyles
|
||||
* @param string[] $newStyles
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generateStyleStringFromDeclarationsArrays(array $oldStyles, array $newStyles)
|
||||
{
|
||||
$combinedStyles = array_merge($oldStyles, $newStyles);
|
||||
$style = '';
|
||||
foreach ($combinedStyles as $attributeName => $attributeValue) {
|
||||
$style .= (strtolower(trim($attributeName)) . ': ' . trim($attributeValue) . '; ');
|
||||
}
|
||||
return trim($style);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Copies the media part from CSS array parts to $xmlDocument.
|
||||
*
|
||||
* @param string[] $cssParts
|
||||
* @param \DOMDocument $xmlDocument
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function copyCssWithMediaToStyleNode(array $cssParts, \DOMDocument $xmlDocument)
|
||||
{
|
||||
if (isset($cssParts['media']) && $cssParts['media'] !== '') {
|
||||
$this->addStyleElementToDocument($xmlDocument, $cssParts['media']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CSS content.
|
||||
*
|
||||
* @param \DOMXPath $xpath
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getCssFromAllStyleNodes(\DOMXPath $xpath)
|
||||
{
|
||||
$styleNodes = @$xpath->query('//style');
|
||||
|
||||
if (!$styleNodes) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$css = '';
|
||||
/** @var \DOMNode $styleNode */
|
||||
foreach ($styleNodes as $styleNode) {
|
||||
$css .= "\n\n" . $styleNode->nodeValue;
|
||||
$styleNode->parentNode->removeChild($styleNode);
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a style element with $css to $document.
|
||||
*
|
||||
* This method is protected to allow overriding.
|
||||
*
|
||||
* @see https://github.com/jjriv/emogrifier/issues/103
|
||||
*
|
||||
* @param \DOMDocument $document
|
||||
* @param string $css
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function addStyleElementToDocument(\DOMDocument $document, $css)
|
||||
{
|
||||
$styleElement = $document->createElement('style', $css);
|
||||
$styleAttribute = $document->createAttribute('type');
|
||||
$styleAttribute->value = 'text/css';
|
||||
$styleElement->appendChild($styleAttribute);
|
||||
|
||||
$head = $this->getOrCreateHeadElement($document);
|
||||
$head->appendChild($styleElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the existing or creates a new head element in $document.
|
||||
*
|
||||
* @param \DOMDocument $document
|
||||
*
|
||||
* @return \DOMNode the head element
|
||||
*/
|
||||
private function getOrCreateHeadElement(\DOMDocument $document)
|
||||
{
|
||||
$head = $document->getElementsByTagName('head')->item(0);
|
||||
|
||||
if ($head === null) {
|
||||
$head = $document->createElement('head');
|
||||
$html = $document->getElementsByTagName('html')->item(0);
|
||||
$html->insertBefore($head, $document->getElementsByTagName('body')->item(0));
|
||||
}
|
||||
|
||||
return $head;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits input CSS code to an array where:
|
||||
*
|
||||
* - key "css" will be contains clean CSS code
|
||||
* - key "media" will be contains all valuable media queries
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* The CSS code
|
||||
*
|
||||
* "@import "file.css"; h1 { color:red; } @media { h1 {}} @media tv { h1 {}}"
|
||||
*
|
||||
* will be parsed into the following array:
|
||||
*
|
||||
* "css" => "h1 { color:red; }"
|
||||
* "media" => "@media { h1 {}}"
|
||||
*
|
||||
* @param string $css
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function splitCssAndMediaQuery($css)
|
||||
{
|
||||
$media = '';
|
||||
|
||||
$css = preg_replace_callback(
|
||||
'#@media\\s+(?:only\\s)?(?:[\\s{\\(]|screen|all)\\s?[^{]+{.*}\\s*}\\s*#misU',
|
||||
function ($matches) use (&$media) {
|
||||
$media .= $matches[0];
|
||||
},
|
||||
$css
|
||||
);
|
||||
|
||||
// filter the CSS
|
||||
$search = array(
|
||||
// get rid of css comment code
|
||||
'/\\/\\*.*\\*\\//sU',
|
||||
// strip out any import directives
|
||||
'/^\\s*@import\\s[^;]+;/misU',
|
||||
// strip remains media enclosures
|
||||
'/^\\s*@media\\s[^{]+{(.*)}\\s*}\\s/misU',
|
||||
);
|
||||
|
||||
$replace = array(
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
);
|
||||
|
||||
// clean CSS before output
|
||||
$css = preg_replace($search, $replace, $css);
|
||||
|
||||
return array('css' => $css, 'media' => $media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DOMDocument instance with the current HTML.
|
||||
*
|
||||
* @return \DOMDocument
|
||||
*/
|
||||
private function createXmlDocument()
|
||||
{
|
||||
$xmlDocument = new \DOMDocument;
|
||||
$xmlDocument->encoding = self::ENCODING;
|
||||
$xmlDocument->strictErrorChecking = false;
|
||||
$xmlDocument->formatOutput = true;
|
||||
$libXmlState = libxml_use_internal_errors(true);
|
||||
@$xmlDocument->loadHTML($this->getUnifiedHtml());
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($libXmlState);
|
||||
$xmlDocument->normalizeDocument();
|
||||
|
||||
return $xmlDocument;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML with the non-ASCII characters converts into HTML entities and the unprocessable
|
||||
* HTML tags removed.
|
||||
*
|
||||
* @return string the unified HTML
|
||||
*
|
||||
* @throws \BadMethodCallException
|
||||
*/
|
||||
private function getUnifiedHtml()
|
||||
{
|
||||
if (!empty($this->unprocessableHtmlTags)) {
|
||||
$unprocessableHtmlTags = implode('|', $this->unprocessableHtmlTags);
|
||||
$bodyWithoutUnprocessableTags = preg_replace(
|
||||
'/<\\/?(' . $unprocessableHtmlTags . ')[^>]*>/i',
|
||||
'',
|
||||
$this->html
|
||||
);
|
||||
} else {
|
||||
$bodyWithoutUnprocessableTags = $this->html;
|
||||
}
|
||||
|
||||
return mb_convert_encoding($bodyWithoutUnprocessableTags, 'HTML-ENTITIES', self::ENCODING);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $a
|
||||
* @param string[] $b
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function sortBySelectorPrecedence(array $a, array $b)
|
||||
{
|
||||
$precedenceA = $this->getCssSelectorPrecedence($a['selector']);
|
||||
$precedenceB = $this->getCssSelectorPrecedence($b['selector']);
|
||||
|
||||
// We want these sorted in ascending order so selectors with lesser precedence get processed first and
|
||||
// selectors with greater precedence get sorted last.
|
||||
$precedenceForEquals = ($a['line'] < $b['line'] ? -1 : 1);
|
||||
$precedenceForNotEquals = ($precedenceA < $precedenceB ? -1 : 1);
|
||||
return ($precedenceA === $precedenceB) ? $precedenceForEquals : $precedenceForNotEquals;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $selector
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function getCssSelectorPrecedence($selector)
|
||||
{
|
||||
$selectorKey = md5($selector);
|
||||
if (!isset($this->caches[self::CACHE_KEY_SELECTOR][$selectorKey])) {
|
||||
$precedence = 0;
|
||||
$value = 100;
|
||||
// ids: worth 100, classes: worth 10, elements: worth 1
|
||||
$search = array('\\#','\\.','');
|
||||
|
||||
foreach ($search as $s) {
|
||||
if (trim($selector) === '') {
|
||||
break;
|
||||
}
|
||||
$number = 0;
|
||||
$selector = preg_replace('/' . $s . '\\w+/', '', $selector, -1, $number);
|
||||
$precedence += ($value * $number);
|
||||
$value /= 10;
|
||||
}
|
||||
$this->caches[self::CACHE_KEY_SELECTOR][$selectorKey] = $precedence;
|
||||
}
|
||||
|
||||
return $this->caches[self::CACHE_KEY_SELECTOR][$selectorKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Right now, we support all CSS 1 selectors and most CSS2/3 selectors.
|
||||
*
|
||||
* @see http://plasmasturm.org/log/444/
|
||||
*
|
||||
* @param string $paramCssSelector
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function translateCssToXpath($paramCssSelector)
|
||||
{
|
||||
$cssSelector = ' ' . $paramCssSelector . ' ';
|
||||
$cssSelector = preg_replace_callback(
|
||||
'/\\s+\\w+\\s+/',
|
||||
function (array $matches) {
|
||||
return strtolower($matches[0]);
|
||||
},
|
||||
$cssSelector
|
||||
);
|
||||
$cssSelector = trim($cssSelector);
|
||||
$xpathKey = md5($cssSelector);
|
||||
if (!isset($this->caches[self::CACHE_KEY_XPATH][$xpathKey])) {
|
||||
// returns an Xpath selector
|
||||
$search = array(
|
||||
// Matches any element that is a child of parent.
|
||||
'/\\s+>\\s+/',
|
||||
// Matches any element that is an adjacent sibling.
|
||||
'/\\s+\\+\\s+/',
|
||||
// Matches any element that is a descendant of an parent element element.
|
||||
'/\\s+/',
|
||||
// first-child pseudo-selector
|
||||
'/([^\\/]+):first-child/i',
|
||||
// last-child pseudo-selector
|
||||
'/([^\\/]+):last-child/i',
|
||||
// Matches attribute only selector
|
||||
'/^\\[(\\w+)\\]/',
|
||||
// Matches element with attribute
|
||||
'/(\\w)\\[(\\w+)\\]/',
|
||||
// Matches element with EXACT attribute
|
||||
'/(\\w)\\[(\\w+)\\=[\'"]?(\\w+)[\'"]?\\]/',
|
||||
);
|
||||
$replace = array(
|
||||
'/',
|
||||
'/following-sibling::*[1]/self::',
|
||||
'//',
|
||||
'*[1]/self::\\1',
|
||||
'*[last()]/self::\\1',
|
||||
'*[@\\1]',
|
||||
'\\1[@\\2]',
|
||||
'\\1[@\\2="\\3"]',
|
||||
);
|
||||
|
||||
$cssSelector = '//' . preg_replace($search, $replace, $cssSelector);
|
||||
|
||||
$cssSelector = preg_replace_callback(
|
||||
self::ID_ATTRIBUTE_MATCHER,
|
||||
array($this, 'matchIdAttributes'),
|
||||
$cssSelector
|
||||
);
|
||||
$cssSelector = preg_replace_callback(
|
||||
self::CLASS_ATTRIBUTE_MATCHER,
|
||||
array($this, 'matchClassAttributes'),
|
||||
$cssSelector
|
||||
);
|
||||
|
||||
// Advanced selectors are going to require a bit more advanced emogrification.
|
||||
// When we required PHP 5.3, we could do this with closures.
|
||||
$cssSelector = preg_replace_callback(
|
||||
'/([^\\/]+):nth-child\\(\\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
|
||||
array($this, 'translateNthChild'),
|
||||
$cssSelector
|
||||
);
|
||||
$cssSelector = preg_replace_callback(
|
||||
'/([^\\/]+):nth-of-type\\(\s*(odd|even|[+\\-]?\\d|[+\\-]?\\d?n(\\s*[+\\-]\\s*\\d)?)\\s*\\)/i',
|
||||
array($this, 'translateNthOfType'),
|
||||
$cssSelector
|
||||
);
|
||||
|
||||
$this->caches[self::CACHE_KEY_SELECTOR][$xpathKey] = $cssSelector;
|
||||
}
|
||||
return $this->caches[self::CACHE_KEY_SELECTOR][$xpathKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $match
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function matchIdAttributes(array $match)
|
||||
{
|
||||
return (strlen($match[1]) ? $match[1] : '*') . '[@id="' . $match[2] . '"]';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $match
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function matchClassAttributes(array $match)
|
||||
{
|
||||
return (strlen($match[1]) ? $match[1] : '*') . '[contains(concat(" ",@class," "),concat(" ","' .
|
||||
implode(
|
||||
'"," "))][contains(concat(" ",@class," "),concat(" ","',
|
||||
explode('.', substr($match[2], 1))
|
||||
) . '"," "))]';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $match
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function translateNthChild(array $match)
|
||||
{
|
||||
$result = $this->parseNth($match);
|
||||
|
||||
if (isset($result[self::MULTIPLIER])) {
|
||||
if ($result[self::MULTIPLIER] < 0) {
|
||||
$result[self::MULTIPLIER] = abs($result[self::MULTIPLIER]);
|
||||
return sprintf(
|
||||
'*[(last() - position()) mod %u = %u]/self::%s',
|
||||
$result[self::MULTIPLIER],
|
||||
$result[self::INDEX],
|
||||
$match[1]
|
||||
);
|
||||
} else {
|
||||
return sprintf(
|
||||
'*[position() mod %u = %u]/self::%s',
|
||||
$result[self::MULTIPLIER],
|
||||
$result[self::INDEX],
|
||||
$match[1]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return sprintf('*[%u]/self::%s', $result[self::INDEX], $match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $match
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function translateNthOfType(array $match)
|
||||
{
|
||||
$result = $this->parseNth($match);
|
||||
|
||||
if (isset($result[self::MULTIPLIER])) {
|
||||
if ($result[self::MULTIPLIER] < 0) {
|
||||
$result[self::MULTIPLIER] = abs($result[self::MULTIPLIER]);
|
||||
return sprintf(
|
||||
'%s[(last() - position()) mod %u = %u]',
|
||||
$match[1],
|
||||
$result[self::MULTIPLIER],
|
||||
$result[self::INDEX]
|
||||
);
|
||||
} else {
|
||||
return sprintf(
|
||||
'%s[position() mod %u = %u]',
|
||||
$match[1],
|
||||
$result[self::MULTIPLIER],
|
||||
$result[self::INDEX]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return sprintf('%s[%u]', $match[1], $result[self::INDEX]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $match
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
private function parseNth(array $match)
|
||||
{
|
||||
if (in_array(strtolower($match[2]), array('even','odd'), true)) {
|
||||
$index = strtolower($match[2]) === 'even' ? 0 : 1;
|
||||
return array(self::MULTIPLIER => 2, self::INDEX => $index);
|
||||
} elseif (stripos($match[2], 'n') === false) {
|
||||
// if there is a multiplier
|
||||
$index = (int) str_replace(' ', '', $match[2]);
|
||||
return array(self::INDEX => $index);
|
||||
} else {
|
||||
if (isset($match[3])) {
|
||||
$multipleTerm = str_replace($match[3], '', $match[2]);
|
||||
$index = (int) str_replace(' ', '', $match[3]);
|
||||
} else {
|
||||
$multipleTerm = $match[2];
|
||||
$index = 0;
|
||||
}
|
||||
|
||||
$multiplier = (int) str_ireplace('n', '', $multipleTerm);
|
||||
|
||||
if (!strlen($multiplier)) {
|
||||
$multiplier = 1;
|
||||
} elseif ($multiplier === 0) {
|
||||
return array(self::INDEX => $index);
|
||||
} else {
|
||||
$multiplier = (int) $multiplier;
|
||||
}
|
||||
|
||||
while ($index < 0) {
|
||||
$index += abs($multiplier);
|
||||
}
|
||||
|
||||
return array(self::MULTIPLIER => $multiplier, self::INDEX => $index);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a CSS declaration block into property name/value pairs.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* The declaration block
|
||||
*
|
||||
* "color: #000; font-weight: bold;"
|
||||
*
|
||||
* will be parsed into the following array:
|
||||
*
|
||||
* "color" => "#000"
|
||||
* "font-weight" => "bold"
|
||||
*
|
||||
* @param string $cssDeclarationBlock the CSS declaration block without the curly braces, may be empty
|
||||
*
|
||||
* @return string[]
|
||||
* the CSS declarations with the property names as array keys and the property values as array values
|
||||
*/
|
||||
private function parseCssDeclarationBlock($cssDeclarationBlock)
|
||||
{
|
||||
if (isset($this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock])) {
|
||||
return $this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock];
|
||||
}
|
||||
|
||||
$properties = array();
|
||||
$declarations = explode(';', $cssDeclarationBlock);
|
||||
foreach ($declarations as $declaration) {
|
||||
$matches = array();
|
||||
if (!preg_match('/ *([A-Za-z\\-]+) *: *([^;]+) */', $declaration, $matches)) {
|
||||
continue;
|
||||
}
|
||||
$propertyName = strtolower($matches[1]);
|
||||
$propertyValue = $matches[2];
|
||||
$properties[$propertyName] = $propertyValue;
|
||||
}
|
||||
$this->caches[self::CACHE_KEY_CSS_DECLARATION_BLOCK][$cssDeclarationBlock] = $properties;
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
21
rainloop/v/0.0.0/app/libraries/emogrifier/LICENSE
Normal file
21
rainloop/v/0.0.0/app/libraries/emogrifier/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
Emogrifier is copyright (c) 2008-2014 Pelago and licensed under the MIT license.
|
||||
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
185
rainloop/v/0.0.0/app/libraries/emogrifier/README.md
Normal file
185
rainloop/v/0.0.0/app/libraries/emogrifier/README.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Emogrifier
|
||||
|
||||
[](https://travis-ci.org/jjriv/emogrifier)
|
||||
[](https://packagist.org/packages/pelago/emogrifier)
|
||||
[](https://packagist.org/packages/pelago/emogrifier)
|
||||
[](https://packagist.org/packages/pelago/emogrifier)
|
||||
[](https://packagist.org/packages/pelago/emogrifier)
|
||||
|
||||
_n. e•mog•ri•fi•er [\ē-'mä-grƏ-,fī-Ər\] - a utility for changing completely the nature or appearance of HTML email,
|
||||
esp. in a particularly fantastic or bizarre manner_
|
||||
|
||||
Emogrifier converts CSS styles into inline style attributes in your HTML code. This ensures proper display on email
|
||||
and mobile device readers that lack stylesheet support.
|
||||
|
||||
This utility was developed as part of [Intervals](http://www.myintervals.com/) to deal with the problems posed by
|
||||
certain email clients (namely Outlook 2007 and Google Gmail) when it comes to the way they handle styling contained
|
||||
in HTML emails. As many web developers and designers already know, certain email clients are notorious for their
|
||||
lack of CSS support. While attempts are being made to develop common
|
||||
[email standards](http://www.email-standards.org/), implementation is still a ways off.
|
||||
|
||||
The primary problem with uncooperative email clients is that most tend to only regard inline CSS, discarding all
|
||||
`<style>` elements and links to stylesheets in `<link>` elements. Emogrifier solves this problem by converting CSS
|
||||
styles into inline style attributes in your HTML code.
|
||||
|
||||
|
||||
- [How it works](#how-it-works)
|
||||
- [Usage](#usage)
|
||||
- [Installing with Composer](#installing-with-composer)
|
||||
- [Usage](#usage)
|
||||
- [Supported CSS selectors](#supported-css-selectors)
|
||||
- [Caveats](#caveats)
|
||||
- [Maintainer](#maintainer)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
|
||||
|
||||
## How it Works
|
||||
|
||||
Emogrifier automagically transmogrifies your HTML by parsing your CSS and inserting your CSS definitions into tags
|
||||
within your HTML based on your CSS selectors.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
First, you provide Emogrifier with the HTML and CSS you would like to merge. This can happen directly during
|
||||
instantiation:
|
||||
|
||||
$html = '<html><h1>Hello world!</h1></html>';
|
||||
$css = 'h1 {font-size: 32px;}';
|
||||
$emogrifier = new \Pelago\Emogrifier($html, $css);
|
||||
|
||||
You could also use the setters for providing this data after instantiation:
|
||||
|
||||
$emogrifier = new \Pelago\Emogrifier();
|
||||
|
||||
$html = '<html><h1>Hello world!</h1></html>';
|
||||
$css = 'h1 {font-size: 32px;}';
|
||||
|
||||
$emogrifier->setHtml($html);
|
||||
$emogrifier->setCss($css);
|
||||
|
||||
After you have set the HTML and CSS, you can call the `emogrify` method to merge both:
|
||||
|
||||
$mergedHtml = $emogrifier->emogrify();
|
||||
|
||||
## Options
|
||||
|
||||
There are several options that you can set on the Emogrifier object before
|
||||
calling the `emogrify` method:
|
||||
|
||||
* `$emogrifier->disableStyleBlocksParsing()` - By default, Emogrifier will grab
|
||||
all `<style>` blocks in the HTML and will apply the CSS styles as inline
|
||||
"style" attributes to the HTML. The `<style>` blocks will then be removed
|
||||
from the HTML. If you want to disable this functionality so that Emogrifier
|
||||
leaves these `<style>` blocks in the HTML and does not parse them, you should
|
||||
use this option.
|
||||
* `$emogrifier->disableInlineStylesParsing()` - By default, Emogrifier
|
||||
preserves all of the "style" attributes on tags in the HTML you pass to it.
|
||||
However if you want to discard all existing inline styles in the HTML before
|
||||
the CSS is applied, you should use this option.
|
||||
|
||||
|
||||
## Installing with Composer
|
||||
|
||||
Download the [`composer.phar`](https://getcomposer.org/composer.phar) locally or install [Composer](https://getcomposer.org/) globally:
|
||||
|
||||
curl -s https://getcomposer.org/installer | php
|
||||
|
||||
Run the following command for a local installation:
|
||||
|
||||
php composer.phar require pelago/emogrifier:@dev
|
||||
|
||||
Or for a global installation, run the following command:
|
||||
|
||||
composer require pelago/emogrifier:@dev
|
||||
|
||||
You can also add follow lines to your `composer.json` and run the `composer update` command:
|
||||
|
||||
"require": {
|
||||
"pelago/emogrifier": "@dev"
|
||||
}
|
||||
|
||||
See https://getcomposer.org/ for more information and documentation.
|
||||
|
||||
|
||||
## Supported CSS selectors
|
||||
|
||||
Emogrifier currently support the following [CSS selectors](http://www.w3.org/TR/CSS2/selector.html):
|
||||
|
||||
* ID
|
||||
* class
|
||||
* type
|
||||
* descendant
|
||||
* child
|
||||
* adjacent
|
||||
* attribute presence
|
||||
* attribute value
|
||||
* attribute only
|
||||
|
||||
The following selectors are implemented, but currently are broken:
|
||||
|
||||
* first-child (currently broken)
|
||||
* last-child (currently broken)
|
||||
|
||||
The following selectors are not implemented yet:
|
||||
|
||||
* universal
|
||||
|
||||
|
||||
## Caveats
|
||||
|
||||
* Emogrifier requires the HTML and the CSS to be UTF-8. Encodings like ISO8859-1 or ISO8859-15 are not supported.
|
||||
* **NEW:** Emogrifier now preserves all valuable @media queries. Media queries can be very useful in
|
||||
responsive email design. See [media query support](https://litmus.com/help/email-clients/media-query-support/).
|
||||
* **NEW:** Emogrifier will grab existing inline style attributes _and_ will grab `<style>` blocks from your HTML, but it
|
||||
will not grab CSS files referenced in <link> elements (the problem email clients are going to ignore these tags
|
||||
anyway, so why leave them in your HTML?).
|
||||
* Even with styles inline, certain CSS properties are ignored by certain email clients. For more information,
|
||||
refer to these resources:
|
||||
* [http://www.email-standards.org/](http://www.email-standards.org/)
|
||||
* [https://www.campaignmonitor.com/css/](https://www.campaignmonitor.com/css/)
|
||||
* [http://templates.mailchimp.com/resources/email-client-css-support/](http://templates.mailchimp.com/resources/email-client-css-support/)
|
||||
* All CSS attributes that apply to a node will be applied, even if they are redundant. For example, if you define a
|
||||
font attribute _and_ a font-size attribute, both attributes will be applied to that node (in other words, the more
|
||||
specific attribute will not be combined into the more general attribute).
|
||||
* There's a good chance you might encounter problems if your HTML is not well formed and valid (DOMDocument might
|
||||
complain). If you get problems like this, consider running your HTML through [Tidy](http://php.net/manual/en/book.tidy.php)
|
||||
before you pass it to Emogrifier.
|
||||
* Finally, Emogrifier only supports CSS1 level selectors and a few CSS2 level selectors (but not all of them). It
|
||||
does not support pseudo selectors (Emogrifier works by converting CSS selectors to XPath selectors, and pseudo
|
||||
selectors cannot be converted accurately).
|
||||
* `!important` currently is not supported yet.
|
||||
|
||||
|
||||
## Maintainer
|
||||
|
||||
Emogrifier is maintained by the good people at [Pelago](http://www.pelagodesign.com/), info AT pelagodesign DOT com.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Those that wish to contribute bug fixes, new features, refactorings and clean-up to Emogrifier are more than welcome.
|
||||
When you contribute, please take the following things into account:
|
||||
|
||||
* Please cover all changes with unit tests and make sure that your code does not break any existing tests.
|
||||
* To run the existing PHPUnit tests, run the following
|
||||
command:
|
||||
|
||||
composer install
|
||||
|
||||
and then run this command:
|
||||
|
||||
vendor/bin/phpunit Tests/
|
||||
|
||||
If you want to uninstall the development packages that were installed when
|
||||
you ran `composer install`, you can run this command:
|
||||
|
||||
composer install --no-dev
|
||||
* Please use the same coding style (PSR-2) as the rest of the code. Indentation is four spaces.
|
||||
* Please make your code clean, well-readable and easy to understand.
|
||||
* If you add new methods or fields, please use proper PHPDoc for the new methods/fields.
|
||||
* Git commits should have a <= 50 character summary, optionally followed by a blank line
|
||||
and a more in depth description of 79 characters per line.
|
||||
* [Please squash related commits together](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html).
|
||||
* Please use grammatically correct, complete sentences in the code documentation and the commit messages.
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
}
|
||||
</script>
|
||||
<meta name="Author" content="RainLoop Team" />
|
||||
<meta name="robots" content="noindex, nofollow, noodp" />
|
||||
<meta name="robots" content="noindex,nofollow,noodp" />
|
||||
<meta name="google" content="notranslate" />
|
||||
<title></title>
|
||||
<style>#rl-content{display:none;}</style>
|
||||
|
|
|
|||
|
|
@ -59,12 +59,15 @@
|
|||
}
|
||||
|
||||
.thm-login {
|
||||
color: @login-color !important;
|
||||
border: @login-border !important;
|
||||
.thm-rgba-background-color(@login-background-color, @login-rgba-background-color);
|
||||
.thm-linear-gradient-mixin(@login-gradient-start, @login-gradient-end);
|
||||
.thm-border-radius(@login-border-radius);
|
||||
.thm-box-shadow(@login-box-shadow);
|
||||
}
|
||||
|
||||
.thm-login-text {
|
||||
color: @login-color !important;
|
||||
|
||||
.legend, .e-checkbox-icon, .g-ui-link, .social-button, .language-button {
|
||||
color: @login-color !important;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<button type="button" class="close" data-bind="click: function () { submitError('') }">×</button>
|
||||
<span data-bind="text: submitError"></span>
|
||||
</div>
|
||||
<form class="wrapper loginForm thm-login" action="#/" data-bind="submit: submitForm, css: {'errorAnimated': formError}">
|
||||
<form class="wrapper loginForm thm-login thm-login-text" action="#/" data-bind="submit: submitForm, css: {'errorAnimated': formError}">
|
||||
<div class="controls" data-bind="css: {'error': loginError, 'animated': loginErrorAnimation}">
|
||||
<div class="input-append">
|
||||
<input type="text" class="input-block-level inputLogin checkAutocomplete"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
<div class="alert alert-info span8" data-bind="visible: 0 === plugins().length">
|
||||
<span data-i18n="TAB_PLUGINS/ALERT_NO_PLUGINS"></span>
|
||||
<br />
|
||||
<br />
|
||||
<strong><a href="#/packages" data-i18n="TAB_PLUGINS/LINK_INSTALL_NEW"></a></strong>
|
||||
</div>
|
||||
<div class="span8" data-bind="visible: 0 < plugins().length">
|
||||
|
|
|
|||
|
|
@ -19,97 +19,102 @@
|
|||
<span data-bind="text: submitErrorAddidional"></span>
|
||||
</div>
|
||||
</div>
|
||||
<form class="wrapper loginForm thm-login" action="#/" data-bind="submit: submitForm, css: {'errorAnimated': formError}">
|
||||
{{INCLUDE/TopControlGroup/PLACE}}
|
||||
<div class="controls" data-bind="css: {'error': emailError, 'animated': emailErrorAnimation}">
|
||||
<div class="input-append">
|
||||
<input type="email" class="i18n input-block-level inputEmail checkAutocomplete"
|
||||
name="RainLoopEmail" id="RainLoopEmail"
|
||||
style="padding-right: 35px;"
|
||||
autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="textInput: email, hasFocus: emailFocus" data-i18n="[placeholder]LOGIN/LABEL_EMAIL" />
|
||||
<span class="add-on">
|
||||
<i class="icon-mail"></i>
|
||||
</span>
|
||||
<div class="wrapper-parent">
|
||||
<div class="wrapper loginWelcomeForm thm-login-text" data-bind="css: {'welcome-on': welcome}">
|
||||
<!-- ko template: { name: 'LoginWelcome' } --><!-- /ko -->
|
||||
</div>
|
||||
<form class="wrapper loginForm thm-login thm-login-text" action="#/" data-bind="submit: submitForm, css: {'errorAnimated': formError, 'welcome-off': welcome}">
|
||||
{{INCLUDE/TopControlGroup/PLACE}}
|
||||
<div class="controls" data-bind="css: {'error': emailError, 'animated': emailErrorAnimation}">
|
||||
<div class="input-append">
|
||||
<input type="email" class="i18n input-block-level inputEmail checkAutocomplete"
|
||||
name="RainLoopEmail" id="RainLoopEmail"
|
||||
style="padding-right: 35px;"
|
||||
autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="textInput: email, hasFocus: emailFocus" data-i18n="[placeholder]LOGIN/LABEL_EMAIL" />
|
||||
<span class="add-on">
|
||||
<i class="icon-mail"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls" data-bind="css: {'error': passwordError, 'animated': passwordErrorAnimation}">
|
||||
<div class="input-append">
|
||||
<input type="password" class="i18n input-block-level inputPassword checkAutocomplete"
|
||||
name="RainLoopPassword" id="RainLoopPassword"
|
||||
style="padding-right: 35px;"
|
||||
autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="textInput: password, hasFocus: passwordFocus" data-i18n="[placeholder]LOGIN/LABEL_PASSWORD" />
|
||||
<span class="add-on">
|
||||
<i class="icon-key"></i>
|
||||
</span>
|
||||
<div class="controls" data-bind="css: {'error': passwordError, 'animated': passwordErrorAnimation}">
|
||||
<div class="input-append">
|
||||
<input type="password" class="i18n input-block-level inputPassword checkAutocomplete"
|
||||
name="RainLoopPassword" id="RainLoopPassword"
|
||||
style="padding-right: 35px;"
|
||||
autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
data-bind="textInput: password, hasFocus: passwordFocus" data-i18n="[placeholder]LOGIN/LABEL_PASSWORD" />
|
||||
<span class="add-on">
|
||||
<i class="icon-key"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls" data-bind="visible: additionalCode.visibility(), css: {'error': additionalCode.error, 'animated': additionalCode.errorAnimation}">
|
||||
<div class="input-append">
|
||||
<input type="text" class="i18n input-block-level inputAdditionalCode"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
style="padding-right: 35px;"
|
||||
data-bind="textInput: additionalCode, hasFocus: additionalCode.focused" data-i18n="[placeholder]LOGIN/LABEL_VERIFICATION_CODE" />
|
||||
<span class="add-on">
|
||||
<i class="icon-key"></i>
|
||||
</span>
|
||||
<div class="controls" data-bind="visible: additionalCode.visibility(), css: {'error': additionalCode.error, 'animated': additionalCode.errorAnimation}">
|
||||
<div class="input-append">
|
||||
<input type="text" class="i18n input-block-level inputAdditionalCode"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
style="padding-right: 35px;"
|
||||
data-bind="textInput: additionalCode, hasFocus: additionalCode.focused" data-i18n="[placeholder]LOGIN/LABEL_VERIFICATION_CODE" />
|
||||
<span class="add-on">
|
||||
<i class="icon-key"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls" data-bind="visible: additionalCode.visibility()">
|
||||
<div class="additionalCodeSignMeLabel" data-bind="component: {
|
||||
name: 'CheckboxSimple',
|
||||
params: {
|
||||
label: 'LOGIN/LABEL_DONT_ASK_VERIFICATION_CODE',
|
||||
value: additionalCodeSignMe
|
||||
}
|
||||
}"></div>
|
||||
</div>
|
||||
{{INCLUDE/BottomControlGroup/PLACE}}
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn btn-large btn-block buttonLogin" data-bind="command: submitCommand, hasFocus: submitFocus">
|
||||
<i class="icon-spinner animated" data-bind="visible: submitRequest"></i>
|
||||
<span class="i18n i18n-animation" data-i18n="LOGIN/BUTTON_SIGN_IN" data-bind="visible: !submitRequest()"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls clearfix" style="margin-bottom: 20px">
|
||||
<div class="pull-right social-buttons">
|
||||
<a href="#" tabindex="-1" class="social-button" data-bind="visible: facebookLoginEnabled, command: facebookCommand, tooltip: 'LOGIN/TITLE_SIGN_IN_FACEBOOK'">
|
||||
<i class="icon-facebook-alt"></i>
|
||||
</a>
|
||||
<a href="#" tabindex="-1" class="social-button" data-bind="visible: googleLoginEnabled, command: googleCommand, tooltip: 'LOGIN/TITLE_SIGN_IN_GOOGLE'">
|
||||
<i class="icon-google"></i>
|
||||
</a>
|
||||
<a href="#" tabindex="-1" class="social-button" data-bind="visible: twitterLoginEnabled, command: twitterCommand, tooltip: 'LOGIN/TITLE_SIGN_IN_TWITTER'">
|
||||
<i class="icon-twitter"></i>
|
||||
</a>
|
||||
<a href="#" tabindex="-1" class="language-button" data-bind="visible: allowLanguagesOnLogin() && !socialLoginEnabled(), click: selectLanguage, tooltip: 'POPUPS_LANGUAGES/TITLE_LANGUAGES'">
|
||||
<i data-bind="css: langRequest() ? 'icon-spinner animated' : 'icon-world'"></i>
|
||||
</a>
|
||||
<div class="controls" data-bind="visible: additionalCode.visibility()">
|
||||
<div class="additionalCodeSignMeLabel" data-bind="component: {
|
||||
name: 'CheckboxSimple',
|
||||
params: {
|
||||
label: 'LOGIN/LABEL_DONT_ASK_VERIFICATION_CODE',
|
||||
value: additionalCodeSignMe
|
||||
}
|
||||
}"></div>
|
||||
</div>
|
||||
{{INCLUDE/BottomControlGroup/PLACE}}
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn btn-large btn-block buttonLogin" data-bind="command: submitCommand, hasFocus: submitFocus">
|
||||
<i class="icon-spinner animated" data-bind="visible: submitRequest"></i>
|
||||
<span class="i18n i18n-animation" data-i18n="LOGIN/BUTTON_SIGN_IN" data-bind="visible: !submitRequest()"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls clearfix" style="margin-bottom: 20px">
|
||||
<div class="pull-right social-buttons">
|
||||
<a href="#" tabindex="-1" class="social-button" data-bind="visible: facebookLoginEnabled, command: facebookCommand, tooltip: 'LOGIN/TITLE_SIGN_IN_FACEBOOK'">
|
||||
<i class="icon-facebook-alt"></i>
|
||||
</a>
|
||||
<a href="#" tabindex="-1" class="social-button" data-bind="visible: googleLoginEnabled, command: googleCommand, tooltip: 'LOGIN/TITLE_SIGN_IN_GOOGLE'">
|
||||
<i class="icon-google"></i>
|
||||
</a>
|
||||
<a href="#" tabindex="-1" class="social-button" data-bind="visible: twitterLoginEnabled, command: twitterCommand, tooltip: 'LOGIN/TITLE_SIGN_IN_TWITTER'">
|
||||
<i class="icon-twitter"></i>
|
||||
</a>
|
||||
<a href="#" tabindex="-1" class="language-button" data-bind="visible: allowLanguagesOnLogin() && !socialLoginEnabled(), click: selectLanguage, tooltip: 'POPUPS_LANGUAGES/TITLE_LANGUAGES'">
|
||||
<i data-bind="css: langRequest() ? 'icon-spinner animated' : 'icon-world'"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="signMeLabel" data-bind="visible: signMeVisibility, component: {
|
||||
name: 'CheckboxSimple',
|
||||
params: {
|
||||
label: 'LOGIN/LABEL_SIGN_ME',
|
||||
labelAnimated: true,
|
||||
value: signMe
|
||||
}
|
||||
}"></div>
|
||||
<div class="signMeLabel" data-bind="visible: signMeVisibility, component: {
|
||||
name: 'CheckboxSimple',
|
||||
params: {
|
||||
label: 'LOGIN/LABEL_SIGN_ME',
|
||||
labelAnimated: true,
|
||||
value: signMe
|
||||
}
|
||||
}"></div>
|
||||
|
||||
</div>
|
||||
<div class="controls clearfix" data-bind="visible: '' !== forgotPasswordLinkUrl || '' !== registrationLinkUrl">
|
||||
<div class="forgot-link thm-forgot pull-left" data-bind="visible: '' !== forgotPasswordLinkUrl" style="text-align: center">
|
||||
<a href="#" target="_blank" class="g-ui-link" data-bind="attr: {href: forgotPasswordLinkUrl}, css: {'pull-right': '' !== registrationLinkUrl}"
|
||||
><span class="i18n" data-i18n="LOGIN/LABEL_FORGOT_PASSWORD"></span></a>
|
||||
</div>
|
||||
|
||||
<div class="registration-link thm-registration pull-right" data-bind="visible: '' !== registrationLinkUrl" style="text-align: center">
|
||||
<a href="#" target="_blank" class="g-ui-link" data-bind="attr: {href: registrationLinkUrl}, css: {'pull-left': '' !== forgotPasswordLinkUrl}"
|
||||
><span class="i18n" data-i18n="LOGIN/LABEL_REGISTRATION"></span></a>
|
||||
<div class="controls clearfix" data-bind="visible: '' !== forgotPasswordLinkUrl || '' !== registrationLinkUrl">
|
||||
<div class="forgot-link thm-forgot pull-left" data-bind="visible: '' !== forgotPasswordLinkUrl" style="text-align: center">
|
||||
<a href="#" target="_blank" class="g-ui-link" data-bind="attr: {href: forgotPasswordLinkUrl}, css: {'pull-right': '' !== registrationLinkUrl}"
|
||||
><span class="i18n" data-i18n="LOGIN/LABEL_FORGOT_PASSWORD"></span></a>
|
||||
</div>
|
||||
|
||||
<div class="registration-link thm-registration pull-right" data-bind="visible: '' !== registrationLinkUrl" style="text-align: center">
|
||||
<a href="#" target="_blank" class="g-ui-link" data-bind="attr: {href: registrationLinkUrl}, css: {'pull-left': '' !== forgotPasswordLinkUrl}"
|
||||
><span class="i18n" data-i18n="LOGIN/LABEL_REGISTRATION"></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</div>
|
||||
<div class="e-powered thm-powered" data-bind="visible: logoPowered">
|
||||
Powered by <a href="http://rainloop.net" target="_blank">RainLoop</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<div class="b-login--welcome-content">
|
||||
<i class="icon-google iconsize50" style="cursor: pointer; padding-right: 20px" data-bind="command: googleFastCommand, visible: googleFastLoginEnabled"></i>
|
||||
<i class="icon-mail iconsize50" style="cursor: pointer" data-bind="click: displayMainForm"></i>
|
||||
<br /><br />
|
||||
</div>
|
||||
|
|
@ -295,10 +295,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="messageItem fixIndex" data-bind="css: viewLineAsCss(), nano: true, attr: {'style': 'top:' + viewBodyTopValue() + 'px' }">
|
||||
<div class="content g-scrollbox" tabindex="0" data-bind="hasfocus: messageDomFocused, css: { 'focused': messageDomFocused }">
|
||||
<div class="content-wrapper">
|
||||
<div>
|
||||
<div class="content g-scrollbox" tabindex="0" data-bind="hasfocus: messageDomFocused, css: { 'focused': messageDomFocused },
|
||||
style: {'background-color': messageLoadingThrottle() ? '' : bodyBackgroundColor()}">
|
||||
|
||||
<div class="content-wrapper">
|
||||
|
||||
<div>
|
||||
<span class="buttonUp" data-bind="click: scrollMessageToTop">
|
||||
<i class="icon-up"></i>
|
||||
</span>
|
||||
|
|
@ -316,128 +318,132 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g-ui-min-height-300" data-bind="visible: !messageLoadingThrottle()">
|
||||
<div class="showImages" data-bind="visible: message() && message().hasImages(), click: function() { showImages(message()); }">
|
||||
<i class="icon-image"></i>
|
||||
|
||||
<span class="i18n text" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></span>
|
||||
</div>
|
||||
<div class="readReceipt" data-bind="visible: message() && !isDraftOrSentFolder() && '' !== message().readReceipt() && !message().isReadReceipt(), click: function() { readReceipt(message()); }">
|
||||
<i class="icon-mail"></i>
|
||||
|
||||
<span class="i18n text" data-i18n="MESSAGE/BUTTON_NOTIFY_READ_RECEIPT"></span>
|
||||
</div>
|
||||
<div class="pgpInfo" data-bind="visible: isPgpStatusVerifyVisible(), css: {'success': isPgpStatusVerifySuccess()}">
|
||||
<i class="icon-key"></i>
|
||||
|
||||
<span data-bind="text: pgpStatusVerifyMessage()"></span>
|
||||
</div>
|
||||
<div class="pgpSigned" data-bind="visible: message() && message().isPgpSigned() && isPgpActionVisible(), click: function() { verifyPgpSignedClearMessage(message()); }">
|
||||
<i class="icon-lock"></i>
|
||||
|
||||
<span class="i18n" data-i18n="MESSAGE/PGP_SIGNED_MESSAGE_DESC"></span>
|
||||
</div>
|
||||
<div class="pgpEncrypted" data-bind="visible: message() && message().isPgpEncrypted() && isPgpActionVisible()">
|
||||
<i class="icon-lock"></i>
|
||||
|
||||
<span class="i18n" data-i18n="MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC" data-bind="click: function() { decryptPgpEncryptedMessage(message()); }"></span>
|
||||
|
||||
<input type="password" class="i18n span3 inputPgpPassword"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
style="margin-bottom: 0" data-i18n="[placeholder]MESSAGE/PGP_PASSWORD_INPUT_PLACEHOLDER" data-bind="value: viewPgpPassword, onEnter: function() { decryptPgpEncryptedMessage(message()); }" />
|
||||
</div>
|
||||
<div class="attachmentsPlace" data-bind="visible: message() && message().hasVisibleAttachments(),
|
||||
css: {'selection-mode' : showAttachmnetControls, 'unselectedAttachmentsError': highlightUnselectedAttachments}">
|
||||
<ul class="attachmentList" data-bind="foreach: message() ? message().attachments() : []">
|
||||
<li class="attachmentItem clearfix" draggable="true" data-tooltip-join="top"
|
||||
data-bind="visible: !isLinked, event: { 'dragstart': eventDragStart }, attr: { 'title': fileName }, css: {'checked': checked}">
|
||||
<div class="attachmentIconParent pull-left" data-bind="css: { 'hasPreview': hasPreview(), 'hasPreplay': hasPreplay(), 'isImage': isImage() }">
|
||||
<div class="hidePreview">
|
||||
<div class="iconMain">
|
||||
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
|
||||
<i class="attachmentIconText attachmentMainIconText" data-bind="text: iconText()"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="showPreview">
|
||||
<a data-bind="css: {'attachmentImagePreview': isImage(), 'visible': !isLinked}, attr: { 'title': fileName, 'href': linkPreviewMain() }" target="_blank">
|
||||
<div class="g-ui-min-height-300 content-content" data-bind="visible: !messageLoadingThrottle()">
|
||||
<div class="bodySubHeader">
|
||||
<div class="showImages" data-bind="visible: message() && message().hasImages(), click: function() { showImages(message()); }">
|
||||
<i class="icon-image"></i>
|
||||
|
||||
<span class="i18n text" data-i18n="MESSAGE/BUTTON_SHOW_IMAGES"></span>
|
||||
</div>
|
||||
<div class="readReceipt" data-bind="visible: message() && !isDraftOrSentFolder() && '' !== message().readReceipt() && !message().isReadReceipt(), click: function() { readReceipt(message()); }">
|
||||
<i class="icon-mail"></i>
|
||||
|
||||
<span class="i18n text" data-i18n="MESSAGE/BUTTON_NOTIFY_READ_RECEIPT"></span>
|
||||
</div>
|
||||
<div class="pgpInfo" data-bind="visible: isPgpStatusVerifyVisible(), css: {'success': isPgpStatusVerifySuccess()}">
|
||||
<i class="icon-key"></i>
|
||||
|
||||
<span data-bind="text: pgpStatusVerifyMessage()"></span>
|
||||
</div>
|
||||
<div class="pgpSigned" data-bind="visible: message() && message().isPgpSigned() && isPgpActionVisible(), click: function() { verifyPgpSignedClearMessage(message()); }">
|
||||
<i class="icon-lock"></i>
|
||||
|
||||
<span class="i18n" data-i18n="MESSAGE/PGP_SIGNED_MESSAGE_DESC"></span>
|
||||
</div>
|
||||
<div class="pgpEncrypted" data-bind="visible: message() && message().isPgpEncrypted() && isPgpActionVisible()">
|
||||
<i class="icon-lock"></i>
|
||||
|
||||
<span class="i18n" data-i18n="MESSAGE/PGP_ENCRYPTED_MESSAGE_DESC" data-bind="click: function() { decryptPgpEncryptedMessage(message()); }"></span>
|
||||
|
||||
<input type="password" class="i18n span3 inputPgpPassword"
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||||
style="margin-bottom: 0" data-i18n="[placeholder]MESSAGE/PGP_PASSWORD_INPUT_PLACEHOLDER" data-bind="value: viewPgpPassword, onEnter: function() { decryptPgpEncryptedMessage(message()); }" />
|
||||
</div>
|
||||
<div class="attachmentsPlace" data-bind="visible: message() && message().hasVisibleAttachments(),
|
||||
css: {'selection-mode' : showAttachmnetControls, 'unselectedAttachmentsError': highlightUnselectedAttachments}">
|
||||
<ul class="attachmentList" data-bind="foreach: message() ? message().attachments() : []">
|
||||
<li class="attachmentItem clearfix" draggable="true" data-tooltip-join="top"
|
||||
data-bind="visible: !isLinked, event: { 'dragstart': eventDragStart }, attr: { 'title': fileName }, css: {'checked': checked}">
|
||||
<div class="attachmentIconParent pull-left" data-bind="css: { 'hasPreview': hasPreview(), 'hasPreplay': hasPreplay(), 'isImage': isImage() }">
|
||||
<div class="hidePreview">
|
||||
<div class="iconMain">
|
||||
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
|
||||
<i class="attachmentIconText attachmentMainIconText" data-bind="text: iconText()"></i>
|
||||
</div>
|
||||
<div class="iconBG" data-bind="attr: { 'style': linkThumbnailPreviewStyle() }"></div>
|
||||
<div class="iconPreview">
|
||||
<i class="attachmentIcon icon-eye show-hover"></i>
|
||||
</div>
|
||||
<div class="showPreview">
|
||||
<a data-bind="css: {'attachmentImagePreview': isImage(), 'visible': !isLinked}, attr: { 'title': fileName, 'href': linkPreviewMain() }" target="_blank">
|
||||
<div class="iconMain">
|
||||
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
|
||||
<i class="attachmentIconText attachmentMainIconText" data-bind="text: iconText()"></i>
|
||||
</div>
|
||||
<div class="iconBG" data-bind="attr: { 'style': linkThumbnailPreviewStyle() }"></div>
|
||||
<div class="iconPreview">
|
||||
<i class="attachmentIcon icon-eye show-hover"></i>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="showPreplay">
|
||||
<div class="iconMain">
|
||||
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
|
||||
</div>
|
||||
<div class="iconPreview">
|
||||
<i class="attachmentIcon icon-play show-hover"></i>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="showPreplay">
|
||||
<div class="iconMain">
|
||||
<i class="attachmentIcon attachmentMainIcon" data-bind="css: iconClass()"></i>
|
||||
</div>
|
||||
<div class="iconPreview">
|
||||
<i class="attachmentIcon icon-play show-hover"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachmentNameParent">
|
||||
<div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">
|
||||
<span class="attachmentName" data-bind="text: fileName"></span>
|
||||
<div class="attachmentNameParent">
|
||||
<div style="white-space: nowrap; text-overflow: ellipsis; overflow: hidden;">
|
||||
<span class="attachmentName" data-bind="text: fileName"></span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="attachmentSize" data-bind="text: friendlySize"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="attachmentSize" data-bind="text: friendlySize"></span>
|
||||
<div class="checkedParent">
|
||||
<i class="checkboxAttachment icon-checkbox-unchecked"
|
||||
data-bind="css: checked() ? 'checkboxMessage icon-checkbox-checked' : 'checkboxMessage icon-checkbox-unchecked',
|
||||
click: function () { checked(!checked()); return false }"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkedParent">
|
||||
<i class="checkboxAttachment icon-checkbox-unchecked"
|
||||
data-bind="css: checked() ? 'checkboxMessage icon-checkbox-checked' : 'checkboxMessage icon-checkbox-unchecked',
|
||||
click: function () { checked(!checked()); return false }"></i>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<i class="icon-cog controls-handle" data-bind="visible: allowAttachmnetControls() && !showAttachmnetControls(), click: function () { showAttachmnetControls(true) }"></i>
|
||||
</div>
|
||||
<div class="attachmentsControls"
|
||||
</li>
|
||||
</ul>
|
||||
<i class="icon-cog controls-handle" data-bind="visible: allowAttachmnetControls() && !showAttachmnetControls(), click: function () { showAttachmnetControls(true) }"></i>
|
||||
</div>
|
||||
|
||||
<div class="attachmentsControls"
|
||||
data-bind="visible: showAttachmnetControls() && message() && message().hasVisibleAttachments()">
|
||||
|
||||
<span data-bind="visible: downloadAsZipAllowed">
|
||||
<i class="icon-remove iconcolor-red" data-bind="visible: downloadAsZipError"></i>
|
||||
<i class="icon-file-zip" data-bind="visible: !downloadAsZipError(),
|
||||
css: {'icon-file-zip': !downloadAsZipLoading(), 'icon-spinner animated': downloadAsZipLoading()}"></i>
|
||||
|
||||
<span class="g-ui-link" data-bind="click: downloadAsZip">
|
||||
<span class="i18n" data-i18n="MESSAGE/LINK_DOWNLOAD_AS_ZIP"></span>
|
||||
<span data-bind="visible: downloadAsZipAllowed">
|
||||
<i class="icon-remove iconcolor-red" data-bind="visible: downloadAsZipError"></i>
|
||||
<i class="icon-file-zip" data-bind="visible: !downloadAsZipError(),
|
||||
css: {'icon-file-zip': !downloadAsZipLoading(), 'icon-spinner animated': downloadAsZipLoading()}"></i>
|
||||
|
||||
<span class="g-ui-link" data-bind="click: downloadAsZip">
|
||||
<span class="i18n" data-i18n="MESSAGE/LINK_DOWNLOAD_AS_ZIP"></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span data-bind="visible: saveToOwnCloudAllowed">
|
||||
|
||||
<i class="icon-remove iconcolor-red" data-bind="visible: saveToOwnCloudError"></i>
|
||||
<i class="icon-ok iconcolor-green" data-bind="visible: saveToOwnCloudSuccess"></i>
|
||||
<i class="icon-cloud-up" data-bind="visible: !saveToOwnCloudSuccess() && !saveToOwnCloudError(),
|
||||
css: {'icon-cloud-up': !saveToOwnCloudLoading(), 'icon-spinner animated': saveToOwnCloudLoading()}"></i>
|
||||
|
||||
<span class="g-ui-link" data-bind="click: saveToOwnCloud">
|
||||
<span class="i18n" data-i18n="MESSAGE/LINK_SAVE_TO_OWNCLOUD"></span>
|
||||
<span data-bind="visible: saveToOwnCloudAllowed">
|
||||
|
||||
<i class="icon-remove iconcolor-red" data-bind="visible: saveToOwnCloudError"></i>
|
||||
<i class="icon-ok iconcolor-green" data-bind="visible: saveToOwnCloudSuccess"></i>
|
||||
<i class="icon-cloud-up" data-bind="visible: !saveToOwnCloudSuccess() && !saveToOwnCloudError(),
|
||||
css: {'icon-cloud-up': !saveToOwnCloudLoading(), 'icon-spinner animated': saveToOwnCloudLoading()}"></i>
|
||||
|
||||
<span class="g-ui-link" data-bind="click: saveToOwnCloud">
|
||||
<span class="i18n" data-i18n="MESSAGE/LINK_SAVE_TO_OWNCLOUD"></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span data-bind="visible: saveToDropboxAllowed">
|
||||
|
||||
<i class="icon-remove iconcolor-red" data-bind="visible: saveToDropboxError"></i>
|
||||
<i class="icon-ok iconcolor-green" data-bind="visible: saveToDropboxSuccess"></i>
|
||||
<i class="icon-dropbox" data-bind="visible: !saveToDropboxSuccess() && !saveToDropboxError(),
|
||||
css: {'icon-dropbox': !saveToDropboxLoading(), 'icon-spinner animated': saveToDropboxLoading()}"></i>
|
||||
|
||||
<span class="g-ui-link" data-bind="click: saveToDropbox">
|
||||
<span class="i18n" data-i18n="MESSAGE/LINK_SAVE_TO_DROPBOX"></span>
|
||||
<span data-bind="visible: saveToDropboxAllowed">
|
||||
|
||||
<i class="icon-remove iconcolor-red" data-bind="visible: saveToDropboxError"></i>
|
||||
<i class="icon-ok iconcolor-green" data-bind="visible: saveToDropboxSuccess"></i>
|
||||
<i class="icon-dropbox" data-bind="visible: !saveToDropboxSuccess() && !saveToDropboxError(),
|
||||
css: {'icon-dropbox': !saveToDropboxLoading(), 'icon-spinner animated': saveToDropboxLoading()}"></i>
|
||||
|
||||
<span class="g-ui-link" data-bind="click: saveToDropbox">
|
||||
<span class="i18n" data-i18n="MESSAGE/LINK_SAVE_TO_DROPBOX"></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- // --->
|
||||
<button type="button" class="close" style="margin-right: 5px;"
|
||||
data-bind="click: function () { showAttachmnetControls(false); }">×</button>
|
||||
<button type="button" class="close" style="margin-right: 5px;"
|
||||
data-bind="click: function () { showAttachmnetControls(false); }">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bodyText g-ui-min-height-300 rl-mv-root" data-bind="initDom: messagesBodiesDom"></div>
|
||||
|
||||
<div class="bodyText g-ui-min-height-300 rl-mv-root"
|
||||
data-bind="initDom: messagesBodiesDom"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,8 @@
|
|||
</label>
|
||||
<div class="controls">
|
||||
<div style="display: inline-block; margin-top: 5px">
|
||||
<b data-bind="text: identityMainDesc"></b>
|
||||
|
||||
<span data-bind="click: editMainIdentity" style="cursor: pointer">
|
||||
<i class="icon-cog"></i>
|
||||
</span>
|
||||
<b data-bind="click: editMainIdentity, text: identityMainDesc"
|
||||
style="border-bottom: 1px dashed #555; cursor: pointer; padding: 2px 0px;"></b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ LABEL_BRANDING_NAME = "Брендинг"
|
|||
LABEL_CONTACTS_NAME = "Контакты"
|
||||
LABEL_DOMAINS_NAME = "Домены"
|
||||
LABEL_SECURITY_NAME = "Безопасность"
|
||||
LABEL_INTEGRATION_NAME = "Инеграция"
|
||||
LABEL_INTEGRATION_NAME = "Интеграция"
|
||||
LABEL_PLUGINS_NAME = "Плагины"
|
||||
LABEL_PACKAGES_NAME = "Пакеты"
|
||||
LABEL_LICENSING_NAME = "Лицензия"
|
||||
|
|
|
|||
3
vendors/bootstrap/less/tables.less
vendored
3
vendors/bootstrap/less/tables.less
vendored
|
|
@ -6,7 +6,8 @@
|
|||
// BASE TABLES
|
||||
// -----------------
|
||||
|
||||
table {
|
||||
// table {
|
||||
.table {
|
||||
max-width: 100%;
|
||||
background-color: @tableBackground;
|
||||
border-collapse: collapse;
|
||||
|
|
|
|||
3
vendors/normalize/normalize.css
vendored
3
vendors/normalize/normalize.css
vendored
|
|
@ -498,7 +498,10 @@ textarea {
|
|||
* Remove most spacing between table cells
|
||||
*/
|
||||
|
||||
/*
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue