PluginProbe
Autoptimize / 2.7.2
Autoptimize v2.7.2
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeStyles.php

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

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