PluginProbe
Autoptimize / 1.8.2
Autoptimize v1.8.2
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
← All changes | classes/autoptimizeStyles.php +448 -1258 2.7.61.8.2 View file →
@@ -1,1278 +1,468 @@
1 1 <?php
2 -/**
3 - * Class for CSS optimization.
4 - */
2 +if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
5 3
6 -if ( ! defined( 'ABSPATH' ) ) {
7 - exit;
8 -}
4 +class autoptimizeStyles extends autoptimizeBase {
5 + private $css = array();
6 + private $csscode = array();
7 + private $url = array();
8 + private $restofcontent = '';
9 + private $mhtml = '';
10 + private $datauris = false;
11 + private $hashmap = array();
12 +
13 + //Reads the page and collects style tags
14 + public function read($options) {
15 + //Remove everything that's not the header
16 + if($options['justhead'] == true) {
17 + $content = explode('</head>',$this->content,2);
18 + $this->content = $content[0].'</head>';
19 + $this->restofcontent = $content[1];
20 + }
21 +
22 + // what CSS shouldn't be autoptimized
23 + $excludeCSS = $options['css_exclude'];
24 + $excludeCSS = apply_filters( 'autoptimize_filter_css_exclude', $excludeCSS );
25 + if ($excludeCSS!=="") {
26 + $this->dontmove = array_filter(array_map('trim',explode(",",$excludeCSS)));
27 + }
9 28
10 -class autoptimizeStyles extends autoptimizeBase
11 -{
12 - const ASSETS_REGEX = '/url\s*\(\s*(?!["\']?data:)(?![\'|\"]?[\#|\%|])([^)]+)\s*\)([^;},\s]*)/i';
29 + // should we defer css?
30 + $this->defer = $options['defer'];
31 +
32 + // should we inline?
33 + $this->inline = $options['inline'];
34 +
35 + // get cdn url
36 + $this->cdn_url = $options['cdn_url'];
37 +
38 + // Store data: URIs setting for later use
39 + $this->datauris = $options['datauris'];
40 +
41 + // noptimize me
42 + $this->content = $this->hide_noptimize($this->content);
43 +
44 + // exclude noscript, as those may contain CSS
45 + if ( strpos( $this->content, '<noscript>' ) !== false ) {
46 + $this->content = preg_replace_callback(
47 + '#<noscript>.*?</noscript>#is',
48 + create_function(
49 + '$matches',
50 + 'return "%%NOSCRIPT%%".base64_encode($matches[0])."%%NOSCRIPT%%";'
51 + ),
52 + $this->content
53 + );
54 + }
13 55
14 - /**
15 - * Font-face regex-fu from HamZa at: https://stackoverflow.com/a/21395083
16 - */
17 - const FONT_FACE_REGEX = '~@font-face\s*(\{(?:[^{}]+|(?1))*\})~xsi'; // added `i` flag for case-insensitivity.
56 + // Save IE hacks
57 + $this->content = $this->hide_iehacks($this->content);
18 58
19 - /**
20 - * Store CSS.
21 - *
22 - * @var array
23 - */
24 - private $css = array();
59 + // hide comments
60 + $this->content = $this->hide_comments($this->content);
61 +
62 + // Get <style> and <link>
63 + if(preg_match_all('#(<style[^>]*>.*</style>)|(<link[^>]*stylesheet[^>]*>)#Usmi',$this->content,$matches)) {
64 + foreach($matches[0] as $tag) {
65 + if ($this->ismovable($tag)) {
66 + // Get the media
67 + if(strpos($tag,'media=')!==false) {
68 + preg_match('#media=(?:"|\')([^>]*)(?:"|\')#Ui',$tag,$medias);
69 + $medias = explode(',',$medias[1]);
70 + $media = array();
71 + foreach($medias as $elem) {
72 + // $media[] = current(explode(' ',trim($elem),2));
73 + $media[] = $elem;
74 + }
75 + } else {
76 + //No media specified - applies to all
77 + $media = array('all');
78 + }
79 +
80 + if(preg_match('#<link.*href=("|\')(.*)("|\')#Usmi',$tag,$source)) {
81 + //<link>
82 + $url = current(explode('?',$source[2],2));
83 + $path = $this->getpath($url);
84 +
85 + if($path !==false && preg_match('#\.css$#',$path)) {
86 + //Good link
87 + $this->css[] = array($media,$path);
88 + }else{
89 + //Link is dynamic (.php etc)
90 + $tag = '';
91 + }
92 + } else {
93 + //<style>
94 + preg_match('#<style.*>(.*)</style>#Usmi',$tag,$code);
95 + $code = preg_replace('#^.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*$#sm','$1',$code[1]);
96 + $this->css[] = array($media,'INLINE;'.$code);
97 + }
98 +
99 + //Remove the original style tag
100 + $this->content = str_replace($tag,'',$this->content);
101 + }
102 + }
103 + return true;
104 + }
105 + // Really, no styles?
106 + return false;
107 + }
108 +
109 + // Joins and optimizes CSS
110 + public function minify() {
111 + foreach($this->css as $group) {
112 + list($media,$css) = $group;
113 + if(preg_match('#^INLINE;#',$css)) {
114 + //<style>
115 + $css = preg_replace('#^INLINE;#','',$css);
116 + $css = $this->fixurls(ABSPATH.'/index.php',$css);
117 + } else {
118 + //<link>
119 + if($css !== false && file_exists($css) && is_readable($css)) {
120 + $css = $this->fixurls($css,file_get_contents($css));
121 + $css = preg_replace('/\x{EF}\x{BB}\x{BF}/','',$css);
122 + } else {
123 + //Couldn't read CSS. Maybe getpath isn't working?
124 + $css = '';
125 + }
126 + }
127 +
128 + foreach($media as $elem) {
129 + if(!isset($this->csscode[$elem]))
130 + $this->csscode[$elem] = '';
131 + $this->csscode[$elem] .= "\n/*FILESTART*/".$css;
132 + }
133 + }
134 +
135 + // Check for duplicate code
136 + $md5list = array();
137 + $tmpcss = $this->csscode;
138 + foreach($tmpcss as $media => $code) {
139 + $md5sum = md5($code);
140 + $medianame = $media;
141 + foreach($md5list as $med => $sum) {
142 + //If same code
143 + if($sum === $md5sum) {
144 + //Add the merged code
145 + $medianame = $med.', '.$media;
146 + $this->csscode[$medianame] = $code;
147 + $md5list[$medianame] = $md5list[$med];
148 + unset($this->csscode[$med], $this->csscode[$media]);
149 + unset($md5list[$med]);
150 + }
151 + }
152 + $md5list[$medianame] = $md5sum;
153 + }
154 + unset($tmpcss);
155 +
156 + //Manage @imports, while is for recursive import management
157 + foreach($this->csscode as &$thiscss) {
158 + // Flag to trigger import reconstitution and var to hold external imports
159 + $fiximports = false;
160 + $external_imports = "";
25 161
26 - /**
27 - * To store CSS code
28 - *
29 - * @var array
30 - */
31 - private $csscode = array();
162 + while(preg_match_all('#^(/*\s?)@import.*(?:;|$)#Um',$thiscss,$matches)) {
163 + foreach($matches[0] as $import) {
164 + $url = trim(preg_replace('#^.*((?:https?|ftp)://.*\.css).*$#','$1',trim($import))," \t\n\r\0\x0B\"'");
165 + $path = $this->getpath($url);
166 + $import_ok = false;
167 + if (file_exists($path) && is_readable($path)) {
168 + $code = addcslashes($this->fixurls($path,file_get_contents($path)),"\\");
169 + $code = preg_replace('/\x{EF}\x{BB}\x{BF}/','',$code);
170 + if(!empty($code)) {
171 + $tmp_thiscss = preg_replace('#(/\*FILESTART\*/.*)'.preg_quote($import,'#').'#Us','/*FILESTART2*/'.$code.'$1',$thiscss);
172 + if (!empty($tmp_thiscss)) {
173 + $thiscss = $tmp_thiscss;
174 + $import_ok = true;
175 + unset($tmp_thiscss);
176 + }
177 + unset($code);
178 + }
179 + }
32 180
33 - /**
34 - * To store urls
35 - *
36 - * @var array
37 - */
38 - private $url = array();
181 + if (!$import_ok) {
182 + // external imports and general fall-back
183 + $external_imports .= $import;
184 + $thiscss = str_replace($import,'',$thiscss);
185 + $fiximports = true;
186 + }
187 + }
188 + $thiscss = preg_replace('#/\*FILESTART\*/#','',$thiscss);
189 + $thiscss = preg_replace('#/\*FILESTART2\*/#','/*FILESTART*/',$thiscss);
190 + }
191 +
192 + // add external imports to top of aggregated CSS
193 + if($fiximports) {
194 + $thiscss=$external_imports.$thiscss;
195 + }
196 + }
197 + unset($thiscss);
198 +
199 + // $this->csscode has all the uncompressed code now.
200 + $mhtmlcount = 0;
201 + foreach($this->csscode as &$code) {
202 + // Check for already-minified code
203 + $hash = md5($code);
204 + $ccheck = new autoptimizeCache($hash,'css');
205 + if($ccheck->check()) {
206 + $code = $ccheck->retrieve();
207 + $this->hashmap[md5($code)] = $hash;
208 + continue;
209 + }
210 + unset($ccheck);
211 + //Do the imaging!
212 + $imgreplace = array();
213 + preg_match_all('#(background[^;}]*url\((?!data)(.*)\)[^;}]*)(?:;|$|})#Usm',$code,$matches);
214 +
215 + if(($this->datauris == true) && (function_exists('base64_encode')) && (is_array($matches))) {
216 + foreach($matches[2] as $count => $quotedurl) {
217 + $url = trim($quotedurl," \t\n\r\0\x0B\"'");
39 218
40 - /**
41 - * String to store rest of content (when old setting "only in head" is used)
42 - *
43 - * @var string
44 - */
45 - private $restofcontent = '';
219 + // if querystring, remove it from url
220 + if (strpos($url,'?') !== false) { $url = reset(explode('?',$url)); }
221 +
222 + $path = $this->getpath($url);
46 223
47 - /**
48 - * Setting to change small images to inline CSS
49 - *
50 - * @var bool
51 - */
52 - private $datauris = false;
224 + $datauri_max_size = 2560;
225 + $datauri_max_size = (int) apply_filters( 'autoptimize_filter_css_datauri_maxsize', $datauri_max_size );
226 +
227 + if($path != false && preg_match('#\.(jpe?g|png|gif|bmp)$#',$path) && file_exists($path) && is_readable($path) && filesize($path) <= $datauri_max_size) {
228 + //It's an image, get the type
229 + $type=end(explode('.',$path));
230 + switch($type) {
231 + case 'jpeg':
232 + $dataurihead = 'data:image/jpeg;base64,';
233 + break;
234 + case 'jpg':
235 + $dataurihead = 'data:image/jpeg;base64,';
236 + break;
237 + case 'gif':
238 + $dataurihead = 'data:image/gif;base64,';
239 + break;
240 + case 'png':
241 + $dataurihead = 'data:image/png;base64,';
242 + break;
243 + case 'bmp':
244 + $dataurihead = 'data:image/bmp;base64,';
245 + break;
246 + default:
247 + $dataurihead = 'data:application/octet-stream;base64,';
248 + }
249 +
250 + //Encode the data
251 + $base64data = base64_encode(file_get_contents($path));
252 +
253 + //Add it to the list for replacement
254 + $imgreplace[$matches[1][$count]] = str_replace($quotedurl,$dataurihead.$base64data,$matches[1][$count]).';\n*'.str_replace($quotedurl,'mhtml:%%MHTML%%!'.$mhtmlcount,$matches[1][$count]).";\n_".$matches[1][$count].';';
255 +
256 + //Store image on the mhtml document
257 + $this->mhtml .= "--_\r\nContent-Location:{$mhtmlcount}\r\nContent-Transfer-Encoding:base64\r\n\r\n{$base64data}\r\n";
258 + $mhtmlcount++;
259 + }
260 + }
261 + } else if ((is_array($matches)) && (!empty($this->cdn_url))) {
262 + // change background image urls to cdn-url
263 + foreach($matches[2] as $count => $quotedurl) {
264 + $url = trim($quotedurl," \t\n\r\0\x0B\"'");
265 + $cdn_url=$this->url_replace_cdn($url);
266 + $imgreplace[$matches[1][$count]] = str_replace($quotedurl,$cdn_url,$matches[1][$count]);
267 + }
268 + }
269 +
270 + if(!empty($imgreplace)) {
271 + $code = str_replace(array_keys($imgreplace),array_values($imgreplace),$code);
272 + }
273 +
274 + //Minify
275 + if (class_exists('Minify_CSS_Compressor')) {
276 + $tmp_code = trim(Minify_CSS_Compressor::process($code));
277 + } else if(class_exists('CSSmin')) {
278 + $cssmin = new CSSmin();
279 + if (method_exists($cssmin,"run")) {
280 + $tmp_code = trim($cssmin->run($code));
281 + } elseif (is_callable(array($cssmin,"minify"))) {
282 + $tmp_code = trim(CssMin::minify($code));
283 + }
284 + }
285 +
286 + if (!empty($tmp_code)) {
287 + $code = $tmp_code;
288 + unset($tmp_code);
289 + }
290 +
291 + $this->hashmap[md5($code)] = $hash;
292 + }
293 + unset($code);
294 + return true;
295 + }
296 +
297 + //Caches the CSS in uncompressed, deflated and gzipped form.
298 + public function cache() {
299 + if($this->datauris) {
300 + // MHTML Preparation
301 + $this->mhtml = "/*\r\nContent-Type: multipart/related; boundary=\"_\"\r\n\r\n".$this->mhtml."*/\r\n";
302 + $md5 = md5($this->mhtml);
303 + $cache = new autoptimizeCache($md5,'txt');
304 + if(!$cache->check()) {
305 + //Cache our images for IE
306 + $cache->cache($this->mhtml,'text/plain');
307 + }
308 + $mhtml = AUTOPTIMIZE_CACHE_URL.$cache->getname();
309 + }
310 +
311 + //CSS cache
312 + foreach($this->csscode as $media => $code) {
313 + $md5 = $this->hashmap[md5($code)];
53 314
54 - /**
55 - * Array to store hashmap
56 - *
57 - * @var array
58 - */
59 - private $hashmap = array();
315 + if($this->datauris) {
316 + // Images for ie! Get the right url
317 + $code = str_replace('%%MHTML%%',$mhtml,$code);
318 + }
319 +
320 + $cache = new autoptimizeCache($md5,'css');
321 + if(!$cache->check()) {
322 + // Cache our code
323 + $cache->cache($code,'text/css');
324 + }
325 + $this->url[$media] = AUTOPTIMIZE_CACHE_URL.$cache->getname();
326 + }
327 + }
328 +
329 + //Returns the content
330 + public function getcontent() {
331 + // restore IE hacks
332 + $this->content = $this->restore_iehacks($this->content);
60 333
61 - /**
62 - * Flag to indicate if CSS is already minified
63 - *
64 - * @var bool
65 - */
66 - private $alreadyminified = false;
334 + // restore comments
335 + $this->content = $this->restore_comments($this->content);
336 +
337 + // restore noscript
338 + if ( strpos( $this->content, '%%NOSCRIPT%%' ) !== false ) {
339 + $this->content = preg_replace_callback(
340 + '#%%NOSCRIPT%%(.*?)%%NOSCRIPT%%#is',
341 + create_function(
342 + '$matches',
343 + 'return stripslashes(base64_decode($matches[1]));'
344 + ),
345 + $this->content
346 + );
347 + }
67 348
68 - /**
69 - * Setting if CSS should be aggregated
70 - *
71 - * @var bool
72 - */
73 - private $aggregate = true;
349 + // restore noptimize
350 + $this->content = $this->restore_noptimize($this->content);
351 +
352 + //Restore the full content
353 + if(!empty($this->restofcontent)) {
354 + $this->content .= $this->restofcontent;
355 + $this->restofcontent = '';
356 + }
357 +
358 + $warn_html_template=false;
74 359
75 - /**
76 - * Setting if all CSS should be inlined
77 - *
78 - * @var bool
79 - */
80 - private $inline = false;
360 + //Add the new stylesheets
361 + if ($this->inline == true) {
362 + foreach($this->csscode as $media => $code) {
363 + if (strpos($this->content,"<title")!==false) {
364 + $this->content = str_replace('<title','<style type="text/css" media="'.$media.'">'.$code.'</style><title',$this->content);
365 + } else {
366 + $warn_html_template=true;
367 + $this->content .= '<style type="text/css" media="'.$media.'">'.$code.'</style>';
368 + }
369 + }
370 + } else {
371 + if($this->defer == true) {
372 + $deferredCssBlock = "<script>function lCss(url,media) {var d=document;var l=d.createElement('link');l.rel='stylesheet';l.type='text/css';l.href=url;l.media=media; d.getElementsByTagName('head')[0].appendChild(l);}function deferredCSS() {";
373 + $noScriptCssBlock = "<noscript>";
374 + }
81 375
82 - /**
83 - * Setting if CSS should be deferred
84 - *
85 - * @var bool
86 - */
87 - private $defer = false;
376 + foreach($this->url as $media => $url) {
377 + $url = $this->url_replace_cdn($url);
378 +
379 + //Add the stylesheet either deferred (import at bottom) or normal links in head
380 + if($this->defer == true) {
381 + $deferredCssBlock .= "lCss('".$url."','".$media."');";
382 + $noScriptCssBlock .= '<link type="text/css" media="'.$media.'" href="'.$url.'" rel="stylesheet" />';
383 + } else {
384 + if (strpos($this->content,"<title")!==false) {
385 + $this->content = str_replace('<title','<link type="text/css" media="'.$media.'" href="'.$url.'" rel="stylesheet" /><title',$this->content);
386 + } else {
387 + $warn_html_template=true;
388 + $this->content .= '<link type="text/css" media="'.$media.'" href="'.$url.'" rel="stylesheet" />';
389 + }
390 + }
391 + }
392 +
393 + if($this->defer == true) {
394 + $deferredCssBlock .= "}if(window.addEventListener){window.addEventListener('DOMContentLoaded',deferredCSS,false);}else{window.onload = deferredCSS;}</script>";
395 + $noScriptCssBlock .= "</noscript>";
396 + if (strpos($this->content,"<title")!==false) {
397 + $this->content = str_replace('<title',$noScriptCssBlock.'<title',$this->content);
398 + } else {
399 + $warn_html_template=true;
400 + $this->content .= $noScriptCssBlock;
401 + }
402 + if (strpos($this->content,"</body>")!==false) {
403 + $this->content = str_replace('</body>',$deferredCssBlock.'</body>',$this->content);
404 + } else {
405 + $warn_html_template=true;
406 + $this->content .= $deferredCssBlock;
407 + }
408 + }
409 + }
88 410
89 - /**
90 - * Setting for to be inlined CSS.
91 - *
92 - * @var string
93 - */
94 - private $defer_inline = '';
411 + if ($warn_html_template) {
412 + $this->warn_html();
413 + }
95 414
96 - /**
97 - * Setting for allowlist of what should be aggregated.
98 - *
99 - * @var string
100 - */
101 - private $allowlist = '';
415 + //Return the modified stylesheet
416 + return $this->content;
417 + }
418 +
419 + private function fixurls($file,$code) {
420 + // $file = str_replace(ABSPATH,'/',$file); //Sth like /wp-content/file.css
421 + $file = str_replace(WP_ROOT_DIR,'/',$file);
422 + $dir = dirname($file); //Like /wp-content
102 423
103 - /**
104 - * Setting (only filter) for size under which CSS should be inlined instead of linked.
105 - *
106 - * @var string
107 - */
108 - private $cssinlinesize = '';
424 + // quick fix for import-troubles in e.g. arras theme
425 + $code=preg_replace('#@import ("|\')(.+?)\.css("|\')#','@import url("${2}.css")',$code);
109 426
110 - /**
111 - * Setting (only filter) of CSS that can be removed.
112 - *
113 - * @var array
114 - */
115 - private $cssremovables = array();
427 + if(preg_match_all('#url\((?!data)(.*)\)#Usi',$code,$matches))
428 + {
429 + $replace = array();
430 + foreach($matches[1] as $k => $url)
431 + {
432 + //Remove quotes
433 + $url = trim($url," \t\n\r\0\x0B\"'");
434 + if(substr($url,0,1)=='/' || preg_match('#^(https?://|ftp://|data:)#i',$url))
435 + {
436 + //URL is absolute
437 + continue;
438 + }else{
439 + // relative URL
440 + $newurl = AUTOPTIMIZE_WP_ROOT_URL.str_replace('//','/',$dir.'/'.$url);
441 + $hash = md5($url);
442 + $code = str_replace($matches[0][$k],$hash,$code);
443 + $replace[$hash] = 'url('.$newurl.')';
444 + }
445 + }
446 +
447 + //Do the replacing here to avoid breaking URLs
448 + $code = str_replace(array_keys($replace),array_values($replace),$code);
449 + }
450 +
451 + return $code;
452 + }
453 +
454 + private function ismovable($tag) {
455 + if (is_array($this->dontmove)) {
456 + foreach($this->dontmove as $match) {
457 + if(strpos($tag,$match)!==false) {
458 + //Matched something
459 + return false;
460 + }
461 + }
462 + }
463 +
464 + //If we're here it's safe to move
465 + return true;
466 + }
116 467
117 - /**
118 - * Setting: should inline CSS be aggregated.
119 - *
120 - * @var bool
121 - */
122 - private $include_inline = false;
123 -
124 - /**
125 - * Setting (only filter) if minified CSS can be injected after minificatoin of aggregated CSS.
126 - *
127 - * @var bool
128 - */
129 - private $inject_min_late = true;
130 -
131 - /**
132 - * Holds all exclusions.
133 - *
134 - * @var array
135 - */
136 - private $dontmove = array();
137 -
138 - /**
139 - * Holds all options.
140 - *
141 - * @var array
142 - */
143 - private $options = array();
144 -
145 - /**
146 - * Setting; should excluded CSS-files be minified.
147 - *
148 - * @var bool
149 - */
150 - private $minify_excluded = true;
151 -
152 - /**
153 - * Setting (filter only); should all media-attributes be forced to "all".
154 - *
155 - * @var bool
156 - */
157 - private $media_force_all = false;
158 -
159 - /**
160 - * Reads the page and collects style tags.
161 - *
162 - * @param array $options all options.
163 - */
164 - public function read( $options )
165 - {
166 - $noptimize_css = apply_filters( 'autoptimize_filter_css_noptimize', false, $this->content );
167 - if ( $noptimize_css ) {
168 - return false;
169 - }
170 -
171 - $allowlist_css = apply_filters( 'autoptimize_filter_css_allowlist', '', $this->content );
172 - $allowlist_css = apply_filters( 'autoptimize_filter_css_whitelist', $allowlist_css, $this->content ); // fixme: to be removed in next version.
173 - if ( ! empty( $allowlist_css ) ) {
174 - $this->allowlist = array_filter( array_map( 'trim', explode( ',', $allowlist_css ) ) );
175 - }
176 -
177 - $removable_css = apply_filters( 'autoptimize_filter_css_removables', '' );
178 - if ( ! empty( $removable_css ) ) {
179 - $this->cssremovables = array_filter( array_map( 'trim', explode( ',', $removable_css ) ) );
180 - }
181 -
182 - $this->cssinlinesize = apply_filters( 'autoptimize_filter_css_inlinesize', 256 );
183 -
184 - // filter to "late inject minified CSS", default to true for now (it is faster).
185 - $this->inject_min_late = apply_filters( 'autoptimize_filter_css_inject_min_late', true );
186 -
187 - // Remove everything that's not the header.
188 - if ( apply_filters( 'autoptimize_filter_css_justhead', $options['justhead'] ) ) {
189 - $content = explode( '</head>', $this->content, 2 );
190 - $this->content = $content[0] . '</head>';
191 - $this->restofcontent = $content[1];
192 - }
193 -
194 - // Determine whether we're doing CSS-files aggregation or not.
195 - if ( isset( $options['aggregate'] ) && ! $options['aggregate'] ) {
196 - $this->aggregate = false;
197 - }
198 - // Returning true for "dontaggregate" turns off aggregation.
199 - if ( $this->aggregate && apply_filters( 'autoptimize_filter_css_dontaggregate', false ) ) {
200 - $this->aggregate = false;
201 - }
202 -
203 - // include inline?
204 - if ( apply_filters( 'autoptimize_css_include_inline', $options['include_inline'] ) ) {
205 - $this->include_inline = true;
206 - }
207 -
208 - // List of CSS strings which are excluded from autoptimization.
209 - $exclude_css = apply_filters( 'autoptimize_filter_css_exclude', $options['css_exclude'], $this->content );
210 - if ( '' !== $exclude_css ) {
211 - $this->dontmove = array_filter( array_map( 'trim', explode( ',', $exclude_css ) ) );
212 - } else {
213 - $this->dontmove = array();
214 - }
215 -
216 - // forcefully exclude CSS with data-noptimize attrib.
217 - $this->dontmove[] = 'data-noptimize';
218 -
219 - // Should we defer css?
220 - // value: true / false.
221 - $this->defer = $options['defer'];
222 - $this->defer = apply_filters( 'autoptimize_filter_css_defer', $this->defer, $this->content );
223 -
224 - // Should we inline while deferring?
225 - // value: inlined CSS.
226 - $this->defer_inline = apply_filters( 'autoptimize_filter_css_defer_inline', $options['defer_inline'], $this->content );
227 -
228 - // Should we inline?
229 - // value: true / false.
230 - $this->inline = $options['inline'];
231 - $this->inline = apply_filters( 'autoptimize_filter_css_inline', $this->inline, $this->content );
232 -
233 - // Store cdn url.
234 - $this->cdn_url = $options['cdn_url'];
235 -
236 - // Store data: URIs setting for later use.
237 - $this->datauris = $options['datauris'];
238 -
239 - // Determine whether excluded files should be minified if not yet so.
240 - if ( ! $options['minify_excluded'] && $options['aggregate'] ) {
241 - $this->minify_excluded = false;
242 - }
243 - $this->minify_excluded = apply_filters( 'autoptimize_filter_css_minify_excluded', $this->minify_excluded, '' );
244 -
245 - // should we force all media-attributes to all?
246 - $this->media_force_all = apply_filters( 'autoptimize_filter_css_tagmedia_forceall', false );
247 -
248 - // noptimize me.
249 - $this->content = $this->hide_noptimize( $this->content );
250 -
251 - // Exclude (no)script, as those may contain CSS which should be left as is.
252 - $this->content = $this->replace_contents_with_marker_if_exists(
253 - 'SCRIPT',
254 - '<script',
255 - '#<(?:no)?script.*?<\/(?:no)?script>#is',
256 - $this->content
257 - );
258 -
259 - // Save IE hacks.
260 - $this->content = $this->hide_iehacks( $this->content );
261 -
262 - // Hide HTML comments.
263 - $this->content = $this->hide_comments( $this->content );
264 -
265 - // Get <style> and <link>.
266 - if ( preg_match_all( '#(<style[^>]*>.*</style>)|(<link[^>]*stylesheet[^>]*>)#Usmi', $this->content, $matches ) ) {
267 -
268 - foreach ( $matches[0] as $tag ) {
269 - if ( $this->isremovable( $tag, $this->cssremovables ) ) {
270 - $this->content = str_replace( $tag, '', $this->content );
271 - } elseif ( $this->ismovable( $tag ) ) {
272 - // Get the media.
273 - if ( false !== strpos( $tag, 'media=' ) ) {
274 - preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $medias );
275 - $medias = explode( ',', $medias[1] );
276 - $media = array();
277 - foreach ( $medias as $elem ) {
278 - if ( empty( $elem ) ) {
279 - $elem = 'all';
280 - }
281 -
282 - $media[] = $elem;
283 - }
284 - } else {
285 - // No media specified - applies to all.
286 - $media = array( 'all' );
287 - }
288 -
289 - // forcing media attribute to all to merge all in one file.
290 - if ( $this->media_force_all ) {
291 - $media = array( 'all' );
292 - }
293 -
294 - $media = apply_filters( 'autoptimize_filter_css_tagmedia', $media, $tag );
295 -
296 - if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) {
297 - // <link>.
298 - $url = current( explode( '?', $source[2], 2 ) );
299 - $path = $this->getpath( $url );
300 -
301 - if ( false !== $path && preg_match( '#\.css$#', $path ) ) {
302 - // Good link.
303 - $this->css[] = array( $media, $path );
304 - } else {
305 - // Link is dynamic (.php etc).
306 - $new_tag = $this->optionally_defer_excluded( $tag, 'none' );
307 - if ( '' !== $new_tag && $new_tag !== $tag ) {
308 - $this->content = str_replace( $tag, $new_tag, $this->content );
309 - }
310 - $tag = '';
311 - }
312 - } else {
313 - // Inline css in style tags can be wrapped in comment tags, so restore comments.
314 - $tag = $this->restore_comments( $tag );
315 - preg_match( '#<style.*>(.*)</style>#Usmi', $tag, $code );
316 -
317 - // And re-hide them to be able to to the removal based on tag.
318 - $tag = $this->hide_comments( $tag );
319 -
320 - if ( $this->include_inline ) {
321 - $code = preg_replace( '#^.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*$#sm', '$1', $code[1] );
322 - $this->css[] = array( $media, 'INLINE;' . $code );
323 - } else {
324 - $tag = '';
325 - }
326 - }
327 -
328 - // Remove the original style tag.
329 - $this->content = str_replace( $tag, '', $this->content );
330 - } else {
331 - if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) {
332 - $exploded_url = explode( '?', $source[2], 2 );
333 - $url = $exploded_url[0];
334 - $path = $this->getpath( $url );
335 - $new_tag = $tag;
336 -
337 - // Excluded CSS, minify that file:
338 - // -> if aggregate is on and exclude minify is on
339 - // -> if aggregate is off and the file is not in dontmove.
340 - if ( $path && $this->minify_excluded ) {
341 - $consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false );
342 - if ( ( false === $this->aggregate && str_replace( $this->dontmove, '', $path ) === $path ) || ( true === $this->aggregate && ( false === $consider_minified_array || str_replace( $consider_minified_array, '', $path ) === $path ) ) ) {
343 - $minified_url = $this->minify_single( $path );
344 - if ( ! empty( $minified_url ) ) {
345 - // Replace orig URL with cached minified URL.
346 - $new_tag = str_replace( $url, $minified_url, $tag );
347 - } elseif ( apply_filters( 'autoptimize_filter_ccsjs_remove_empty_minified_url', false ) ) {
348 - // Remove the original style tag, because cache content is empty but only if
349 - // filter is true-ed because $minified_url is also false if file is minified already.
350 - $new_tag = '';
351 - }
352 - }
353 - }
354 -
355 - if ( '' !== $new_tag ) {
356 - // Optionally defer (preload) non-aggregated CSS.
357 - $new_tag = $this->optionally_defer_excluded( $new_tag, $url );
358 - }
359 -
360 - // And replace!
361 - if ( ( '' !== $new_tag && $new_tag !== $tag ) || ( '' === $new_tag && apply_filters( 'autoptimize_filter_css_remove_empty_files', false ) ) ) {
362 - $this->content = str_replace( $tag, $new_tag, $this->content );
363 - }
364 - }
365 - }
366 - }
367 - return true;
368 - }
369 -
370 - // Really, no styles?
371 - return false;
372 - }
373 -
374 - /**
375 - * Checks if non-optimized CSS is to be preloaded and if so return
376 - * the tag with preload code.
377 - *
378 - * @param string $tag (required).
379 - * @param string $url (optional).
380 - *
381 - * @return string $new_tag
382 - */
383 - private function optionally_defer_excluded( $tag, $url = '' )
384 - {
385 - // Defer single CSS if "inline & defer" is ON and there is inline CSS.
386 - if ( ! empty( $tag ) && false === strpos( $tag, ' onload=' ) && $this->defer && ! empty( $this->defer_inline ) && apply_filters( 'autoptimize_filter_css_defer_excluded', true, $tag ) ) {
387 - // get media attribute and based on that create onload JS attribute value.
388 - if ( false === strpos( $tag, 'media=' ) ) {
389 - $tag = str_replace( '<link', "<link media='all'", $tag );
390 - }
391 -
392 - preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $_medias );
393 - $_media = $_medias[1];
394 - $_preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $_media );
395 -
396 - if ( 'print' !== $_media ) {
397 - // If not media=print, adapt original <link> element for CSS to be preloaded and add <noscript>-version for fallback.
398 - $new_tag = '<noscript>' . autoptimizeUtils::remove_id_from_node( $tag ) . '</noscript>' . str_replace(
399 - $_medias[0],
400 - "media='print' onload=\"" . $_preload_onload . '"',
401 - $tag
402 - );
403 -
404 - // Optionally (but default false) preload the (excluded) CSS-file.
405 - if ( apply_filters( 'autoptimize_fitler_css_preload_and_print', false ) && 'none' !== $url ) {
406 - $new_tag = '<link rel="preload" as="stylesheet" href="' . $url . '"/>' . $new_tag;
407 - }
408 - } else {
409 - $new_tag = $tag;
410 - }
411 -
412 - return $new_tag;
413 - }
414 -
415 - // Return unchanged $tag.
416 - return $tag;
417 - }
418 -
419 - /**
420 - * Checks if the local file referenced by $path is a valid
421 - * candidate for being inlined into a data: URI
422 - *
423 - * @param string $path image path.
424 - * @return boolean
425 - */
426 - private function is_datauri_candidate( $path )
427 - {
428 - // Call only once since it's called from a loop.
429 - static $max_size = null;
430 - if ( null === $max_size ) {
431 - $max_size = $this->get_datauri_maxsize();
432 - }
433 -
434 - if ( $path && preg_match( '#\.(jpe?g|png|gif|webp|bmp)$#i', $path ) &&
435 - file_exists( $path ) && is_readable( $path ) && filesize( $path ) <= $max_size ) {
436 -
437 - // Seems we have a candidate.
438 - $is_candidate = true;
439 - } else {
440 - // Filter allows overriding default decision (which checks for local file existence).
441 - $is_candidate = apply_filters( 'autoptimize_filter_css_is_datauri_candidate', false, $path );
442 - }
443 -
444 - return $is_candidate;
445 - }
446 -
447 - /**
448 - * Returns the amount of bytes that shouldn't be exceeded if a file is to
449 - * be inlined into a data: URI. Defaults to 4096, passed through
450 - * `autoptimize_filter_css_datauri_maxsize` filter.
451 - *
452 - * @return mixed
453 - */
454 - private function get_datauri_maxsize()
455 - {
456 - static $max_size = null;
457 -
458 - /**
459 - * No need to apply the filter multiple times in case the
460 - * method itself is invoked multiple times during a single request.
461 - * This prevents some wild stuff like having different maxsizes
462 - * for different files/site-sections etc. But if you're into that sort
463 - * of thing you're probably better of building assets completely
464 - * outside of WordPress anyway.
465 - */
466 - if ( null === $max_size ) {
467 - $max_size = (int) apply_filters( 'autoptimize_filter_css_datauri_maxsize', 4096 );
468 - }
469 -
470 - return $max_size;
471 - }
472 -
473 - private function check_datauri_exclude_list( $url )
474 - {
475 - static $exclude_list = null;
476 - $no_datauris = array();
477 -
478 - // Again, skip doing certain stuff repeatedly when loop-called.
479 - if ( null === $exclude_list ) {
480 - $exclude_list = apply_filters( 'autoptimize_filter_css_datauri_exclude', '' );
481 - $no_datauris = array_filter( array_map( 'trim', explode( ',', $exclude_list ) ) );
482 - }
483 -
484 - $matched = false;
485 -
486 - if ( ! empty( $exclude_list ) ) {
487 - foreach ( $no_datauris as $no_datauri ) {
488 - if ( false !== strpos( $url, $no_datauri ) ) {
489 - $matched = true;
490 - break;
491 - }
492 - }
493 - }
494 -
495 - return $matched;
496 - }
497 -
498 - private function build_or_get_datauri_image( $path )
499 - {
500 - /**
501 - * TODO/FIXME: document the required return array format, or better yet,
502 - * use a string, since we don't really need an array for this. That would, however,
503 - * require changing even more code, which is not happening right now...
504 - */
505 -
506 - // Allows short-circuiting datauri generation for an image.
507 - $result = apply_filters( 'autoptimize_filter_css_datauri_image', array(), $path );
508 - if ( ! empty( $result ) ) {
509 - if ( is_array( $result ) && isset( $result['full'] ) && isset( $result['base64data'] ) ) {
510 - return $result;
511 - }
512 - }
513 -
514 - $hash = md5( $path );
515 - $check = new autoptimizeCache( $hash, 'img' );
516 - if ( $check->check() ) {
517 - // we have the base64 image in cache.
518 - $head_and_data = $check->retrieve();
519 - $_base64data = explode( ';base64,', $head_and_data );
520 - $base64data = $_base64data[1];
521 - unset( $_base64data );
522 - } else {
523 - // It's an image and we don't have it in cache, get the type by extension.
524 - $exploded_path = explode( '.', $path );
525 - $type = end( $exploded_path );
526 -
527 - switch ( $type ) {
528 - case 'jpg':
529 - case 'jpeg':
530 - $dataurihead = 'data:image/jpeg;base64,';
531 - break;
532 - case 'gif':
533 - $dataurihead = 'data:image/gif;base64,';
534 - break;
535 - case 'png':
536 - $dataurihead = 'data:image/png;base64,';
537 - break;
538 - case 'bmp':
539 - $dataurihead = 'data:image/bmp;base64,';
540 - break;
541 - case 'webp':
542 - $dataurihead = 'data:image/webp;base64,';
543 - break;
544 - default:
545 - $dataurihead = 'data:application/octet-stream;base64,';
546 - }
547 -
548 - // Encode the data.
549 - $base64data = base64_encode( file_get_contents( $path ) );
550 - $head_and_data = $dataurihead . $base64data;
551 -
552 - // Save in cache.
553 - $check->cache( $head_and_data, 'text/plain' );
554 - }
555 - unset( $check );
556 -
557 - return array(
558 - 'full' => $head_and_data,
559 - 'base64data' => $base64data,
560 - );
561 - }
562 -
563 - /**
564 - * Given an array of key/value pairs to replace in $string,
565 - * it does so by replacing the longest-matching strings first.
566 - *
567 - * @param string $string string in which to replace.
568 - * @param array $replacements to be replaced strings and replacement.
569 - *
570 - * @return string
571 - */
572 - protected static function replace_longest_matches_first( $string, $replacements = array() )
573 - {
574 - if ( ! empty( $replacements ) ) {
575 - // Sort the replacements array by key length in desc order (so that the longest strings are replaced first).
576 - $keys = array_map( 'strlen', array_keys( $replacements ) );
577 - array_multisort( $keys, SORT_DESC, $replacements );
578 - $string = str_replace( array_keys( $replacements ), array_values( $replacements ), $string );
579 - }
580 -
581 - return $string;
582 - }
583 -
584 - /**
585 - * Rewrites/Replaces any ASSETS_REGEX-matching urls in a string.
586 - * Removes quotes/cruft around each one and passes it through to
587 - * `autoptimizeBase::url_replace_cdn()`.
588 - * Replacements are performed in a `longest-match-replaced-first` way.
589 - *
590 - * @param string $code CSS code.
591 - *
592 - * @return string
593 - */
594 - public function replace_urls( $code = '' )
595 - {
596 - $replacements = array();
597 -
598 - preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
599 - if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
600 - foreach ( $url_src_matches[1] as $count => $original_url ) {
601 - // Removes quotes and other cruft.
602 - $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
603 -
604 - /**
605 - * TODO/FIXME: Add a way for other code / callable to be called here
606 - * and provide it's own results for the $replacements array
607 - * for the "current" key.
608 - * If such a result is returned/provided, we sholud then avoid
609 - * calling url_replace_cdn() here for the current iteration.
610 - *
611 - * This would maybe allow the inlining logic currently present
612 - * in `autoptimizeStyles::rewrite_assets()` to be "pulled out"
613 - * and given as a callable to this method or something... and
614 - * then we could "just" call `replace_urls()` from within
615 - * `autoptimizeStyles::rewrite_assets()` and avoid some
616 - * (currently present) code/logic duplication.
617 - */
618 -
619 - // Do CDN replacement if needed.
620 - if ( ! empty( $this->cdn_url ) ) {
621 - $replacement_url = $this->url_replace_cdn( $url );
622 - // Prepare replacements array.
623 - $replacements[ $url_src_matches[1][ $count ] ] = str_replace(
624 - $original_url, $replacement_url, $url_src_matches[1][ $count ]
625 - );
626 - }
627 - }
628 - }
629 -
630 - $code = self::replace_longest_matches_first( $code, $replacements );
631 -
632 - return $code;
633 - }
634 -
635 - /**
636 - * "Hides" @font-face declarations by replacing them with `%%FONTFACE%%` markers.
637 - * Also does CDN replacement of any font-urls within those declarations if the `autoptimize_filter_css_fonts_cdn`
638 - * filter is used.
639 - *
640 - * @param string $code HTML being processed to hide fonts.
641 - * @return string
642 - */
643 - public function hide_fontface_and_maybe_cdn( $code )
644 - {
645 - // Proceed only if @font-face declarations exist within $code.
646 - preg_match_all( self::FONT_FACE_REGEX, $code, $fontfaces );
647 - if ( isset( $fontfaces[0] ) ) {
648 - // Check if we need to cdn fonts or not.
649 - $do_font_cdn = apply_filters( 'autoptimize_filter_css_fonts_cdn', false );
650 -
651 - foreach ( $fontfaces[0] as $full_match ) {
652 - // Keep original match so we can search/replace it.
653 - $match_search = $full_match;
654 -
655 - // Do font cdn if needed.
656 - if ( $do_font_cdn ) {
657 - $full_match = $this->replace_urls( $full_match );
658 - }
659 -
660 - // Replace declaration with its base64 encoded string.
661 - $replacement = self::build_marker( 'FONTFACE', $full_match );
662 - $code = str_replace( $match_search, $replacement, $code );
663 - }
664 - }
665 -
666 - return $code;
667 - }
668 -
669 - /**
670 - * Restores original @font-face declarations that have been "hidden"
671 - * using `hide_fontface_and_maybe_cdn()`.
672 - *
673 - * @param string $code HTML being processed to unhide fonts.
674 - * @return string
675 - */
676 - public function restore_fontface( $code )
677 - {
678 - return $this->restore_marked_content( 'FONTFACE', $code );
679 - }
680 -
681 - /**
682 - * Re-write (and/or inline) referenced assets.
683 - *
684 - * @param string $code HTML being processed rewrite assets.
685 - * @return string
686 - */
687 - public function rewrite_assets( $code )
688 - {
689 - // Handle @font-face rules by hiding and processing them separately.
690 - $code = $this->hide_fontface_and_maybe_cdn( $code );
691 -
692 - /**
693 - * TODO/FIXME:
694 - * Certain code parts below are kind-of repeated now in `replace_urls()`, which is not ideal.
695 - * There is maybe a way to separate/refactor things and then be able to keep
696 - * the ASSETS_REGEX rewriting/handling logic in a single place (along with removing quotes/cruft from matched urls).
697 - * See comments in `replace_urls()` regarding this. The idea is to extract the inlining
698 - * logic out (which is the only real difference between replace_urls() and the code below), but still
699 - * achieve identical results as before.
700 - */
701 -
702 - // Re-write (and/or inline) URLs to point them to the CDN host.
703 - $url_src_matches = array();
704 - $imgreplace = array();
705 -
706 - // Matches and captures anything specified within the literal `url()` and excludes those containing data: URIs.
707 - preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
708 - if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
709 - foreach ( $url_src_matches[1] as $count => $original_url ) {
710 - // Removes quotes and other cruft.
711 - $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
712 -
713 - // If datauri inlining is turned on, do it.
714 - $inlined = false;
715 - if ( $this->datauris ) {
716 - $iurl = $url;
717 - if ( false !== strpos( $iurl, '?' ) ) {
718 - $iurl = strtok( $iurl, '?' );
719 - }
720 -
721 - $ipath = $this->getpath( $iurl );
722 -
723 - $excluded = $this->check_datauri_exclude_list( $ipath );
724 - if ( ! $excluded ) {
725 - $is_datauri_candidate = $this->is_datauri_candidate( $ipath );
726 - if ( $is_datauri_candidate ) {
727 - $datauri = $this->build_or_get_datauri_image( $ipath );
728 - $base64data = $datauri['base64data'];
729 - // Add it to the list for replacement.
730 - $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
731 - $original_url,
732 - $datauri['full'],
733 - $url_src_matches[1][ $count ]
734 - );
735 - $inlined = true;
736 - }
737 - }
738 - }
739 -
740 - /**
741 - * Doing CDN URL replacement for every found match (if CDN is
742 - * specified). This way we make sure to do it even if
743 - * inlining isn't turned on, or if a resource is skipped from
744 - * being inlined for whatever reason above.
745 - */
746 - if ( ! $inlined && ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) ) {
747 - // Just do the "simple" CDN replacement.
748 - $replacement_url = $this->url_replace_cdn( $url );
749 - $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
750 - $original_url, $replacement_url, $url_src_matches[1][ $count ]
751 - );
752 - }
753 - }
754 - }
755 -
756 - $code = self::replace_longest_matches_first( $code, $imgreplace );
757 -
758 - // Replace back font-face markers with actual font-face declarations.
759 - $code = $this->restore_fontface( $code );
760 -
761 - return $code;
762 - }
763 -
764 - /**
765 - * Joins and optimizes CSS.
766 - */
767 - public function minify()
768 - {
769 - foreach ( $this->css as $group ) {
770 - list( $media, $css ) = $group;
771 - if ( preg_match( '#^INLINE;#', $css ) ) {
772 - // <style>.
773 - $css = preg_replace( '#^INLINE;#', '', $css );
774 - $css = self::fixurls( ABSPATH . 'index.php', $css ); // ABSPATH already contains a trailing slash.
775 - $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, '' );
776 - if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
777 - $css = $tmpstyle;
778 - $this->alreadyminified = true;
779 - }
780 - } else {
781 - // <link>
782 - if ( false !== $css && file_exists( $css ) && is_readable( $css ) ) {
783 - $css_path = $css;
784 - $css = self::fixurls( $css_path, file_get_contents( $css_path ) );
785 - $css = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $css );
786 - $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, $css_path );
787 - if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
788 - $css = $tmpstyle;
789 - $this->alreadyminified = true;
790 - } elseif ( $this->can_inject_late( $css_path, $css ) ) {
791 - $css = self::build_injectlater_marker( $css_path, md5( $css ) );
792 - }
793 - } else {
794 - // Couldn't read CSS. Maybe getpath isn't working?
795 - $css = '';
796 - }
797 - }
798 -
799 - foreach ( $media as $elem ) {
800 - if ( ! empty( $css ) ) {
801 - if ( ! isset( $this->csscode[ $elem ] ) ) {
802 - $this->csscode[ $elem ] = '';
803 - }
804 - $this->csscode[ $elem ] .= "\n/*FILESTART*/" . $css;
805 - }
806 - }
807 - }
808 -
809 - // Check for duplicate code.
810 - $md5list = array();
811 - $tmpcss = $this->csscode;
812 - foreach ( $tmpcss as $media => $code ) {
813 - $md5sum = md5( $code );
814 - $medianame = $media;
815 - foreach ( $md5list as $med => $sum ) {
816 - // If same code.
817 - if ( $sum === $md5sum ) {
818 - // Add the merged code.
819 - $medianame = $med . ', ' . $media;
820 - $this->csscode[ $medianame ] = $code;
821 - $md5list[ $medianame ] = $md5list[ $med ];
822 - unset( $this->csscode[ $med ], $this->csscode[ $media ], $md5list[ $med ] );
823 - }
824 - }
825 - $md5list[ $medianame ] = $md5sum;
826 - }
827 - unset( $tmpcss );
828 -
829 - // Manage @imports, while is for recursive import management.
830 - foreach ( $this->csscode as &$thiscss ) {
831 - // Flag to trigger import reconstitution and var to hold external imports.
832 - $fiximports = false;
833 - $external_imports = '';
834 -
835 - // remove comments to avoid importing commented-out imports.
836 - $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
837 - while ( preg_match_all( '#@import +(?:url)?(?:(?:\((["\']?)(?:[^"\')]+)\1\)|(["\'])(?:[^"\']+)\2)(?:[^,;"\']+(?:,[^,;"\']+)*)?)(?:;)#mi', $thiscss_nocomments, $matches ) ) {
838 - foreach ( $matches[0] as $import ) {
839 - if ( $this->isremovable( $import, $this->cssremovables ) ) {
840 - $thiscss = str_replace( $import, '', $thiscss );
841 - $import_ok = true;
842 - } else {
843 - $url = trim( preg_replace( '#^.*((?:https?:|ftp:)?//.*\.css).*$#', '$1', trim( $import ) ), " \t\n\r\0\x0B\"'" );
844 - $path = $this->getpath( $url );
845 - $import_ok = false;
846 - if ( file_exists( $path ) && is_readable( $path ) ) {
847 - $code = addcslashes( self::fixurls( $path, file_get_contents( $path ) ), '\\' );
848 - $code = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $code );
849 - $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $code, '' );
850 - if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
851 - $code = $tmpstyle;
852 - $this->alreadyminified = true;
853 - } elseif ( $this->can_inject_late( $path, $code ) ) {
854 - $code = self::build_injectlater_marker( $path, md5( $code ) );
855 - }
856 -
857 - if ( ! empty( $code ) ) {
858 - $tmp_thiscss = preg_replace( '#(/\*FILESTART\*/.*)' . preg_quote( $import, '#' ) . '#Us', '/*FILESTART2*/' . $code . '$1', $thiscss );
859 - if ( ! empty( $tmp_thiscss ) ) {
860 - $thiscss = $tmp_thiscss;
861 - $import_ok = true;
862 - unset( $tmp_thiscss );
863 - }
864 - }
865 - unset( $code );
866 - }
867 - }
868 - if ( ! $import_ok ) {
869 - // External imports and general fall-back.
870 - $external_imports .= $import;
871 -
872 - $thiscss = str_replace( $import, '', $thiscss );
873 - $fiximports = true;
874 - }
875 - }
876 - $thiscss = preg_replace( '#/\*FILESTART\*/#', '', $thiscss );
877 - $thiscss = preg_replace( '#/\*FILESTART2\*/#', '/*FILESTART*/', $thiscss );
878 -
879 - // and update $thiscss_nocomments before going into next iteration in while loop.
880 - $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
881 - }
882 - unset( $thiscss_nocomments );
883 -
884 - // Add external imports to top of aggregated CSS.
885 - if ( $fiximports ) {
886 - $thiscss = $external_imports . $thiscss;
887 - }
888 - }
889 - unset( $thiscss );
890 -
891 - // $this->csscode has all the uncompressed code now.
892 - foreach ( $this->csscode as &$code ) {
893 - // Check for already-minified code.
894 - $hash = md5( $code );
895 - do_action( 'autoptimize_action_css_hash', $hash );
896 - $ccheck = new autoptimizeCache( $hash, 'css' );
897 - if ( $ccheck->check() ) {
898 - $code = $ccheck->retrieve();
899 - $this->hashmap[ md5( $code ) ] = $hash;
900 - continue;
901 - }
902 - unset( $ccheck );
903 -
904 - // Rewrite and/or inline referenced assets.
905 - $code = $this->rewrite_assets( $code );
906 -
907 - // Minify.
908 - $code = $this->run_minifier_on( $code );
909 -
910 - // Bring back INJECTLATER stuff.
911 - $code = $this->inject_minified( $code );
912 -
913 - // Filter results.
914 - $tmp_code = apply_filters( 'autoptimize_css_after_minify', $code );
915 - if ( ! empty( $tmp_code ) ) {
916 - $code = $tmp_code;
917 - unset( $tmp_code );
918 - }
919 -
920 - $this->hashmap[ md5( $code ) ] = $hash;
921 - }
922 -
923 - unset( $code );
924 - return true;
925 - }
926 -
927 - public function run_minifier_on( $code )
928 - {
929 - if ( ! $this->alreadyminified ) {
930 - $do_minify = apply_filters( 'autoptimize_css_do_minify', true );
931 -
932 - if ( $do_minify ) {
933 - $cssmin = new autoptimizeCSSmin();
934 - $tmp_code = trim( $cssmin->run( $code ) );
935 -
936 - if ( ! empty( $tmp_code ) ) {
937 - $code = $tmp_code;
938 - unset( $tmp_code );
939 - }
940 - }
941 - }
942 -
943 - return $code;
944 - }
945 -
946 - /**
947 - * Caches the CSS in uncompressed, deflated and gzipped form.
948 - */
949 - public function cache()
950 - {
951 - // CSS cache.
952 - foreach ( $this->csscode as $media => $code ) {
953 - if ( empty( $code ) ) {
954 - continue;
955 - }
956 -
957 - $md5 = $this->hashmap[ md5( $code ) ];
958 - $cache = new autoptimizeCache( $md5, 'css' );
959 - if ( ! $cache->check() ) {
960 - // Cache our code.
961 - $cache->cache( $code, 'text/css' );
962 - }
963 - $this->url[ $media ] = AUTOPTIMIZE_CACHE_URL . $cache->getname();
964 - }
965 - }
966 -
967 - /**
968 - * Returns the content.
969 - */
970 - public function getcontent()
971 - {
972 - // Restore the full content (only applies when "autoptimize_filter_css_justhead" filter is true).
973 - if ( ! empty( $this->restofcontent ) ) {
974 - $this->content .= $this->restofcontent;
975 - $this->restofcontent = '';
976 - }
977 -
978 - // type is not added by default.
979 - $type_css = '';
980 - if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
981 - $type_css = 'type="text/css" ';
982 - }
983 -
984 - // Inject the new stylesheets.
985 - $replace_tag = array( '<title', 'before' );
986 - $replace_tag = apply_filters( 'autoptimize_filter_css_replacetag', $replace_tag, $this->content );
987 -
988 - if ( $this->inline ) {
989 - foreach ( $this->csscode as $media => $code ) {
990 - $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $code . '</style>' ), $replace_tag );
991 - }
992 - } else {
993 - if ( $this->defer ) {
994 - $preload_css_block = '';
995 - $inlined_ccss_block = '';
996 - $noscript_css_block = '<noscript id="aonoscrcss">';
997 -
998 - $defer_inline_code = $this->defer_inline;
999 - if ( ! empty( $defer_inline_code ) ) {
1000 - if ( apply_filters( 'autoptimize_filter_css_critcss_minify', true ) ) {
1001 - $icss_hash = md5( $defer_inline_code );
1002 - $icss_cache = new autoptimizeCache( $icss_hash, 'css' );
1003 - if ( $icss_cache->check() ) {
1004 - // we have the optimized inline CSS in cache.
1005 - $defer_inline_code = $icss_cache->retrieve();
1006 - } else {
1007 - $cssmin = new autoptimizeCSSmin();
1008 - $tmp_code = trim( $cssmin->run( $defer_inline_code ) );
1009 -
1010 - if ( ! empty( $tmp_code ) ) {
1011 - $defer_inline_code = $tmp_code;
1012 - $icss_cache->cache( $defer_inline_code, 'text/css' );
1013 - unset( $tmp_code );
1014 - }
1015 - }
1016 - }
1017 - // inlined critical css set here, but injected when full CSS is injected
1018 - // to avoid CSS containing SVG with <title tag receiving the full CSS link.
1019 - $inlined_ccss_block = '<style ' . $type_css . 'id="aoatfcss" media="all">' . $defer_inline_code . '</style>';
1020 - }
1021 - }
1022 -
1023 - foreach ( $this->url as $media => $url ) {
1024 - $url = $this->url_replace_cdn( $url );
1025 -
1026 - // Add the stylesheet either deferred (import at bottom) or normal links in head.
1027 - if ( $this->defer && 'print' !== $media ) {
1028 - $preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $media );
1029 -
1030 - $preload_css_block .= '<link rel="stylesheet" media="print" href="' . $url . '" onload="' . $preload_onload . '" />';
1031 - if ( apply_filters( 'autoptimize_fitler_css_preload_and_print', false ) ) {
1032 - $preload_css_block = '<link rel="preload" as="stylesheet" href="' . $url . '"/>' . $preload_css_block;
1033 - }
1034 - $noscript_css_block .= '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />';
1035 - } else {
1036 - if ( strlen( $this->csscode[ $media ] ) > $this->cssinlinesize ) {
1037 - $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />' ), $replace_tag );
1038 - } elseif ( strlen( $this->csscode[ $media ] ) > 0 ) {
1039 - $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $this->csscode[ $media ] . '</style>' ), $replace_tag );
1040 - }
1041 - }
1042 - }
1043 -
1044 - if ( $this->defer ) {
1045 - $noscript_css_block .= '</noscript>';
1046 - // Inject inline critical CSS, the preloaded full CSS and the noscript-CSS.
1047 - $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', $inlined_ccss_block . $preload_css_block . $noscript_css_block ), $replace_tag );
1048 - }
1049 - }
1050 -
1051 - // restore comments.
1052 - $this->content = $this->restore_comments( $this->content );
1053 -
1054 - // restore IE hacks.
1055 - $this->content = $this->restore_iehacks( $this->content );
1056 -
1057 - // restore (no)script.
1058 - $this->content = $this->restore_marked_content( 'SCRIPT', $this->content );
1059 -
1060 - // Restore noptimize.
1061 - $this->content = $this->restore_noptimize( $this->content );
1062 -
1063 - // Return the modified stylesheet.
1064 - return $this->content;
1065 - }
1066 -
1067 - /**
1068 - * Make sure URL's are absolute iso relative to original CSS location.
1069 - *
1070 - * @param string $file filename of optimized CSS-file.
1071 - * @param string $code CSS-code in which to fix URL's.
1072 - */
1073 - static function fixurls( $file, $code )
1074 - {
1075 - // Switch all imports to the url() syntax.
1076 - $code = preg_replace( '#@import ("|\')(.+?)\.css.*?("|\')#', '@import url("${2}.css")', $code );
1077 -
1078 - if ( preg_match_all( self::ASSETS_REGEX, $code, $matches ) ) {
1079 - $file = str_replace( WP_ROOT_DIR, '/', $file );
1080 - /**
1081 - * Rollback as per https://github.com/futtta/autoptimize/issues/94
1082 - * $file = str_replace( AUTOPTIMIZE_WP_CONTENT_NAME, '', $file );
1083 - */
1084 - $dir = dirname( $file ); // Like /themes/expound/css.
1085 -
1086 - /**
1087 - * $dir should not contain backslashes, since it's used to replace
1088 - * urls, but it can contain them when running on Windows because
1089 - * fixurls() is sometimes called with `ABSPATH . 'index.php'`
1090 - */
1091 - $dir = str_replace( '\\', '/', $dir );
1092 - unset( $file ); // not used below at all.
1093 -
1094 - $replace = array();
1095 - foreach ( $matches[1] as $k => $url ) {
1096 - // Remove quotes.
1097 - $url = trim( $url, " \t\n\r\0\x0B\"'" );
1098 - $no_q_url = trim( $url, "\"'" );
1099 - if ( $url !== $no_q_url ) {
1100 - $removed_quotes = true;
1101 - } else {
1102 - $removed_quotes = false;
1103 - }
1104 -
1105 - if ( '' === $no_q_url ) {
1106 - continue;
1107 - }
1108 -
1109 - $url = $no_q_url;
1110 - if ( '/' === $url[0] || preg_match( '#^(https?://|ftp://|data:)#i', $url ) ) {
1111 - // URL is protocol-relative, host-relative or something we don't touch.
1112 - continue;
1113 - } else { // Relative URL.
1114 -
1115 - /*
1116 - * rollback as per https://github.com/futtta/autoptimize/issues/94
1117 - * $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_CONTENT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1118 - */
1119 - $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_ROOT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1120 - $newurl = apply_filters( 'autoptimize_filter_css_fixurl_newurl', $newurl );
1121 -
1122 - /**
1123 - * Hash the url + whatever was behind potentially for replacement
1124 - * We must do this, or different css classes referencing the same bg image (but
1125 - * different parts of it, say, in sprites and such) loose their stuff...
1126 - */
1127 - $hash = md5( $url . $matches[2][ $k ] );
1128 - $code = str_replace( $matches[0][ $k ], $hash, $code );
1129 -
1130 - if ( $removed_quotes ) {
1131 - $replace[ $hash ] = "url('" . $newurl . "')" . $matches[2][ $k ];
1132 - } else {
1133 - $replace[ $hash ] = 'url(' . $newurl . ')' . $matches[2][ $k ];
1134 - }
1135 - }
1136 - }
1137 -
1138 - $code = self::replace_longest_matches_first( $code, $replace );
1139 - }
1140 -
1141 - return $code;
1142 - }
1143 -
1144 - private function ismovable( $tag )
1145 - {
1146 - if ( ! $this->aggregate ) {
1147 - return false;
1148 - }
1149 -
1150 - if ( ! empty( $this->allowlist ) ) {
1151 - foreach ( $this->allowlist as $match ) {
1152 - if ( false !== strpos( $tag, $match ) ) {
1153 - return true;
1154 - }
1155 - }
1156 - // no match with allowlist.
1157 - return false;
1158 - } else {
1159 - if ( is_array( $this->dontmove ) && ! empty( $this->dontmove ) ) {
1160 - foreach ( $this->dontmove as $match ) {
1161 - if ( false !== strpos( $tag, $match ) ) {
1162 - // Matched something.
1163 - return false;
1164 - }
1165 - }
1166 - }
1167 -
1168 - // If we're here it's safe to move.
1169 - return true;
1170 - }
1171 - }
1172 -
1173 - private function can_inject_late( $css_path, $css )
1174 - {
1175 - $consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false, $css_path );
1176 - if ( true !== $this->inject_min_late ) {
1177 - // late-inject turned off.
1178 - return false;
1179 - } elseif ( ( false === strpos( $css_path, 'min.css' ) ) && ( str_replace( $consider_minified_array, '', $css_path ) === $css_path ) ) {
1180 - // file not minified based on filename & filter.
1181 - return false;
1182 - } elseif ( false !== strpos( $css, '@import' ) ) {
1183 - // can't late-inject files with imports as those need to be aggregated.
1184 - return false;
1185 - } elseif ( ( false !== strpos( $css, '@font-face' ) ) && ( apply_filters( 'autoptimize_filter_css_fonts_cdn', false ) === true ) && ( ! empty( $this->cdn_url ) ) ) {
1186 - // don't late-inject CSS with font-src's if fonts are set to be CDN'ed.
1187 - return false;
1188 - } elseif ( ( ( true == $this->datauris ) || ( ! empty( $this->cdn_url ) ) ) && preg_match( '#background[^;}]*url\(#Ui', $css ) ) {
1189 - // don't late-inject CSS with images if CDN is set OR if image inlining is on.
1190 - return false;
1191 - } else {
1192 - // phew, all is safe, we can late-inject.
1193 - return true;
1194 - }
1195 - }
1196 -
1197 - /**
1198 - * Minifies (and cdn-replaces) a single local css file
1199 - * and returns its (cached) url.
1200 - *
1201 - * @param string $filepath Filepath.
1202 - * @param bool $cache_miss Optional. Force a cache miss. Default false.
1203 - *
1204 - * @return bool|string Url pointing to the minified css file or false.
1205 - */
1206 - public function minify_single( $filepath, $cache_miss = false )
1207 - {
1208 - $contents = $this->prepare_minify_single( $filepath );
1209 -
1210 - if ( empty( $contents ) ) {
1211 - return false;
1212 - }
1213 -
1214 - // Check cache.
1215 - $hash = 'single_' . md5( $contents );
1216 - $cache = new autoptimizeCache( $hash, 'css' );
1217 - do_action( 'autoptimize_action_css_hash', $hash );
1218 -
1219 - // If not in cache already, minify...
1220 - if ( ! $cache->check() || $cache_miss ) {
1221 - // Fixurls...
1222 - $contents = self::fixurls( $filepath, $contents );
1223 - // CDN-replace any referenced assets if needed...
1224 - $contents = $this->hide_fontface_and_maybe_cdn( $contents );
1225 - $contents = $this->replace_urls( $contents );
1226 - $contents = $this->restore_fontface( $contents );
1227 - // Now minify...
1228 - $cssmin = new autoptimizeCSSmin();
1229 - $contents = trim( $cssmin->run( $contents ) );
1230 -
1231 - // Check if minified cache content is empty.
1232 - if ( empty( $contents ) ) {
1233 - return false;
1234 - }
1235 -
1236 - // Filter contents of excluded minified CSS.
1237 - $contents = apply_filters( 'autoptimize_filter_css_single_after_minify', $contents );
1238 -
1239 - // Store in cache.
1240 - $cache->cache( $contents, 'text/css' );
1241 - }
1242 -
1243 - $url = $this->build_minify_single_url( $cache );
1244 -
1245 - return $url;
1246 - }
1247 -
1248 - /**
1249 - * Returns whether we're doing aggregation or not.
1250 - *
1251 - * @return bool
1252 - */
1253 - public function aggregating()
1254 - {
1255 - return $this->aggregate;
1256 - }
1257 -
1258 - public function getOptions()
1259 - {
1260 - return $this->options;
1261 - }
1262 -
1263 - public function replaceOptions( $options )
1264 - {
1265 - $this->options = $options;
1266 - }
1267 -
1268 - public function setOption( $name, $value )
1269 - {
1270 - $this->options[ $name ] = $value;
1271 - $this->$name = $value;
1272 - }
1273 -
1274 - public function getOption( $name )
1275 - {
1276 - return $this->options[ $name ];
1277 - }
1278 468 }