InlineCssLib.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. <?php
  2. /**
  3. * Wrapper for Inline CSS replacement.
  4. * Useful for sending HTML emails.
  5. *
  6. * Note: requires vendors CssToInline or emogrifier!
  7. * Default engine: CssToInline
  8. *
  9. * @author Mark Scherer
  10. * @copyright Mark Scherer
  11. * @license MIT
  12. */
  13. class InlineCssLib {
  14. const ENGINE_CSS_TO_INLINE = 'cssToInline';
  15. const ENGINE_EMOGRIFIER = 'emogrifier';
  16. /**
  17. * Default config
  18. *
  19. * @var array
  20. */
  21. protected $_defaults = array(
  22. 'engine' => self::ENGINE_CSS_TO_INLINE,
  23. 'cleanup' => true,
  24. 'useInlineStylesBlock' => true,
  25. 'debug' => false, // only cssToInline
  26. 'xhtmlOutput' => false, // only cssToInline
  27. 'removeCss' => true, // only cssToInline
  28. 'correctUtf8' => false // only cssToInline
  29. );
  30. public $config = array();
  31. /**
  32. * Inits with auto merged config.
  33. */
  34. public function __construct($config = array()) {
  35. $defaults = (array)Configure::read('InlineCss') + $this->_defaults;
  36. $this->config = $config + $defaults;
  37. if (!method_exists($this, '_process' . ucfirst($this->config['engine']))) {
  38. throw new InternalErrorException('Engine does not exist: ' . $this->config['engine']);
  39. }
  40. }
  41. /**
  42. * Processes HTML and CSS.
  43. *
  44. * @return string Result
  45. */
  46. public function process($html, $css = null) {
  47. if (($html = trim($html)) === '') {
  48. return $html;
  49. }
  50. $method = '_process' . ucfirst($this->config['engine']);
  51. return $this->{$method}($html, $css);
  52. }
  53. /**
  54. * @return string Result
  55. */
  56. protected function _processEmogrifier($html, $css) {
  57. //$css .= $this->_extractAndRemoveCss($html);
  58. App::import('Vendor', 'Tools.Emogrifier', array('file' => 'Emogrifier/Emogrifier.php'));
  59. $Emogrifier = new Emogrifier($html, $css);
  60. //$Emogrifier->preserveEncoding = true;
  61. $result = $Emogrifier->emogrify();
  62. if ($this->config['cleanup']) {
  63. // Remove comments and whitespace
  64. $result = preg_replace( '/<!--(.|\s)*?-->/', '', $result);
  65. $result = preg_replace( '/\s\s+/', '', $result);
  66. // Result classes and ids
  67. $result = preg_replace('/\bclass="[^"]*"/', '', $result);
  68. $result = preg_replace('/\bid="[^"]*"/', '', $result);
  69. }
  70. return $result;
  71. }
  72. /**
  73. * Process css blocks to inline css
  74. * Also works for html snippets (without <html>)
  75. *
  76. * @return string HTML output
  77. */
  78. protected function _processCssToInline($html, $css) {
  79. App::import('Vendor', 'Tools.CssToInlineStyles', array('file' => 'CssToInlineStyles' . DS . 'CssToInlineStyles.php'));
  80. //fix issue with <html> being added
  81. $separator = '~~~~~~~~~~~~~~~~~~~~';
  82. if (strpos($html, '<html') === false) {
  83. $incomplete = true;
  84. $html = $separator . $html . $separator;
  85. }
  86. $CssToInlineStyles = new CssToInlineStyles($html, $css);
  87. if ($this->config['cleanup']) {
  88. $CssToInlineStyles->setCleanup();
  89. }
  90. if ($this->config['useInlineStylesBlock']) {
  91. $CssToInlineStyles->setUseInlineStylesBlock();
  92. }
  93. if ($this->config['removeCss']) {
  94. $CssToInlineStyles->setStripOriginalStyleTags();
  95. }
  96. if ($this->config['correctUtf8']) {
  97. $CssToInlineStyles->setCorrectUtf8();
  98. }
  99. if ($this->config['debug']) {
  100. CakeLog::write('css', $html);
  101. }
  102. $html = $CssToInlineStyles->convert($this->config['xhtmlOutput']);
  103. if ($this->config['removeCss']) {
  104. //$html = preg_replace('/\<style(.*)\>(.*)\<\/style\>/i', '', $html);
  105. $html = $this->stripOnly($html, array('style', 'script'), true);
  106. //CakeLog::write('css', $html);
  107. }
  108. if (!empty($incomplete)) {
  109. $html = substr($html, strpos($html, $separator) + 20);
  110. $html = substr($html, 0, strpos($html, $separator));
  111. $html = trim($html);
  112. }
  113. return $html;
  114. }
  115. /**
  116. * Some reverse function of strip_tags with blacklisting instead of whitelisting
  117. * //maybe move to Tools.Utility/String/Text?
  118. *
  119. * @return string cleanedStr
  120. */
  121. public function stripOnly($str, $tags, $stripContent = false) {
  122. $content = '';
  123. if (!is_array($tags)) {
  124. $tags = (strpos($str, '>') !== false ? explode('>', str_replace('<', '', $tags)) : array($tags));
  125. if (end($tags) === '') {
  126. array_pop($tags);
  127. }
  128. }
  129. foreach ($tags as $tag) {
  130. if ($stripContent) {
  131. $content = '(.+</' . $tag . '[^>]*>|)';
  132. }
  133. $str = preg_replace('#</?' . $tag . '[^>]*>' . $content . '#is', '', $str);
  134. }
  135. return $str;
  136. }
  137. /**
  138. * _extractAndRemoveCss - extracts any CSS from the rendered view and
  139. * removes it from the $html
  140. *
  141. * @return string
  142. */
  143. protected function _extractAndRemoveCss($html) {
  144. $css = null;
  145. $DOM = new DOMDocument;
  146. $DOM->loadHTML($html);
  147. // DOM removal queue
  148. $removeDoms = array();
  149. // catch <link> style sheet content
  150. $links = $DOM->getElementsByTagName('link');
  151. foreach ($links as $link) {
  152. if ($link->hasAttribute('href') && preg_match("/\.css$/i", $link->getAttribute('href'))) {
  153. // find the css file and load contents
  154. if ($link->hasAttribute('media')) {
  155. // FOR NOW
  156. continue;
  157. foreach ($this->mediaTypes as $cssLinkMedia) {
  158. if (strstr($link->getAttribute('media'), $cssLinkMedia)) {
  159. $css .= $this->_findAndLoadCssFile($link->getAttribute('href')) . "\n\n";
  160. $removeDoms[] = $link;
  161. }
  162. }
  163. } else {
  164. $css .= $this->_findAndLoadCssFile($link->getAttribute('href')) . "\n\n";
  165. $removeDoms[] = $link;
  166. }
  167. }
  168. }
  169. // Catch embeded <style> and @import CSS content
  170. $styles = $DOM->getElementsByTagName('style');
  171. // Style
  172. foreach ($styles as $style) {
  173. if ($style->hasAttribute('media')) {
  174. foreach ($this->mediaTypes as $cssLinkMedia) {
  175. if (strstr($style->getAttribute('media'), $cssLinkMedia)) {
  176. $css .= $this->_parseInlineCssAndLoadImports($style->nodeValue);
  177. $removeDoms[] = $style;
  178. }
  179. }
  180. } else {
  181. $css .= $this->_parseInlineCssAndLoadImports($style->nodeValue);
  182. $removeDoms[] = $style;
  183. }
  184. }
  185. // Remove
  186. if ($this->config['removeCss']) {
  187. foreach ($removeDoms as $removeDom) {
  188. try {
  189. $removeDom->parentNode->removeChild($removeDom);
  190. } catch (DOMException $e) {}
  191. }
  192. $html = $DOM->saveHTML();
  193. }
  194. return $html;
  195. }
  196. /**
  197. * _findAndLoadCssFile - finds the appropriate css file within the CSS path
  198. *
  199. * @param string $cssHref
  200. * @return string Content
  201. */
  202. protected function _findAndLoadCssFile($cssHref) {
  203. $cssFilenames = array_merge($this->_globRecursive(CSS . '*.Css'), $this->_globRecursive(CSS . '*.CSS'), $this->_globRecursive(CSS . '*.css'));
  204. // Build an array of the ever more path specific $cssHref location
  205. $cssHref = str_replace(array('\\', '/'), '/', $cssHref);
  206. $cssHrefs = explode(DS, $cssHref);
  207. $cssHrefPaths = array();
  208. for ($i = count($cssHrefs) - 1; $i > 0; $i--) {
  209. if (isset($cssHrefPaths[count($cssHrefPaths) - 1])) {
  210. $cssHrefPaths[] = $cssHrefs[$i] . DS . $cssHrefPaths[count($cssHrefPaths) - 1];
  211. } else {
  212. $cssHrefPaths[] = $cssHrefs[$i];
  213. }
  214. }
  215. // the longest string match will be the match we are looking for
  216. $bestCssFilename = null;
  217. $bestCssMatchLength = 0;
  218. foreach ($cssFilenames as $cssFilename) {
  219. foreach ($cssHrefPaths as $cssHrefPath) {
  220. $regex = '/' . str_replace('/', '\/', str_replace('.', '\.', $cssHrefPath)) . '/';
  221. if (preg_match($regex, $cssFilename, $match)) {
  222. if (strlen($match[0]) > $bestCssMatchLength) {
  223. $bestCssMatchLength = strlen($match[0]);
  224. $bestCssFilename = $cssFilename;
  225. }
  226. }
  227. }
  228. }
  229. $css = null;
  230. if (!empty($bestCssFilename) && is_file($bestCssFilename)) {
  231. $css = file_get_contents($bestCssFilename);
  232. }
  233. return $css;
  234. }
  235. /**
  236. * _globRecursive
  237. *
  238. * @param string $pattern
  239. * @param int $flags
  240. * @return array
  241. */
  242. protected function _globRecursive($pattern, $flags = 0) {
  243. $files = glob($pattern, $flags);
  244. foreach (glob(dirname($pattern) . '/*', GLOB_ONLYDIR | GLOB_NOSORT) as $dir) {
  245. $files = array_merge($files, $this->_globRecursive($dir . '/' . basename($pattern), $flags));
  246. }
  247. return $files;
  248. }
  249. /**
  250. * _parseInlineCssAndLoadImports
  251. *
  252. * @param string Input
  253. * @return string Result
  254. */
  255. protected function _parseInlineCssAndLoadImports($css) {
  256. // Load up the @import CSS if any exists
  257. preg_match_all("/\@import.*?url\((.*?)\)/i", $css, $matches);
  258. if (isset($matches[1]) && is_array($matches[1])) {
  259. // First remove the @imports
  260. $css = preg_replace("/\@import.*?url\(.*?\).*?;/i", '', $css);
  261. foreach ($matches[1] as $url) {
  262. if (preg_match("/^http/i", $url)) {
  263. if ($this->importExternalCss) {
  264. $css .= file_get_contents($url);
  265. }
  266. } else {
  267. $css .= $this->_findAndLoadCssFile($url);
  268. }
  269. }
  270. }
  271. return $css;
  272. }
  273. }