From 396c4564b4327156b3b84fe3bf09e98fe4383491 Mon Sep 17 00:00:00 2001
From: Romain Quetiez
Date: Mon, 4 Jul 2016 15:06:28 +0000
Subject: [PATCH] HTML formatting: TWO fixes in one! Fixed a bug introduced in
2.3.0-beta: the stylesheet cannot be defined within the email templates (aka
ActionEmail) anymore. Instead, a default (ready for use) stylesheet is
provided into /css/email.css and it can be overriden by the configuration
parameter email_css. The fix consists in transforming the stylesheet into
inline style... which fixes a limitation of gmail and Outlook that support
only the inline styles. The implementation relies on a new library:
emogrifier. This library has been changed (home-made utility) to be
compatible with PHP 5.3 (declaration of arrays).
SVN:trunk[4277]
---
core/action.class.inc.php | 6 +-
core/config.class.inc.php | 8 +
core/email.class.inc.php | 8 +-
css/email.css | 12 +
lib/emogrifier/.gitignore | 24 +
lib/emogrifier/.travis.yml | 31 +
lib/emogrifier/CHANGELOG.md | 92 ++
lib/emogrifier/CONTRIBUTING.md | 78 ++
lib/emogrifier/Classes/Emogrifier.php | 1020 ++++++++++++++
.../Standards/Emogrifier/ruleset.xml | 136 ++
lib/emogrifier/LICENSE | 21 +
lib/emogrifier/README.md | 198 +++
lib/emogrifier/Tests/Unit/EmogrifierTest.php | 1221 +++++++++++++++++
lib/emogrifier/composer.json | 46 +
setup/licenses/community-licences.xml | 24 +
15 files changed, 2922 insertions(+), 3 deletions(-)
create mode 100644 css/email.css
create mode 100644 lib/emogrifier/.gitignore
create mode 100644 lib/emogrifier/.travis.yml
create mode 100644 lib/emogrifier/CHANGELOG.md
create mode 100644 lib/emogrifier/CONTRIBUTING.md
create mode 100644 lib/emogrifier/Classes/Emogrifier.php
create mode 100644 lib/emogrifier/Configuration/PhpCodeSniffer/Standards/Emogrifier/ruleset.xml
create mode 100644 lib/emogrifier/LICENSE
create mode 100644 lib/emogrifier/README.md
create mode 100644 lib/emogrifier/Tests/Unit/EmogrifierTest.php
create mode 100644 lib/emogrifier/composer.json
diff --git a/core/action.class.inc.php b/core/action.class.inc.php
index 5e65a974b..df5367985 100644
--- a/core/action.class.inc.php
+++ b/core/action.class.inc.php
@@ -324,6 +324,8 @@ class ActionEmail extends ActionNotification
if (isset($sSubject)) $oLog->Set('subject', $sSubject);
if (isset($sBody)) $oLog->Set('body', $sBody);
}
+ $sStyles = file_get_contents(APPROOT.'css/email.css');
+ $sStyles .= MetaModel::GetConfig()->Get('email_css');
$oEmail = new EMail();
@@ -344,7 +346,7 @@ class ActionEmail extends ActionNotification
$sTestBody .= "\n";
$sTestBody .= "
\n";
$sTestBody .= "\n";
- $oEmail->SetBody($sTestBody);
+ $oEmail->SetBody($sTestBody, 'text/html', $sStyles);
$oEmail->SetRecipientTO($this->Get('test_recipient'));
$oEmail->SetRecipientFrom($this->Get('test_recipient'));
$oEmail->SetReferences($sReference);
@@ -353,7 +355,7 @@ class ActionEmail extends ActionNotification
else
{
$oEmail->SetSubject($sSubject);
- $oEmail->SetBody($sBody);
+ $oEmail->SetBody($sBody, 'text/html', $sStyles);
$oEmail->SetRecipientTO($sTo);
$oEmail->SetRecipientCC($sCC);
$oEmail->SetRecipientBCC($sBCC);
diff --git a/core/config.class.inc.php b/core/config.class.inc.php
index 05dc55c58..5ea6218cf 100644
--- a/core/config.class.inc.php
+++ b/core/config.class.inc.php
@@ -404,6 +404,14 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
+ 'email_css' => array(
+ 'type' => 'string',
+ 'description' => 'CSS that will override the standard stylesheet used for the notifications',
+ 'default' => "",
+ 'value' => "",
+ 'source_of_value' => '',
+ 'show_in_conf_sample' => false,
+ ),
'apc_cache.enabled' => array(
'type' => 'bool',
'description' => 'If set, the APC cache is allowed (the PHP extension must also be active)',
diff --git a/core/email.class.inc.php b/core/email.class.inc.php
index 3586d4569..d0b8e14f9 100644
--- a/core/email.class.inc.php
+++ b/core/email.class.inc.php
@@ -305,8 +305,14 @@ IssueLog::Info(__METHOD__.' '.$this->m_oMessage->toString());
$this->AddToHeader('References', $sReferences);
}
- public function SetBody($sBody, $sMimeType = 'text/html')
+ public function SetBody($sBody, $sMimeType = 'text/html', $sCustomStyles = null)
{
+ if (($sMimeType === 'text/html') && ($sCustomStyles !== null))
+ {
+ require_once(APPROOT.'lib/emogrifier/classes/emogrifier.php');
+ $emogrifier = new \Pelago\Emogrifier($sBody, $sCustomStyles);
+ $sBody = $emogrifier->emogrify(); // Adds html/body tags if not already present
+ }
$this->m_aData['body'] = array('body' => $sBody, 'mimeType' => $sMimeType);
$this->m_oMessage->setBody($sBody, $sMimeType);
}
diff --git a/css/email.css b/css/email.css
new file mode 100644
index 000000000..3a27e6d01
--- /dev/null
+++ b/css/email.css
@@ -0,0 +1,12 @@
+/* Note: only CSS1 is supported here (see the limitations of emogrifier: https://github.com/jjriv/emogrifier/) */
+.caselog_header {
+ padding: 3px;
+ border-top: 1px solid #fff;
+ background-color: #ddd;
+ padding-left: 16px;
+ width: 100%;
+}
+.caselog_header_date {
+}
+.caselog_header_user {
+}
\ No newline at end of file
diff --git a/lib/emogrifier/.gitignore b/lib/emogrifier/.gitignore
new file mode 100644
index 000000000..6be926125
--- /dev/null
+++ b/lib/emogrifier/.gitignore
@@ -0,0 +1,24 @@
+#########################
+# global ignore file
+########################
+# ignoring temporary files (left by e.g. vim)
+# ignoring by common IDE's used directories/files
+# dont ignore .rej and .orig as we want to see/clean files after conflict resolution
+#
+# for local exclude patterns please edit .git/info/exclude
+#
+*~
+*.bak
+*.idea
+*.project
+*.swp
+.buildpath
+.cache
+.project
+.session
+.settings
+.TemporaryItems
+.webprj
+nbproject
+/vendor/
+composer.lock
diff --git a/lib/emogrifier/.travis.yml b/lib/emogrifier/.travis.yml
new file mode 100644
index 000000000..961cb92de
--- /dev/null
+++ b/lib/emogrifier/.travis.yml
@@ -0,0 +1,31 @@
+sudo: false
+
+language: php
+
+cache:
+ directories:
+ - vendor
+
+env:
+ global:
+ secure: nOIIWvxRsDlkg+5H21dmVeqvFbweOAk3l3ZiyZO1m5XuGuuZR9yj10oOudee8m0hzJ7e9eoZ+dfB3t8lmK0fTRTB6w0G7RuGiQb89ief3Zhs1vOveYOgS5yfTMRym57iluxsLeCe7AxWmy7+0fWAvx1qL7bKp+THGK9yv/aj9eM=
+
+php:
+ - 5.4
+ - 5.5
+ - 5.6
+ - 7.0
+ - hhvm
+
+before_script:
+ - composer install
+ - vendor/bin/phpcs --config-set encoding utf-8
+ - if [ "$GITHUB_COMPOSER_AUTH" ]; then composer config -g github-oauth.github.com $GITHUB_COMPOSER_AUTH; fi
+
+script:
+ # Run PHP lint on all PHP files.
+ - find Classes/ Tests/ -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l
+ # Check the coding style.
+ - vendor/bin/phpcs --standard=Configuration/PhpCodeSniffer/Standards/Emogrifier/ Classes/ Tests/
+ # Run the unit tests.
+ - vendor/bin/phpunit Tests/
diff --git a/lib/emogrifier/CHANGELOG.md b/lib/emogrifier/CHANGELOG.md
new file mode 100644
index 000000000..fcad58b97
--- /dev/null
+++ b/lib/emogrifier/CHANGELOG.md
@@ -0,0 +1,92 @@
+# Emogrifier Change Log
+
+All notable changes to this project will be documented in this file.
+This project adheres to [Semantic Versioning](http://semver.org/).
+
+Emogrifier is in a pre-1.0 state. This means that its APIs and behavior are
+subject to breaking changes without deprecation notices.
+
+
+## [1.0.0][] (2015-10-15)
+
+### Added
+- Add branch alias ([#231](https://github.com/jjriv/emogrifier/pull/231))
+- Remove media queries which do not impact the document
+ ([#217](https://github.com/jjriv/emogrifier/pull/217))
+- Allow elements to be excluded from emogrification
+ ([#215](https://github.com/jjriv/emogrifier/pull/215))
+- Handle !important ([#214](https://github.com/jjriv/emogrifier/pull/214))
+- emogrifyBodyContent() method
+ ([#206](https://github.com/jjriv/emogrifier/pull/206))
+- Cache combinedStyles ([#211](https://github.com/jjriv/emogrifier/pull/211))
+- Allow user to define media types to keep
+ ([#200](https://github.com/jjriv/emogrifier/pull/200))
+- Ignore invalid CSS selectors
+ ([#194](https://github.com/jjriv/emogrifier/pull/194))
+- isRemoveDisplayNoneEnabled option
+ ([#162](https://github.com/jjriv/emogrifier/pull/162))
+- Allow disabling of "inline style" and "style block" parsing
+ ([#156](https://github.com/jjriv/emogrifier/pull/156))
+- Preserve @media if necessary
+ ([#62](https://github.com/jjriv/emogrifier/pull/62))
+- Add extraction of style blocks within the HTML
+- Add several new pseudo-selectors (first-child, last-child, nth-child,
+ and nth-of-type)
+
+
+### Changed
+- Make HTML5 the default document type
+ ([#245](https://github.com/jjriv/emogrifier/pull/245))
+- Make copyCssWithMediaToStyleNode private
+ ([#218](https://github.com/jjriv/emogrifier/pull/218))
+- Stop encoding umlauts and dollar signs
+ ([#170](https://github.com/jjriv/emogrifier/pull/170))
+- Convert the classes to namespaces
+ ([#41](https://github.com/jjriv/emogrifier/pull/41))
+
+
+### Deprecated
+- Support for PHP 5.4 will be removed in Emogrifier 2.0.
+
+
+### Removed
+- Drop support for PHP 5.3
+ ([#114](https://github.com/jjriv/emogrifier/pull/114))
+- Support for character sets other than UTF-8 was removed.
+
+
+### Fixed
+- Fix failing tests on Windows due to line endings
+ ([#263](https://github.com/jjriv/emogrifier/pull/263))
+- Parsing CSS declaration blocks
+ ([#261](https://github.com/jjriv/emogrifier/pull/261))
+- Fix first-child and last-child selectors
+ ([#257](https://github.com/jjriv/emogrifier/pull/257))
+- Fix parsing of CSS for data URIs
+ ([#243](https://github.com/jjriv/emogrifier/pull/243))
+- Fix multi-line media queries
+ ([#241](https://github.com/jjriv/emogrifier/pull/241))
+- Keep CSS media queries even if followed by CSS comments
+ ([#201](https://github.com/jjriv/emogrifier/pull/201))
+- Fix CSS selectors with exact attribute only
+ ([#197](https://github.com/jjriv/emogrifier/pull/197))
+- Properly handle UTF-8 characters and entities
+ ([#189](https://github.com/jjriv/emogrifier/pull/189))
+- Add mbstring extension to composer.json
+ ([#93](https://github.com/jjriv/emogrifier/pull/93))
+- Prevent incorrectly capitalized CSS selectors from being stripped
+ ([#85](https://github.com/jjriv/emogrifier/pull/85))
+- Fix CSS selectors with exact attribute only
+ ([#197](https://github.com/jjriv/emogrifier/pull/197))
+- Wrong selector extraction from minified CSS
+ ([#69](https://github.com/jjriv/emogrifier/pull/69))
+- Restore libxml error handler state after clearing
+ ([#65](https://github.com/jjriv/emogrifier/pull/65))
+- Ignore all warnings produced by DOMDocument::loadHTML()
+ ([#63](https://github.com/jjriv/emogrifier/pull/63))
+- Style tags in HTML cause an Xpath invalid query error
+ ([#60](https://github.com/jjriv/emogrifier/pull/60))
+- Fix PHP warnings with PHP 5.5
+ ([#26](https://github.com/jjriv/emogrifier/pull/26))
+- Make removal of invisible nodes operate in a case-insensitive manner
+- Fix a bug that was overwriting existing inline styles from the original HTML
diff --git a/lib/emogrifier/CONTRIBUTING.md b/lib/emogrifier/CONTRIBUTING.md
new file mode 100644
index 000000000..95294bfb4
--- /dev/null
+++ b/lib/emogrifier/CONTRIBUTING.md
@@ -0,0 +1,78 @@
+# Contributing to Emogrifier
+
+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:
+
+
+## General workflow
+
+After you have submitted a pull request, the Emogrifier team will review your
+changes. This will probably result in quite a few comments on ways to improve
+your pull request. The Emogrifier project receives contributions from
+developers around the world, so we need the code to be the most consistent,
+readable, and maintainable that it can be.
+
+Please do not feel frustrated by this - instead please view this both as our
+contribution to your pull request as well as a way to learn more about
+improving code quality.
+
+If you would like to know whether an idea would fit in the general strategy of
+the Emogrifier project or would like to get feedback on the best architecture
+for your ideas, we propose you open a ticket first and discuss your ideas there
+first before investing a lot of time in writing code.
+
+
+## Install the development dependencies
+
+To install the development dependencies (PHPUnit and PHP_CodeSniffer), please
+run the following command:
+
+ composer install
+
+
+## Unit-test your changes
+
+Please cover all changes with unit tests and make sure that your code does not
+break any existing tests. We will only merge pull request that include full
+code coverage of the fixed bugs and the new features.
+
+To run the existing PHPUnit tests, run this command:
+
+ vendor/bin/phpunit Tests/
+
+
+## Coding Style
+
+Please use the same coding style (PSR-2) as the rest of the code. Indentation
+is four spaces.
+
+We will only merge pull requests that follow the project's coding style.
+
+Please check your code with the provided PHP_CodeSniffer standard:
+
+ vendor/bin/phpcs --standard=Configuration/PhpCodeSniffer/Standards/Emogrifier/ Classes/ Tests/
+
+Please make your code clean, well-readable and easy to understand.
+
+If you add new methods or fields, please add proper PHPDoc for the new
+methods/fields. Please use grammatically correct, complete sentences in the
+code documentation.
+
+
+## Git commits
+
+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).
+
+If you already have a commit and work on it, you can also
+[amend the first commit](https://nathanhoad.net/git-amend-your-last-commit).
+
+Please use grammatically correct, complete sentences in the commit messages.
+
+Also, please prefix the subject line of the commit message with either
+[FEATURE], [TASK], [BUGFIX] OR [CLEANUP]. This makes it faster to see what
+a commit is about.
\ No newline at end of file
diff --git a/lib/emogrifier/Classes/Emogrifier.php b/lib/emogrifier/Classes/Emogrifier.php
new file mode 100644
index 000000000..adf7bf639
--- /dev/null
+++ b/lib/emogrifier/Classes/Emogrifier.php
@@ -0,0 +1,1020 @@
+
+ * @author Roman Ožana
+ */
+class Emogrifier
+{
+ /**
+ * @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_DECLARATIONS_BLOCK = 3;
+ /**
+ * @var int
+ */
+ const CACHE_KEY_COMBINED_STYLES = 4;
+ /**
+ * 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
+ */
+ const CONTENT_TYPE_META_TAG = '';
+ /**
+ * @var string
+ */
+ const DEFAULT_DOCUMENT_TYPE = '';
+ /**
+ * @var string
+ */
+ private $html = '';
+ /**
+ * @var string
+ */
+ private $css = '';
+ /**
+ * @var bool[]
+ */
+ private $excludedSelectors = array();
+ /**
+ * @var string[]
+ */
+ private $unprocessableHtmlTags = array('wbr');
+ /**
+ * @var bool[]
+ */
+ private $allowedMediaTypes = array('all' => true, 'screen' => true, 'print' => true);
+ /**
+ * @var array[]
+ */
+ private $caches = array(self::CACHE_KEY_CSS => array(), self::CACHE_KEY_SELECTOR => array(), self::CACHE_KEY_XPATH => array(), self::CACHE_KEY_CSS_DECLARATIONS_BLOCK => array(), self::CACHE_KEY_COMBINED_STYLES => array());
+ /**
+ * the visited nodes with the XPath paths as array keys
+ *
+ * @var \DOMElement[]
+ */
+ 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 target