PluginProbe
Autoptimize / 2.7.3
Autoptimize v2.7.3
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.3, at classes/autoptimizeStyles.php

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