Use only allowed attributes in the html parser.

This commit is contained in:
RainLoop Team 2016-07-13 22:38:20 +03:00
parent 0a6a84b3d7
commit 5a01b59d40
11 changed files with 137 additions and 1328 deletions

View file

@ -272,7 +272,7 @@ class HtmlUtils
$sHtml = \str_replace('<o:p></o:p>', '', $sHtml); $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);
$sHtml = \str_replace('</o:p>', '</span>', $sHtml); $sHtml = \str_replace('</o:p>', '</span>', $sHtml);
return $sHtml; return $sHtml;
@ -337,16 +337,23 @@ class HtmlUtils
$aRemoveTags[] = 'style'; $aRemoveTags[] = 'style';
} }
$aHtmlAllowedTags = isset(\MailSo\Config::$HtmlStrictAllowedTags) &&
\is_array(\MailSo\Config::$HtmlStrictAllowedTags) && 0 < \count(\MailSo\Config::$HtmlStrictAllowedTags) ?
\MailSo\Config::$HtmlStrictAllowedTags : null;
$aRemove = array(); $aRemove = array();
$aNodes = $oDom->getElementsByTagName('*'); $aNodes = $oDom->getElementsByTagName('*');
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement)
{ {
if ($oElement) if ($oElement)
{ {
$sTagNameLower = \strtolower($oElement->tagName); $sTagNameLower = \trim(\strtolower($oElement->tagName));
if ('' !== $sTagNameLower && \in_array($sTagNameLower, $aRemoveTags)) if ('' !== $sTagNameLower)
{ {
$aRemove[] = @$oElement; if (\in_array($sTagNameLower, $aRemoveTags) || ($aHtmlAllowedTags && !\in_array($sTagNameLower, $aHtmlAllowedTags)))
{
$aRemove[] = @$oElement;
}
} }
} }
} }
@ -913,6 +920,7 @@ class HtmlUtils
$aNodes = $oDom->getElementsByTagName('*'); $aNodes = $oDom->getElementsByTagName('*');
foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement) foreach ($aNodes as /* @var $oElement \DOMElement */ $oElement)
{ {
$aRemovedAttrs = array();
$sTagNameLower = \strtolower($oElement->tagName); $sTagNameLower = \strtolower($oElement->tagName);
// convert body attributes to styles // convert body attributes to styles
@ -929,14 +937,14 @@ class HtmlUtils
if (isset($oElement->attributes)) if (isset($oElement->attributes))
{ {
foreach ($oElement->attributes as $sAttributeName => /* @var $oAttributeNode \DOMNode */ $oAttributeNode) foreach ($oElement->attributes as $sAttrName => /* @var $oAttributeNode \DOMNode */ $oAttributeNode)
{ {
if ($oAttributeNode && isset($oAttributeNode->nodeValue)) if ($oAttributeNode && isset($oAttributeNode->nodeValue))
{ {
$sAttributeNameLower = \strtolower($sAttributeName); $sAttrNameLower = \trim(\strtolower($sAttrName));
if (isset($aAttrs[$sAttributeNameLower]) && '' === $aAttrs[$sAttributeNameLower]) if (isset($aAttrs[$sAttrNameLower]) && '' === $aAttrs[$sAttrNameLower])
{ {
$aAttrs[$sAttributeNameLower] = array($sAttributeName, \trim($oAttributeNode->nodeValue)); $aAttrs[$sAttrNameLower] = array($sAttrName, \trim($oAttributeNode->nodeValue));
} }
} }
} }
@ -998,33 +1006,43 @@ class HtmlUtils
'color: '.$sLinkColor.\trim((empty($sStyles) ? '' : '; '.$sStyles))); 'color: '.$sLinkColor.\trim((empty($sStyles) ? '' : '; '.$sStyles)));
} }
if (\in_array($sTagNameLower, array('a', 'form', 'area')))
{
$oElement->setAttribute('target', '_blank');
}
if (\in_array($sTagNameLower, array('a', 'form', 'area', 'input', 'button', 'textarea')))
{
$oElement->setAttribute('tabindex', '-1');
}
if ($oElement->hasAttributes() && isset($oElement->attributes) && $oElement->attributes) if ($oElement->hasAttributes() && isset($oElement->attributes) && $oElement->attributes)
{ {
foreach ($oElement->attributes as $oAttr) $aHtmlAllowedAttributes = isset(\MailSo\Config::$HtmlStrictAllowedAttributes) &&
\is_array(\MailSo\Config::$HtmlStrictAllowedAttributes) && 0 < \count(\MailSo\Config::$HtmlStrictAllowedAttributes) ?
\MailSo\Config::$HtmlStrictAllowedAttributes : null;
$sAttrsForRemove = array();
foreach ($oElement->attributes as $sAttrName => $oAttr)
{ {
if ($oAttr && !empty($oAttr->nodeName)) if ($sAttrName && $oAttr)
{ {
$sAttrName = \trim(\strtolower($oAttr->nodeName)); $sAttrNameLower = \trim(\strtolower($sAttrName));
if ('on' === \substr($sAttrName, 0, 2) || in_array($sAttrName, array( if ($aHtmlAllowedAttributes && !\in_array($sAttrNameLower, $aHtmlAllowedAttributes))
'id', 'class', 'contenteditable', 'designmode', 'formaction', 'manifest', {
$sAttrsForRemove[] = $sAttrName;
}
else if ('on' === \substr($sAttrNameLower, 0, 2) || in_array($sAttrNameLower, array(
'id', 'class', 'contenteditable', 'designmode', 'formaction', 'manifest', 'action',
'data-bind', 'data-reactid', 'xmlns', 'srcset', 'data-x-skip-style', 'data-bind', 'data-reactid', 'xmlns', 'srcset', 'data-x-skip-style',
'fscommand', 'seeksegmenttime' 'fscommand', 'seeksegmenttime'
))) )))
{ {
@$oElement->removeAttribute($oAttr->nodeName); $sAttrsForRemove[] = $sAttrName;
} }
} }
} }
if (0 < \count($sAttrsForRemove))
{
foreach ($sAttrsForRemove as $sName)
{
@$oElement->removeAttribute($sName);
$aRemovedAttrs[\trim(\strtolower($sName))] = true;
}
}
unset($sAttrsForRemove);
} }
if ($oElement->hasAttribute('href')) if ($oElement->hasAttribute('href'))
@ -1038,10 +1056,20 @@ class HtmlUtils
if ('a' === $sTagNameLower) if ('a' === $sTagNameLower)
{ {
$oElement->setAttribute('rel', 'external nofollow'); $oElement->setAttribute('rel', 'external nofollow noopener noreferrer');
} }
} }
if (\in_array($sTagNameLower, array('a', 'form', 'area')))
{
$oElement->setAttribute('target', '_blank');
}
if (\in_array($sTagNameLower, array('a', 'form', 'area', 'input', 'button', 'textarea')))
{
$oElement->setAttribute('tabindex', '-1');
}
if ($bTryToDetectHiddenImages && 'img' === $sTagNameLower) if ($bTryToDetectHiddenImages && 'img' === $sTagNameLower)
{ {
$sAlt = $oElement->hasAttribute('alt') $sAlt = $oElement->hasAttribute('alt')
@ -1177,6 +1205,17 @@ class HtmlUtils
} }
$oElement->removeAttribute('data-x-skip-style'); $oElement->removeAttribute('data-x-skip-style');
if (\MailSo\Config::$HtmlStrictDebug && 0 < \count($aRemovedAttrs))
{
unset($aRemovedAttrs['class'], $aRemovedAttrs['target'], $aRemovedAttrs['id'], $aRemovedAttrs['name']);
$aRemovedAttrs = \array_keys($aRemovedAttrs);
if (0 < \count($aRemovedAttrs))
{
$oElement->setAttribute('data-removed-attrs', \implode(',', $aRemovedAttrs));
}
}
} }
$sResult = \MailSo\Base\HtmlUtils::GetTextFromDom($oDom, $bWrapByFakeHtmlAndBodyDiv); $sResult = \MailSo\Base\HtmlUtils::GetTextFromDom($oDom, $bWrapByFakeHtmlAndBodyDiv);
@ -1251,16 +1290,6 @@ class HtmlUtils
$oElement->removeAttribute('data-x-href'); $oElement->removeAttribute('data-x-href');
} }
if ($oElement->hasAttribute('data-x-additional-src'))
{
$oElement->removeAttribute('data-x-additional-src');
}
if ($oElement->hasAttribute('data-x-additional-style-url'))
{
$oElement->removeAttribute('data-x-additional-style-url');
}
if ($oElement->hasAttribute('data-x-style-cid-name') && $oElement->hasAttribute('data-x-style-cid')) if ($oElement->hasAttribute('data-x-style-cid-name') && $oElement->hasAttribute('data-x-style-cid'))
{ {
$sCidName = $oElement->getAttribute('data-x-style-cid-name'); $sCidName = $oElement->getAttribute('data-x-style-cid-name');
@ -1290,14 +1319,15 @@ class HtmlUtils
} }
} }
if ($oElement->hasAttribute('data-original')) foreach (array(
'data-x-additional-src', 'data-x-additional-style-url', 'data-removed-attrs',
'data-original', 'data-x-div-type', 'data-wrp', 'data-bind'
) as $sName)
{ {
$oElement->removeAttribute('data-original'); if ($oElement->hasAttribute($sName))
} {
$oElement->removeAttribute($sName);
if ($oElement->hasAttribute('data-x-div-type')) }
{
$oElement->removeAttribute('data-x-div-type');
} }
if ($oElement->hasAttribute('data-x-style-url')) if ($oElement->hasAttribute('data-x-style-url'))

View file

@ -26,6 +26,21 @@ class Config
*/ */
public static $MBSTRING = true; public static $MBSTRING = true;
/**
* @var array|null
*/
public static $HtmlStrictAllowedTags = null;
/**
* @var array|null
*/
public static $HtmlStrictAllowedAttributes = null;
/**
* @var boolean
*/
public static $HtmlStrictDebug = false;
/** /**
* @var bool * @var bool
*/ */

View file

@ -9968,24 +9968,6 @@ NewThemeLink IncludeCss LoadingDescriptionEsc TemplatesLink LangLink IncludeBack
$mResult['InReplyTo'] = $mResponse->InReplyTo(); $mResult['InReplyTo'] = $mResponse->InReplyTo();
$mResult['References'] = $mResponse->References(); $mResult['References'] = $mResponse->References();
$fAdditionalDomReader = null;
if (0 < \strlen($sHtml) && $this->Config()->Get('labs', 'emogrifier', false))
{
if (!\class_exists('RainLoopVendor\Pelago\Emogrifier', false))
{
include_once APP_VERSION_ROOT_PATH.'app/libraries/emogrifier/Emogrifier.php';
}
if (\class_exists('RainLoopVendor\Pelago\Emogrifier', false))
{
$fAdditionalDomReader = function ($oDom) {
$oEmogrifier = new \RainLoopVendor\Pelago\Emogrifier();
$oEmogrifier->preserveEncoding = false;
return $oEmogrifier->emogrify($oDom);
};
}
}
$fAdditionalExternalFilter = null; $fAdditionalExternalFilter = null;
if (!!$this->Config()->Get('labs', 'use_local_proxy_for_external_images', false)) if (!!$this->Config()->Get('labs', 'use_local_proxy_for_external_images', false))
{ {
@ -10004,8 +9986,7 @@ NewThemeLink IncludeCss LoadingDescriptionEsc TemplatesLink LangLink IncludeBack
$mResult['Html'] = 0 === \strlen($sHtml) ? '' : \MailSo\Base\HtmlUtils::ClearHtml( $mResult['Html'] = 0 === \strlen($sHtml) ? '' : \MailSo\Base\HtmlUtils::ClearHtml(
$sHtml, $bHasExternals, $mFoundedCIDs, $aContentLocationUrls, $mFoundedContentLocationUrls, false, false, $sHtml, $bHasExternals, $mFoundedCIDs, $aContentLocationUrls, $mFoundedContentLocationUrls, false, false,
$fAdditionalExternalFilter, $fAdditionalDomReader, $fAdditionalExternalFilter, null, !!$this->Config()->Get('labs', 'try_to_detect_hidden_images', false)
!!$this->Config()->Get('labs', 'try_to_detect_hidden_images', false)
); );
$mResult['ExternalProxy'] = null !== $fAdditionalExternalFilter; $mResult['ExternalProxy'] = null !== $fAdditionalExternalFilter;

View file

@ -134,6 +134,52 @@ class Api
} }
}); });
} }
\MailSo\Config::$HtmlStrictDebug = !!\RainLoop\Api::Config()->Get('debug', 'enable', false);
if (\RainLoop\Api::Config()->Get('labs', 'strict_html_parser', true))
{
\MailSo\Config::$HtmlStrictAllowedAttributes = array(
// rainloop
'data-wrp',
// defaults
'name',
'dir', 'lang', 'style', 'title',
'background', 'bgcolor', 'alt', 'height', 'width', 'src', 'href',
'border', 'bordercolor', 'charset', 'direction', 'language',
// a
'coords', 'download', 'hreflang', 'shape',
// body
'alink', 'bgproperties', 'bottommargin', 'leftmargin', 'link', 'rightmargin', 'text', 'topmargin', 'vlink',
'marginwidth', 'marginheight', 'offset',
// button,
'disabled', 'type', 'value',
// col
'align', 'valign',
// font
'color', 'face', 'size',
// form
'novalidate',
// hr
'noshade',
// img
'hspace', 'sizes', 'srcset', 'vspace', 'usemap',
// input, textarea
'checked', 'max', 'min', 'maxlength', 'multiple', 'pattern', 'placeholder', 'readonly', 'required', 'step', 'wrap',
// label
'for',
// meter
'low', 'high', 'optimum',
// ol
'reversed', 'start',
// option
'selected', 'label',
// table
'cols', 'rows', 'frame', 'rules', 'summary', 'cellpadding', 'cellspacing',
// td
'abbr', 'axis', 'colspan', 'rowspan', 'headers', 'nowrap'
);
}
} }
} }

View file

@ -446,8 +446,8 @@ Enables caching in the system'),
'cookie_default_secure' => array(false), 'cookie_default_secure' => array(false),
'replace_env_in_configuration' => array(''), 'replace_env_in_configuration' => array(''),
'startup_url' => array(''), 'startup_url' => array(''),
'emogrifier' => array(true),
'nice_social_redirect' => array(true), 'nice_social_redirect' => array(true),
'strict_html_parser' => array(false),
'dev_email' => array(''), 'dev_email' => array(''),
'dev_password' => array('') 'dev_password' => array('')
), ),

View file

@ -1,73 +0,0 @@
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')

View file

@ -1,981 +0,0 @@
<?php
namespace RainLoopVendor\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;
}
}

View file

@ -1,21 +0,0 @@
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.

View file

@ -1,185 +0,0 @@
# Emogrifier
[![Build Status](https://travis-ci.org/jjriv/emogrifier.svg?branch=master)](https://travis-ci.org/jjriv/emogrifier)
[![Latest Stable Version](https://poser.pugx.org/pelago/emogrifier/v/stable.svg)](https://packagist.org/packages/pelago/emogrifier)
[![Total Downloads](https://poser.pugx.org/pelago/emogrifier/downloads.svg)](https://packagist.org/packages/pelago/emogrifier)
[![Latest Unstable Version](https://poser.pugx.org/pelago/emogrifier/v/unstable.svg)](https://packagist.org/packages/pelago/emogrifier)
[![License](https://poser.pugx.org/pelago/emogrifier/license.svg)](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.

View file

@ -145,9 +145,7 @@
<div class="input-append pull-right" data-bind="visible: allowSearch"> <div class="input-append pull-right" data-bind="visible: allowSearch">
<div class="close-input-wrp"> <div class="close-input-wrp">
<a class="close" data-bind="click: cancelSearch, visible: '' !== messageListSearchDesc()">&times;</a> <a class="close" data-bind="click: cancelSearch, visible: '' !== messageListSearchDesc()">&times;</a>
<input type="search" class="i18n span4 inputSearch" tabindex="-1" <input type="text" class="i18n span4 inputSearch" tabindex="-1" placeholder="Search" autocorrect="off" autocapitalize="off" data-i18n="[placeholder]SEARCH/MAIN_INPUT_PLACEHOLDER" data-bind="value: inputProxyMessageListSearch, onEnter: searchEnterAction, hasfocus: inputMessageListSearchFocus" />
placeholder="Search" autocorrect="off" autocapitalize="off"
data-i18n="[placeholder]SEARCH/MAIN_INPUT_PLACEHOLDER" data-bind="value: inputProxyMessageListSearch, onEnter: searchEnterAction, hasfocus: inputMessageListSearchFocus" />
</div> </div>
<a class="btn buttonMoreSearch" data-bind="visible: allowSearchAdv, click: advancedSearchClick"> <a class="btn buttonMoreSearch" data-bind="visible: allowSearchAdv, click: advancedSearchClick">
<span class="caret"></span> <span class="caret"></span>

View file

@ -69,8 +69,7 @@
</div> </div>
<div class="modal-body" style="position: relative;"> <div class="modal-body" style="position: relative;">
<div class="b-list-toolbar"> <div class="b-list-toolbar">
<input type="search" class="i18n span3 e-search" placeholder="Search" autocorrect="off" autocapitalize="off" <input type="text" class="i18n span3 e-search" placeholder="Search" autocorrect="off" autocapitalize="off" data-i18n="[placeholder]CONTACTS/SEARCH_INPUT_PLACEHOLDER" data-bind="value: search" />
data-i18n="[placeholder]CONTACTS/SEARCH_INPUT_PLACEHOLDER" data-bind="value: search" />
</div> </div>
<div class="b-list-content g-ui-user-select-none" data-bind="nano: true, scrollerShadows: true, css: {'hideContactListCheckbox': !useCheckboxesInList()}"> <div class="b-list-content g-ui-user-select-none" data-bind="nano: true, scrollerShadows: true, css: {'hideContactListCheckbox': !useCheckboxesInList()}">
<div class="content g-scrollbox" data-scroller-shadows-content> <div class="content g-scrollbox" data-scroller-shadows-content>