PluginProbe
Autoptimize / 2.7.4
Autoptimize v2.7.4
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.4, at classes/autoptimizeStyles.php

1,275 lines 49.5 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 preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $_medias );
390 $_media = $_medias[1];
391 } else {
392 $_media = 'all';
393 }
394 $_preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $_media );
395
396 // Adapt original <link> element for CSS to be preloaded and add <noscript>-version for fallback.
397 $new_tag = '<noscript>' . autoptimizeUtils::remove_id_from_node( $tag ) . '</noscript>' . str_replace(
398 $_medias[0],
399 "media='print' onload=\"" . $_preload_onload . '"',
400 $tag
401 );
402
403 // Optionally (but default false) preload the (excluded) CSS-file.
404 if ( apply_filters( 'autoptimize_fitler_css_preload_and_print', false ) && 'none' !== $url ) {
405 $new_tag = '<link rel="preload" as="stylesheet" href="' . $url . '"/>' . $new_tag;
406 }
407
408 return $new_tag;
409 }
410
411 // Return unchanged $tag.
412 return $tag;
413 }
414
415 /**
416 * Checks if the local file referenced by $path is a valid
417 * candidate for being inlined into a data: URI
418 *
419 * @param string $path image path.
420 * @return boolean
421 */
422 private function is_datauri_candidate( $path )
423 {
424 // Call only once since it's called from a loop.
425 static $max_size = null;
426 if ( null === $max_size ) {
427 $max_size = $this->get_datauri_maxsize();
428 }
429
430 if ( $path && preg_match( '#\.(jpe?g|png|gif|webp|bmp)$#i', $path ) &&
431 file_exists( $path ) && is_readable( $path ) && filesize( $path ) <= $max_size ) {
432
433 // Seems we have a candidate.
434 $is_candidate = true;
435 } else {
436 // Filter allows overriding default decision (which checks for local file existence).
437 $is_candidate = apply_filters( 'autoptimize_filter_css_is_datauri_candidate', false, $path );
438 }
439
440 return $is_candidate;
441 }
442
443 /**
444 * Returns the amount of bytes that shouldn't be exceeded if a file is to
445 * be inlined into a data: URI. Defaults to 4096, passed through
446 * `autoptimize_filter_css_datauri_maxsize` filter.
447 *
448 * @return mixed
449 */
450 private function get_datauri_maxsize()
451 {
452 static $max_size = null;
453
454 /**
455 * No need to apply the filter multiple times in case the
456 * method itself is invoked multiple times during a single request.
457 * This prevents some wild stuff like having different maxsizes
458 * for different files/site-sections etc. But if you're into that sort
459 * of thing you're probably better of building assets completely
460 * outside of WordPress anyway.
461 */
462 if ( null === $max_size ) {
463 $max_size = (int) apply_filters( 'autoptimize_filter_css_datauri_maxsize', 4096 );
464 }
465
466 return $max_size;
467 }
468
469 private function check_datauri_exclude_list( $url )
470 {
471 static $exclude_list = null;
472 $no_datauris = array();
473
474 // Again, skip doing certain stuff repeatedly when loop-called.
475 if ( null === $exclude_list ) {
476 $exclude_list = apply_filters( 'autoptimize_filter_css_datauri_exclude', '' );
477 $no_datauris = array_filter( array_map( 'trim', explode( ',', $exclude_list ) ) );
478 }
479
480 $matched = false;
481
482 if ( ! empty( $exclude_list ) ) {
483 foreach ( $no_datauris as $no_datauri ) {
484 if ( false !== strpos( $url, $no_datauri ) ) {
485 $matched = true;
486 break;
487 }
488 }
489 }
490
491 return $matched;
492 }
493
494 private function build_or_get_datauri_image( $path )
495 {
496 /**
497 * TODO/FIXME: document the required return array format, or better yet,
498 * use a string, since we don't really need an array for this. That would, however,
499 * require changing even more code, which is not happening right now...
500 */
501
502 // Allows short-circuiting datauri generation for an image.
503 $result = apply_filters( 'autoptimize_filter_css_datauri_image', array(), $path );
504 if ( ! empty( $result ) ) {
505 if ( is_array( $result ) && isset( $result['full'] ) && isset( $result['base64data'] ) ) {
506 return $result;
507 }
508 }
509
510 $hash = md5( $path );
511 $check = new autoptimizeCache( $hash, 'img' );
512 if ( $check->check() ) {
513 // we have the base64 image in cache.
514 $head_and_data = $check->retrieve();
515 $_base64data = explode( ';base64,', $head_and_data );
516 $base64data = $_base64data[1];
517 unset( $_base64data );
518 } else {
519 // It's an image and we don't have it in cache, get the type by extension.
520 $exploded_path = explode( '.', $path );
521 $type = end( $exploded_path );
522
523 switch ( $type ) {
524 case 'jpg':
525 case 'jpeg':
526 $dataurihead = 'data:image/jpeg;base64,';
527 break;
528 case 'gif':
529 $dataurihead = 'data:image/gif;base64,';
530 break;
531 case 'png':
532 $dataurihead = 'data:image/png;base64,';
533 break;
534 case 'bmp':
535 $dataurihead = 'data:image/bmp;base64,';
536 break;
537 case 'webp':
538 $dataurihead = 'data:image/webp;base64,';
539 break;
540 default:
541 $dataurihead = 'data:application/octet-stream;base64,';
542 }
543
544 // Encode the data.
545 $base64data = base64_encode( file_get_contents( $path ) );
546 $head_and_data = $dataurihead . $base64data;
547
548 // Save in cache.
549 $check->cache( $head_and_data, 'text/plain' );
550 }
551 unset( $check );
552
553 return array(
554 'full' => $head_and_data,
555 'base64data' => $base64data,
556 );
557 }
558
559 /**
560 * Given an array of key/value pairs to replace in $string,
561 * it does so by replacing the longest-matching strings first.
562 *
563 * @param string $string string in which to replace.
564 * @param array $replacements to be replaced strings and replacement.
565 *
566 * @return string
567 */
568 protected static function replace_longest_matches_first( $string, $replacements = array() )
569 {
570 if ( ! empty( $replacements ) ) {
571 // Sort the replacements array by key length in desc order (so that the longest strings are replaced first).
572 $keys = array_map( 'strlen', array_keys( $replacements ) );
573 array_multisort( $keys, SORT_DESC, $replacements );
574 $string = str_replace( array_keys( $replacements ), array_values( $replacements ), $string );
575 }
576
577 return $string;
578 }
579
580 /**
581 * Rewrites/Replaces any ASSETS_REGEX-matching urls in a string.
582 * Removes quotes/cruft around each one and passes it through to
583 * `autoptimizeBase::url_replace_cdn()`.
584 * Replacements are performed in a `longest-match-replaced-first` way.
585 *
586 * @param string $code CSS code.
587 *
588 * @return string
589 */
590 public function replace_urls( $code = '' )
591 {
592 $replacements = array();
593
594 preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
595 if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
596 foreach ( $url_src_matches[1] as $count => $original_url ) {
597 // Removes quotes and other cruft.
598 $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
599
600 /**
601 * TODO/FIXME: Add a way for other code / callable to be called here
602 * and provide it's own results for the $replacements array
603 * for the "current" key.
604 * If such a result is returned/provided, we sholud then avoid
605 * calling url_replace_cdn() here for the current iteration.
606 *
607 * This would maybe allow the inlining logic currently present
608 * in `autoptimizeStyles::rewrite_assets()` to be "pulled out"
609 * and given as a callable to this method or something... and
610 * then we could "just" call `replace_urls()` from within
611 * `autoptimizeStyles::rewrite_assets()` and avoid some
612 * (currently present) code/logic duplication.
613 */
614
615 // Do CDN replacement if needed.
616 if ( ! empty( $this->cdn_url ) ) {
617 $replacement_url = $this->url_replace_cdn( $url );
618 // Prepare replacements array.
619 $replacements[ $url_src_matches[1][ $count ] ] = str_replace(
620 $original_url, $replacement_url, $url_src_matches[1][ $count ]
621 );
622 }
623 }
624 }
625
626 $code = self::replace_longest_matches_first( $code, $replacements );
627
628 return $code;
629 }
630
631 /**
632 * "Hides" @font-face declarations by replacing them with `%%FONTFACE%%` markers.
633 * Also does CDN replacement of any font-urls within those declarations if the `autoptimize_filter_css_fonts_cdn`
634 * filter is used.
635 *
636 * @param string $code HTML being processed to hide fonts.
637 * @return string
638 */
639 public function hide_fontface_and_maybe_cdn( $code )
640 {
641 // Proceed only if @font-face declarations exist within $code.
642 preg_match_all( self::FONT_FACE_REGEX, $code, $fontfaces );
643 if ( isset( $fontfaces[0] ) ) {
644 // Check if we need to cdn fonts or not.
645 $do_font_cdn = apply_filters( 'autoptimize_filter_css_fonts_cdn', false );
646
647 foreach ( $fontfaces[0] as $full_match ) {
648 // Keep original match so we can search/replace it.
649 $match_search = $full_match;
650
651 // Do font cdn if needed.
652 if ( $do_font_cdn ) {
653 $full_match = $this->replace_urls( $full_match );
654 }
655
656 // Replace declaration with its base64 encoded string.
657 $replacement = self::build_marker( 'FONTFACE', $full_match );
658 $code = str_replace( $match_search, $replacement, $code );
659 }
660 }
661
662 return $code;
663 }
664
665 /**
666 * Restores original @font-face declarations that have been "hidden"
667 * using `hide_fontface_and_maybe_cdn()`.
668 *
669 * @param string $code HTML being processed to unhide fonts.
670 * @return string
671 */
672 public function restore_fontface( $code )
673 {
674 return $this->restore_marked_content( 'FONTFACE', $code );
675 }
676
677 /**
678 * Re-write (and/or inline) referenced assets.
679 *
680 * @param string $code HTML being processed rewrite assets.
681 * @return string
682 */
683 public function rewrite_assets( $code )
684 {
685 // Handle @font-face rules by hiding and processing them separately.
686 $code = $this->hide_fontface_and_maybe_cdn( $code );
687
688 /**
689 * TODO/FIXME:
690 * Certain code parts below are kind-of repeated now in `replace_urls()`, which is not ideal.
691 * There is maybe a way to separate/refactor things and then be able to keep
692 * the ASSETS_REGEX rewriting/handling logic in a single place (along with removing quotes/cruft from matched urls).
693 * See comments in `replace_urls()` regarding this. The idea is to extract the inlining
694 * logic out (which is the only real difference between replace_urls() and the code below), but still
695 * achieve identical results as before.
696 */
697
698 // Re-write (and/or inline) URLs to point them to the CDN host.
699 $url_src_matches = array();
700 $imgreplace = array();
701
702 // Matches and captures anything specified within the literal `url()` and excludes those containing data: URIs.
703 preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
704 if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
705 foreach ( $url_src_matches[1] as $count => $original_url ) {
706 // Removes quotes and other cruft.
707 $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
708
709 // If datauri inlining is turned on, do it.
710 $inlined = false;
711 if ( $this->datauris ) {
712 $iurl = $url;
713 if ( false !== strpos( $iurl, '?' ) ) {
714 $iurl = strtok( $iurl, '?' );
715 }
716
717 $ipath = $this->getpath( $iurl );
718
719 $excluded = $this->check_datauri_exclude_list( $ipath );
720 if ( ! $excluded ) {
721 $is_datauri_candidate = $this->is_datauri_candidate( $ipath );
722 if ( $is_datauri_candidate ) {
723 $datauri = $this->build_or_get_datauri_image( $ipath );
724 $base64data = $datauri['base64data'];
725 // Add it to the list for replacement.
726 $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
727 $original_url,
728 $datauri['full'],
729 $url_src_matches[1][ $count ]
730 );
731 $inlined = true;
732 }
733 }
734 }
735
736 /**
737 * Doing CDN URL replacement for every found match (if CDN is
738 * specified). This way we make sure to do it even if
739 * inlining isn't turned on, or if a resource is skipped from
740 * being inlined for whatever reason above.
741 */
742 if ( ! $inlined && ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) ) {
743 // Just do the "simple" CDN replacement.
744 $replacement_url = $this->url_replace_cdn( $url );
745 $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
746 $original_url, $replacement_url, $url_src_matches[1][ $count ]
747 );
748 }
749 }
750 }
751
752 $code = self::replace_longest_matches_first( $code, $imgreplace );
753
754 // Replace back font-face markers with actual font-face declarations.
755 $code = $this->restore_fontface( $code );
756
757 return $code;
758 }
759
760 /**
761 * Joins and optimizes CSS.
762 */
763 public function minify()
764 {
765 foreach ( $this->css as $group ) {
766 list( $media, $css ) = $group;
767 if ( preg_match( '#^INLINE;#', $css ) ) {
768 // <style>.
769 $css = preg_replace( '#^INLINE;#', '', $css );
770 $css = self::fixurls( ABSPATH . 'index.php', $css ); // ABSPATH already contains a trailing slash.
771 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, '' );
772 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
773 $css = $tmpstyle;
774 $this->alreadyminified = true;
775 }
776 } else {
777 // <link>
778 if ( false !== $css && file_exists( $css ) && is_readable( $css ) ) {
779 $css_path = $css;
780 $css = self::fixurls( $css_path, file_get_contents( $css_path ) );
781 $css = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $css );
782 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, $css_path );
783 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
784 $css = $tmpstyle;
785 $this->alreadyminified = true;
786 } elseif ( $this->can_inject_late( $css_path, $css ) ) {
787 $css = self::build_injectlater_marker( $css_path, md5( $css ) );
788 }
789 } else {
790 // Couldn't read CSS. Maybe getpath isn't working?
791 $css = '';
792 }
793 }
794
795 foreach ( $media as $elem ) {
796 if ( ! empty( $css ) ) {
797 if ( ! isset( $this->csscode[ $elem ] ) ) {
798 $this->csscode[ $elem ] = '';
799 }
800 $this->csscode[ $elem ] .= "\n/*FILESTART*/" . $css;
801 }
802 }
803 }
804
805 // Check for duplicate code.
806 $md5list = array();
807 $tmpcss = $this->csscode;
808 foreach ( $tmpcss as $media => $code ) {
809 $md5sum = md5( $code );
810 $medianame = $media;
811 foreach ( $md5list as $med => $sum ) {
812 // If same code.
813 if ( $sum === $md5sum ) {
814 // Add the merged code.
815 $medianame = $med . ', ' . $media;
816 $this->csscode[ $medianame ] = $code;
817 $md5list[ $medianame ] = $md5list[ $med ];
818 unset( $this->csscode[ $med ], $this->csscode[ $media ], $md5list[ $med ] );
819 }
820 }
821 $md5list[ $medianame ] = $md5sum;
822 }
823 unset( $tmpcss );
824
825 // Manage @imports, while is for recursive import management.
826 foreach ( $this->csscode as &$thiscss ) {
827 // Flag to trigger import reconstitution and var to hold external imports.
828 $fiximports = false;
829 $external_imports = '';
830
831 // remove comments to avoid importing commented-out imports.
832 $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
833 while ( preg_match_all( '#@import +(?:url)?(?:(?:\((["\']?)(?:[^"\')]+)\1\)|(["\'])(?:[^"\']+)\2)(?:[^,;"\']+(?:,[^,;"\']+)*)?)(?:;)#mi', $thiscss_nocomments, $matches ) ) {
834 foreach ( $matches[0] as $import ) {
835 if ( $this->isremovable( $import, $this->cssremovables ) ) {
836 $thiscss = str_replace( $import, '', $thiscss );
837 $import_ok = true;
838 } else {
839 $url = trim( preg_replace( '#^.*((?:https?:|ftp:)?//.*\.css).*$#', '$1', trim( $import ) ), " \t\n\r\0\x0B\"'" );
840 $path = $this->getpath( $url );
841 $import_ok = false;
842 if ( file_exists( $path ) && is_readable( $path ) ) {
843 $code = addcslashes( self::fixurls( $path, file_get_contents( $path ) ), '\\' );
844 $code = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $code );
845 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $code, '' );
846 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
847 $code = $tmpstyle;
848 $this->alreadyminified = true;
849 } elseif ( $this->can_inject_late( $path, $code ) ) {
850 $code = self::build_injectlater_marker( $path, md5( $code ) );
851 }
852
853 if ( ! empty( $code ) ) {
854 $tmp_thiscss = preg_replace( '#(/\*FILESTART\*/.*)' . preg_quote( $import, '#' ) . '#Us', '/*FILESTART2*/' . $code . '$1', $thiscss );
855 if ( ! empty( $tmp_thiscss ) ) {
856 $thiscss = $tmp_thiscss;
857 $import_ok = true;
858 unset( $tmp_thiscss );
859 }
860 }
861 unset( $code );
862 }
863 }
864 if ( ! $import_ok ) {
865 // External imports and general fall-back.
866 $external_imports .= $import;
867
868 $thiscss = str_replace( $import, '', $thiscss );
869 $fiximports = true;
870 }
871 }
872 $thiscss = preg_replace( '#/\*FILESTART\*/#', '', $thiscss );
873 $thiscss = preg_replace( '#/\*FILESTART2\*/#', '/*FILESTART*/', $thiscss );
874
875 // and update $thiscss_nocomments before going into next iteration in while loop.
876 $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
877 }
878 unset( $thiscss_nocomments );
879
880 // Add external imports to top of aggregated CSS.
881 if ( $fiximports ) {
882 $thiscss = $external_imports . $thiscss;
883 }
884 }
885 unset( $thiscss );
886
887 // $this->csscode has all the uncompressed code now.
888 foreach ( $this->csscode as &$code ) {
889 // Check for already-minified code.
890 $hash = md5( $code );
891 do_action( 'autoptimize_action_css_hash', $hash );
892 $ccheck = new autoptimizeCache( $hash, 'css' );
893 if ( $ccheck->check() ) {
894 $code = $ccheck->retrieve();
895 $this->hashmap[ md5( $code ) ] = $hash;
896 continue;
897 }
898 unset( $ccheck );
899
900 // Rewrite and/or inline referenced assets.
901 $code = $this->rewrite_assets( $code );
902
903 // Minify.
904 $code = $this->run_minifier_on( $code );
905
906 // Bring back INJECTLATER stuff.
907 $code = $this->inject_minified( $code );
908
909 // Filter results.
910 $tmp_code = apply_filters( 'autoptimize_css_after_minify', $code );
911 if ( ! empty( $tmp_code ) ) {
912 $code = $tmp_code;
913 unset( $tmp_code );
914 }
915
916 $this->hashmap[ md5( $code ) ] = $hash;
917 }
918
919 unset( $code );
920 return true;
921 }
922
923 public function run_minifier_on( $code )
924 {
925 if ( ! $this->alreadyminified ) {
926 $do_minify = apply_filters( 'autoptimize_css_do_minify', true );
927
928 if ( $do_minify ) {
929 $cssmin = new autoptimizeCSSmin();
930 $tmp_code = trim( $cssmin->run( $code ) );
931
932 if ( ! empty( $tmp_code ) ) {
933 $code = $tmp_code;
934 unset( $tmp_code );
935 }
936 }
937 }
938
939 return $code;
940 }
941
942 /**
943 * Caches the CSS in uncompressed, deflated and gzipped form.
944 */
945 public function cache()
946 {
947 // CSS cache.
948 foreach ( $this->csscode as $media => $code ) {
949 if ( empty( $code ) ) {
950 continue;
951 }
952
953 $md5 = $this->hashmap[ md5( $code ) ];
954 $cache = new autoptimizeCache( $md5, 'css' );
955 if ( ! $cache->check() ) {
956 // Cache our code.
957 $cache->cache( $code, 'text/css' );
958 }
959 $this->url[ $media ] = AUTOPTIMIZE_CACHE_URL . $cache->getname();
960 }
961 }
962
963 /**
964 * Returns the content.
965 */
966 public function getcontent()
967 {
968 // Restore the full content (only applies when "autoptimize_filter_css_justhead" filter is true).
969 if ( ! empty( $this->restofcontent ) ) {
970 $this->content .= $this->restofcontent;
971 $this->restofcontent = '';
972 }
973
974 // type is not added by default.
975 $type_css = '';
976 if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
977 $type_css = 'type="text/css" ';
978 }
979
980 // Inject the new stylesheets.
981 $replace_tag = array( '<title', 'before' );
982 $replace_tag = apply_filters( 'autoptimize_filter_css_replacetag', $replace_tag, $this->content );
983
984 if ( $this->inline ) {
985 foreach ( $this->csscode as $media => $code ) {
986 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $code . '</style>' ), $replace_tag );
987 }
988 } else {
989 if ( $this->defer ) {
990 $preload_css_block = '';
991 $inlined_ccss_block = '';
992 $noscript_css_block = '<noscript id="aonoscrcss">';
993
994 $defer_inline_code = $this->defer_inline;
995 if ( ! empty( $defer_inline_code ) ) {
996 if ( apply_filters( 'autoptimize_filter_css_critcss_minify', true ) ) {
997 $icss_hash = md5( $defer_inline_code );
998 $icss_cache = new autoptimizeCache( $icss_hash, 'css' );
999 if ( $icss_cache->check() ) {
1000 // we have the optimized inline CSS in cache.
1001 $defer_inline_code = $icss_cache->retrieve();
1002 } else {
1003 $cssmin = new autoptimizeCSSmin();
1004 $tmp_code = trim( $cssmin->run( $defer_inline_code ) );
1005
1006 if ( ! empty( $tmp_code ) ) {
1007 $defer_inline_code = $tmp_code;
1008 $icss_cache->cache( $defer_inline_code, 'text/css' );
1009 unset( $tmp_code );
1010 }
1011 }
1012 }
1013 // inlined critical css set here, but injected when full CSS is injected
1014 // to avoid CSS containing SVG with <title tag receiving the full CSS link.
1015 $inlined_ccss_block = '<style ' . $type_css . 'id="aoatfcss" media="all">' . $defer_inline_code . '</style>';
1016 }
1017 }
1018
1019 foreach ( $this->url as $media => $url ) {
1020 $url = $this->url_replace_cdn( $url );
1021
1022 // Add the stylesheet either deferred (import at bottom) or normal links in head.
1023 if ( $this->defer ) {
1024 $preload_onload = autoptimizeConfig::get_ao_css_preload_onload();
1025
1026 $preload_css_block .= '<link rel="stylesheet" media="print" href="' . $url . '" onload="' . $preload_onload . '" />';
1027 if ( apply_filters( 'autoptimize_fitler_css_preload_and_print', false ) ) {
1028 $preload_css_block = '<link rel="preload" as="stylesheet" href="' . $url . '"/>' . $preload_css_block;
1029 }
1030 $noscript_css_block .= '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />';
1031 } else {
1032 if ( strlen( $this->csscode[ $media ] ) > $this->cssinlinesize ) {
1033 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />' ), $replace_tag );
1034 } elseif ( strlen( $this->csscode[ $media ] ) > 0 ) {
1035 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $this->csscode[ $media ] . '</style>' ), $replace_tag );
1036 }
1037 }
1038 }
1039
1040 if ( $this->defer ) {
1041 $noscript_css_block .= '</noscript>';
1042 // Inject inline critical CSS, the preloaded full CSS and the noscript-CSS.
1043 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', $inlined_ccss_block . $preload_css_block . $noscript_css_block ), $replace_tag );
1044 }
1045 }
1046
1047 // restore comments.
1048 $this->content = $this->restore_comments( $this->content );
1049
1050 // restore IE hacks.
1051 $this->content = $this->restore_iehacks( $this->content );
1052
1053 // restore (no)script.
1054 $this->content = $this->restore_marked_content( 'SCRIPT', $this->content );
1055
1056 // Restore noptimize.
1057 $this->content = $this->restore_noptimize( $this->content );
1058
1059 // Return the modified stylesheet.
1060 return $this->content;
1061 }
1062
1063 /**
1064 * Make sure URL's are absolute iso relative to original CSS location.
1065 *
1066 * @param string $file filename of optimized CSS-file.
1067 * @param string $code CSS-code in which to fix URL's.
1068 */
1069 static function fixurls( $file, $code )
1070 {
1071 // Switch all imports to the url() syntax.
1072 $code = preg_replace( '#@import ("|\')(.+?)\.css.*?("|\')#', '@import url("${2}.css")', $code );
1073
1074 if ( preg_match_all( self::ASSETS_REGEX, $code, $matches ) ) {
1075 $file = str_replace( WP_ROOT_DIR, '/', $file );
1076 /**
1077 * Rollback as per https://github.com/futtta/autoptimize/issues/94
1078 * $file = str_replace( AUTOPTIMIZE_WP_CONTENT_NAME, '', $file );
1079 */
1080 $dir = dirname( $file ); // Like /themes/expound/css.
1081
1082 /**
1083 * $dir should not contain backslashes, since it's used to replace
1084 * urls, but it can contain them when running on Windows because
1085 * fixurls() is sometimes called with `ABSPATH . 'index.php'`
1086 */
1087 $dir = str_replace( '\\', '/', $dir );
1088 unset( $file ); // not used below at all.
1089
1090 $replace = array();
1091 foreach ( $matches[1] as $k => $url ) {
1092 // Remove quotes.
1093 $url = trim( $url, " \t\n\r\0\x0B\"'" );
1094 $no_q_url = trim( $url, "\"'" );
1095 if ( $url !== $no_q_url ) {
1096 $removed_quotes = true;
1097 } else {
1098 $removed_quotes = false;
1099 }
1100
1101 if ( '' === $no_q_url ) {
1102 continue;
1103 }
1104
1105 $url = $no_q_url;
1106 if ( '/' === $url[0] || preg_match( '#^(https?://|ftp://|data:)#i', $url ) ) {
1107 // URL is protocol-relative, host-relative or something we don't touch.
1108 continue;
1109 } else { // Relative URL.
1110
1111 /*
1112 * rollback as per https://github.com/futtta/autoptimize/issues/94
1113 * $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_CONTENT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1114 */
1115 $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_ROOT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1116 $newurl = apply_filters( 'autoptimize_filter_css_fixurl_newurl', $newurl );
1117
1118 /**
1119 * Hash the url + whatever was behind potentially for replacement
1120 * We must do this, or different css classes referencing the same bg image (but
1121 * different parts of it, say, in sprites and such) loose their stuff...
1122 */
1123 $hash = md5( $url . $matches[2][ $k ] );
1124 $code = str_replace( $matches[0][ $k ], $hash, $code );
1125
1126 if ( $removed_quotes ) {
1127 $replace[ $hash ] = "url('" . $newurl . "')" . $matches[2][ $k ];
1128 } else {
1129 $replace[ $hash ] = 'url(' . $newurl . ')' . $matches[2][ $k ];
1130 }
1131 }
1132 }
1133
1134 $code = self::replace_longest_matches_first( $code, $replace );
1135 }
1136
1137 return $code;
1138 }
1139
1140 private function ismovable( $tag )
1141 {
1142 if ( ! $this->aggregate ) {
1143 return false;
1144 }
1145
1146 if ( ! empty( $this->allowlist ) ) {
1147 foreach ( $this->allowlist as $match ) {
1148 if ( false !== strpos( $tag, $match ) ) {
1149 return true;
1150 }
1151 }
1152 // no match with allowlist.
1153 return false;
1154 } else {
1155 if ( is_array( $this->dontmove ) && ! empty( $this->dontmove ) ) {
1156 foreach ( $this->dontmove as $match ) {
1157 if ( false !== strpos( $tag, $match ) ) {
1158 // Matched something.
1159 return false;
1160 }
1161 }
1162 }
1163
1164 // If we're here it's safe to move.
1165 return true;
1166 }
1167 }
1168
1169 private function can_inject_late( $css_path, $css )
1170 {
1171 $consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false, $css_path );
1172 if ( true !== $this->inject_min_late ) {
1173 // late-inject turned off.
1174 return false;
1175 } elseif ( ( false === strpos( $css_path, 'min.css' ) ) && ( str_replace( $consider_minified_array, '', $css_path ) === $css_path ) ) {
1176 // file not minified based on filename & filter.
1177 return false;
1178 } elseif ( false !== strpos( $css, '@import' ) ) {
1179 // can't late-inject files with imports as those need to be aggregated.
1180 return false;
1181 } elseif ( ( false !== strpos( $css, '@font-face' ) ) && ( apply_filters( 'autoptimize_filter_css_fonts_cdn', false ) === true ) && ( ! empty( $this->cdn_url ) ) ) {
1182 // don't late-inject CSS with font-src's if fonts are set to be CDN'ed.
1183 return false;
1184 } elseif ( ( ( true == $this->datauris ) || ( ! empty( $this->cdn_url ) ) ) && preg_match( '#background[^;}]*url\(#Ui', $css ) ) {
1185 // don't late-inject CSS with images if CDN is set OR if image inlining is on.
1186 return false;
1187 } else {
1188 // phew, all is safe, we can late-inject.
1189 return true;
1190 }
1191 }
1192
1193 /**
1194 * Minifies (and cdn-replaces) a single local css file
1195 * and returns its (cached) url.
1196 *
1197 * @param string $filepath Filepath.
1198 * @param bool $cache_miss Optional. Force a cache miss. Default false.
1199 *
1200 * @return bool|string Url pointing to the minified css file or false.
1201 */
1202 public function minify_single( $filepath, $cache_miss = false )
1203 {
1204 $contents = $this->prepare_minify_single( $filepath );
1205
1206 if ( empty( $contents ) ) {
1207 return false;
1208 }
1209
1210 // Check cache.
1211 $hash = 'single_' . md5( $contents );
1212 $cache = new autoptimizeCache( $hash, 'css' );
1213 do_action( 'autoptimize_action_css_hash', $hash );
1214
1215 // If not in cache already, minify...
1216 if ( ! $cache->check() || $cache_miss ) {
1217 // Fixurls...
1218 $contents = self::fixurls( $filepath, $contents );
1219 // CDN-replace any referenced assets if needed...
1220 $contents = $this->hide_fontface_and_maybe_cdn( $contents );
1221 $contents = $this->replace_urls( $contents );
1222 $contents = $this->restore_fontface( $contents );
1223 // Now minify...
1224 $cssmin = new autoptimizeCSSmin();
1225 $contents = trim( $cssmin->run( $contents ) );
1226
1227 // Check if minified cache content is empty.
1228 if ( empty( $contents ) ) {
1229 return false;
1230 }
1231
1232 // Filter contents of excluded minified CSS.
1233 $contents = apply_filters( 'autoptimize_filter_css_single_after_minify', $contents );
1234
1235 // Store in cache.
1236 $cache->cache( $contents, 'text/css' );
1237 }
1238
1239 $url = $this->build_minify_single_url( $cache );
1240
1241 return $url;
1242 }
1243
1244 /**
1245 * Returns whether we're doing aggregation or not.
1246 *
1247 * @return bool
1248 */
1249 public function aggregating()
1250 {
1251 return $this->aggregate;
1252 }
1253
1254 public function getOptions()
1255 {
1256 return $this->options;
1257 }
1258
1259 public function replaceOptions( $options )
1260 {
1261 $this->options = $options;
1262 }
1263
1264 public function setOption( $name, $value )
1265 {
1266 $this->options[ $name ] = $value;
1267 $this->$name = $value;
1268 }
1269
1270 public function getOption( $name )
1271 {
1272 return $this->options[ $name ];
1273 }
1274 }
1275