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