PluginProbe
Autoptimize / 2.7.7
Autoptimize v2.7.7
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
autoptimize / classes / autoptimizeStyles.php

autoptimizeStyles.php in Autoptimize 2.7.7, at classes/autoptimizeStyles.php

1,279 lines 49.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 ) {
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 }
1279