InlineCssLib.php 7.8 KB

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