PluginProbe
Autoptimize / 2.4.0
Autoptimize v2.4.0
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.4.0, at classes/autoptimizeStyles.php

1,071 lines 43.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 * @font-face\s* # Match @font-face and some spaces
18 * ( # Start group 1
19 * \{ # Match {
20 * (?: # A non-capturing group
21 * [^{}]+ # Match anything except {} one or more times
22 * | # Or
23 * (?1) # Recurse/rerun the expression of group 1
24 * )* # Repeat 0 or more times
25 * \} # Match }
26 * ) # End group 1
27 * ~xs';
28 */
29 const FONT_FACE_REGEX = '~@font-face\s*(\{(?:[^{}]+|(?1))*\})~xsi'; // added `i` flag for case-insensitivity.
30
31 private $css = array();
32 private $csscode = array();
33 private $url = array();
34 private $restofcontent = '';
35 private $datauris = false;
36 private $hashmap = array();
37 private $alreadyminified = false;
38 private $aggregate = true;
39 private $inline = false;
40 private $defer = false;
41 private $defer_inline = false;
42 private $whitelist = '';
43 private $cssinlinesize = '';
44 private $cssremovables = array();
45 private $include_inline = false;
46 private $inject_min_late = '';
47
48 // public $cdn_url; // Used all over the place implicitly, so will have to be either public or protected :/ .
49
50 // Reads the page and collects style tags.
51 public function read( $options )
52 {
53 $noptimizeCSS = apply_filters( 'autoptimize_filter_css_noptimize', false, $this->content );
54 if ( $noptimizeCSS ) {
55 return false;
56 }
57
58 $whitelistCSS = apply_filters( 'autoptimize_filter_css_whitelist', '', $this->content );
59 if ( ! empty( $whitelistCSS ) ) {
60 $this->whitelist = array_filter( array_map( 'trim', explode( ',', $whitelistCSS ) ) );
61 }
62
63 $removableCSS = apply_filters( 'autoptimize_filter_css_removables', '' );
64 if ( ! empty( $removableCSS ) ) {
65 $this->cssremovables = array_filter( array_map( 'trim', explode( ',', $removableCSS ) ) );
66 }
67
68 $this->cssinlinesize = apply_filters( 'autoptimize_filter_css_inlinesize', 256 );
69
70 // filter to "late inject minified CSS", default to true for now (it is faster).
71 $this->inject_min_late = apply_filters( 'autoptimize_filter_css_inject_min_late', true );
72
73 // Remove everything that's not the header.
74 if ( apply_filters( 'autoptimize_filter_css_justhead', $options['justhead'] ) ) {
75 $content = explode( '</head>', $this->content, 2 );
76 $this->content = $content[0] . '</head>';
77 $this->restofcontent = $content[1];
78 }
79
80 // Determine whether we're doing CSS-files aggregation or not.
81 if ( isset( $options['aggregate'] ) && ! $options['aggregate'] ) {
82 $this->aggregate = false;
83 }
84 // Returning true for "dontaggregate" turns off aggregation.
85 if ( $this->aggregate && apply_filters( 'autoptimize_filter_css_dontaggregate', false ) ) {
86 $this->aggregate = false;
87 }
88
89 // include inline?
90 if ( apply_filters( 'autoptimize_css_include_inline', $options['include_inline'] ) ) {
91 $this->include_inline = true;
92 }
93
94 // List of CSS strings which are excluded from autoptimization.
95 $excludeCSS = apply_filters( 'autoptimize_filter_css_exclude', $options['css_exclude'], $this->content );
96 if ( '' !== $excludeCSS ) {
97 $this->dontmove = array_filter( array_map( 'trim', explode( ',', $excludeCSS ) ) );
98 } else {
99 $this->dontmove = array();
100 }
101
102 // forcefully exclude CSS with data-noptimize attrib.
103 $this->dontmove[] = 'data-noptimize';
104
105 // Should we defer css?
106 // value: true / false.
107 $this->defer = $options['defer'];
108 $this->defer = apply_filters( 'autoptimize_filter_css_defer', $this->defer, $this->content );
109
110 // Should we inline while deferring?
111 // value: inlined CSS.
112 $this->defer_inline = apply_filters( 'autoptimize_filter_css_defer_inline', $options['defer_inline'], $this->content );
113
114 // Should we inline?
115 // value: true / false.
116 $this->inline = $options['inline'];
117 $this->inline = apply_filters( 'autoptimize_filter_css_inline', $this->inline, $this->content );
118
119 // Store cdn url.
120 $this->cdn_url = $options['cdn_url'];
121
122 // Store data: URIs setting for later use.
123 $this->datauris = $options['datauris'];
124
125 // noptimize me.
126 $this->content = $this->hide_noptimize( $this->content );
127
128 // Exclude (no)script, as those may contain CSS which should be left as is.
129 $this->content = $this->replace_contents_with_marker_if_exists(
130 'SCRIPT',
131 '<script',
132 '#<(?:no)?script.*?<\/(?:no)?script>#is',
133 $this->content
134 );
135
136 // Save IE hacks.
137 $this->content = $this->hide_iehacks( $this->content );
138
139 // Hide HTML comments.
140 $this->content = $this->hide_comments( $this->content );
141
142 // Get <style> and <link>.
143 if ( preg_match_all( '#(<style[^>]*>.*</style>)|(<link[^>]*stylesheet[^>]*>)#Usmi', $this->content, $matches ) ) {
144
145 foreach ( $matches[0] as $tag ) {
146 if ( $this->isremovable( $tag, $this->cssremovables ) ) {
147 $this->content = str_replace( $tag, '', $this->content );
148 } elseif ( $this->ismovable( $tag ) ) {
149 // Get the media.
150 if ( false !== strpos( $tag, 'media=' ) ) {
151 preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $medias );
152 $medias = explode( ',', $medias[1] );
153 $media = array();
154 foreach ( $medias as $elem ) {
155 /* $media[] = current(explode(' ',trim($elem),2)); */
156 if ( empty( $elem ) ) {
157 $elem = 'all';
158 }
159
160 $media[] = $elem;
161 }
162 } else {
163 // No media specified - applies to all.
164 $media = array( 'all' );
165 }
166
167 $media = apply_filters( 'autoptimize_filter_css_tagmedia', $media, $tag );
168
169 if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) {
170 // <link>.
171 $url = current( explode( '?', $source[2], 2 ) );
172 $path = $this->getpath( $url );
173
174 if ( false !== $path && preg_match( '#\.css$#', $path ) ) {
175 // Good link.
176 $this->css[] = array( $media, $path );
177 } else {
178 // Link is dynamic (.php etc).
179 $tag = '';
180 }
181 } else {
182 // Inline css in style tags can be wrapped in comment tags, so restore comments.
183 $tag = $this->restore_comments( $tag );
184 preg_match( '#<style.*>(.*)</style>#Usmi', $tag, $code );
185
186 // And re-hide them to be able to to the removal based on tag.
187 $tag = $this->hide_comments( $tag );
188
189 if ( $this->include_inline ) {
190 $code = preg_replace( '#^.*<!\[CDATA\[(?:\s*\*/)?(.*)(?://|/\*)\s*?\]\]>.*$#sm', '$1', $code[1] );
191 $this->css[] = array( $media, 'INLINE;' . $code );
192 } else {
193 $tag = '';
194 }
195 }
196
197 // Remove the original style tag.
198 $this->content = str_replace( $tag, '', $this->content );
199 } else {
200 // Excluded CSS, minify if getpath and filter says so...
201 if ( preg_match( '#<link.*href=("|\')(.*)("|\')#Usmi', $tag, $source ) ) {
202 $exploded_url = explode( '?', $source[2], 2 );
203 $url = $exploded_url[0];
204 $path = $this->getpath( $url );
205
206 if ( $path && apply_filters( 'autoptimize_filter_css_minify_excluded', true, $url ) ) {
207 $minified_url = $this->minify_single( $path );
208 if ( ! empty( $minified_url ) ) {
209 // Replace orig URL with cached minified URL.
210 $new_tag = str_replace( $url, $minified_url, $tag );
211 } else {
212 $new_tag = $tag;
213 }
214
215 // Removes querystring from URL.
216 if ( ! empty( $exploded_url[1] ) ) {
217 $new_tag = str_replace( '?' . $exploded_url[1], '', $new_tag );
218 }
219
220 // Defer single CSS if "inline & defer" is ON and there is inline CSS.
221 if ( $this->defer && ! empty( $this->defer_inline ) ) {
222 // Get/ set (via filter) the JS to be triggers onload of the preloaded CSS.
223 $_preload_onload = apply_filters(
224 'autoptimize_filter_css_preload_onload',
225 "this.onload=null;this.rel='stylesheet'",
226 $url
227 );
228 // Adapt original <link> element for CSS to be preloaded and add <noscript>-version for fallback.
229 $new_tag = '<noscript>' . $new_tag . '</noscript>' . str_replace(
230 array(
231 "rel='stylesheet'",
232 'rel="stylesheet"',
233 ),
234 "rel='preload' as='style' onload=\"" . $_preload_onload . "\"",
235 $new_tag
236 );
237 }
238
239 // And replace!
240 $this->content = str_replace( $tag, $new_tag, $this->content );
241 }
242 }
243 }
244 }
245 return true;
246 }
247
248 // Really, no styles?
249 return false;
250 }
251
252 /**
253 * Checks if the local file referenced by $path is a valid
254 * candidate for being inlined into a data: URI
255 *
256 * @param string $path
257 * @return boolean
258 */
259 private function is_datauri_candidate( $path )
260 {
261 // Call only once since it's called from a loop.
262 static $max_size = null;
263 if ( null === $max_size ) {
264 $max_size = $this->get_datauri_maxsize();
265 }
266
267 if ( $path && preg_match( '#\.(jpe?g|png|gif|webp|bmp)$#i', $path ) &&
268 file_exists( $path ) && is_readable( $path ) && filesize( $path ) <= $max_size ) {
269
270 // Seems we have a candidate.
271 $is_candidate = true;
272 } else {
273 // Filter allows overriding default decision (which checks for local file existence).
274 $is_candidate = apply_filters( 'autoptimize_filter_css_is_datauri_candidate', false, $path );
275 }
276
277 return $is_candidate;
278 }
279
280 /**
281 * Returns the amount of bytes that shouldn't be exceeded if a file is to
282 * be inlined into a data: URI. Defaults to 4096, passed through
283 * `autoptimize_filter_css_datauri_maxsize` filter.
284 *
285 * @return mixed
286 */
287 private function get_datauri_maxsize()
288 {
289 static $max_size = null;
290
291 /**
292 * No need to apply the filter multiple times in case the
293 * method itself is invoked multiple times during a single request.
294 * This prevents some wild stuff like having different maxsizes
295 * for different files/site-sections etc. But if you're into that sort
296 * of thing you're probably better of building assets completely
297 * outside of WordPress anyway.
298 */
299 if ( null === $max_size ) {
300 $max_size = (int) apply_filters( 'autoptimize_filter_css_datauri_maxsize', 4096 );
301 }
302
303 return $max_size;
304 }
305
306 private function check_datauri_exclude_list( $url )
307 {
308 static $exclude_list = null;
309 $no_datauris = array();
310
311 // Again, skip doing certain stuff repeatedly when loop-called.
312 if ( null === $exclude_list ) {
313 $exclude_list = apply_filters( 'autoptimize_filter_css_datauri_exclude', '' );
314 $no_datauris = array_filter( array_map( 'trim', explode( ',', $exclude_list ) ) );
315 }
316
317 $matched = false;
318
319 if ( ! empty( $exclude_list ) ) {
320 foreach ( $no_datauris as $no_datauri ) {
321 if ( false !== strpos( $url, $no_datauri ) ) {
322 $matched = true;
323 break;
324 }
325 }
326 }
327
328 return $matched;
329 }
330
331 private function build_or_get_datauri_image( $path )
332 {
333 /**
334 * TODO/FIXME: document the required return array format, or better yet,
335 * use a string, since we don't really need an array for this. That would, however,
336 * require changing even more code, which is not happening right now...
337 */
338
339 // Allows short-circuiting datauri generation for an image.
340 $result = apply_filters( 'autoptimize_filter_css_datauri_image', array(), $path );
341 if ( ! empty( $result ) ) {
342 if ( is_array( $result ) && isset( $result['full'] ) && isset( $result['base64data'] ) ) {
343 return $result;
344 }
345 }
346
347 $hash = md5( $path );
348 $check = new autoptimizeCache( $hash, 'img' );
349 if ( $check->check() ) {
350 // we have the base64 image in cache.
351 $headAndData = $check->retrieve();
352 $_base64data = explode( ';base64,', $headAndData );
353 $base64data = $_base64data[1];
354 unset( $_base64data );
355 } else {
356 // It's an image and we don't have it in cache, get the type by extension.
357 $exploded_path = explode( '.', $path );
358 $type = end( $exploded_path );
359
360 switch ( $type ) {
361 case 'jpg':
362 case 'jpeg':
363 $dataurihead = 'data:image/jpeg;base64,';
364 break;
365 case 'gif':
366 $dataurihead = 'data:image/gif;base64,';
367 break;
368 case 'png':
369 $dataurihead = 'data:image/png;base64,';
370 break;
371 case 'bmp':
372 $dataurihead = 'data:image/bmp;base64,';
373 break;
374 case 'webp':
375 $dataurihead = 'data:image/webp;base64,';
376 break;
377 default:
378 $dataurihead = 'data:application/octet-stream;base64,';
379 }
380
381 // Encode the data.
382 $base64data = base64_encode( file_get_contents( $path ) );
383 $headAndData = $dataurihead . $base64data;
384
385 // Save in cache.
386 $check->cache( $headAndData, 'text/plain' );
387 }
388 unset( $check );
389
390 return array( 'full' => $headAndData, 'base64data' => $base64data );
391 }
392
393 /**
394 * Given an array of key/value pairs to replace in $string,
395 * it does so by replacing the longest-matching strings first.
396 *
397 * @param string $string
398 * @param array $replacements
399 *
400 * @return string
401 */
402 protected static function replace_longest_matches_first( $string, $replacements = array() )
403 {
404 if ( ! empty( $replacements ) ) {
405 // Sort the replacements array by key length in desc order (so that the longest strings are replaced first).
406 $keys = array_map( 'strlen', array_keys( $replacements ) );
407 array_multisort( $keys, SORT_DESC, $replacements );
408 $string = str_replace( array_keys( $replacements ), array_values( $replacements ), $string );
409 }
410
411 return $string;
412 }
413
414 /**
415 * Rewrites/Replaces any ASSETS_REGEX-matching urls in a string.
416 * Removes quotes/cruft around each one and passes it through to
417 * `autoptimizeBase::url_replace_cdn()`.
418 * Replacements are performed in a `longest-match-replaced-first` way.
419 *
420 * @param string $code CSS code.
421 *
422 * @return string
423 */
424 public function replace_urls( $code = '' )
425 {
426 $replacements = array();
427
428 preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
429 if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
430 foreach ( $url_src_matches[1] as $count => $original_url ) {
431 // Removes quotes and other cruft.
432 $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
433
434 /**
435 * TODO/FIXME: Add a way for other code / callable to be called here
436 * and provide it's own results for the $replacements array
437 * for the "current" key.
438 * If such a result is returned/provided, we sholud then avoid
439 * calling url_replace_cdn() here for the current iteration.
440 *
441 * This would maybe allow the inlining logic currently present
442 * in `autoptimizeStyles::rewrite_assets()` to be "pulled out"
443 * and given as a callable to this method or something... and
444 * then we could "just" call `replace_urls()` from within
445 * `autoptimizeStyles::rewrite_assets()` and avoid some
446 * (currently present) code/logic duplication.
447 */
448
449 // Do CDN replacement if needed.
450 if ( ! empty( $this->cdn_url ) ) {
451 $replacement_url = $this->url_replace_cdn( $url );
452 // Prepare replacements array.
453 $replacements[ $url_src_matches[1][ $count ] ] = str_replace(
454 $original_url, $replacement_url, $url_src_matches[1][$count]
455 );
456 }
457 }
458 }
459
460 $code = self::replace_longest_matches_first( $code, $replacements );
461
462 return $code;
463 }
464
465 /**
466 * "Hides" @font-face declarations by replacing them with `%%FONTFACE%%` markers.
467 * Also does CDN replacement of any font-urls within those declarations if the `autoptimize_filter_css_fonts_cdn`
468 * filter is used.
469 *
470 * @param string $code
471 * @return string
472 */
473 public function hide_fontface_and_maybe_cdn( $code )
474 {
475 // Proceed only if @font-face declarations exist within $code.
476 preg_match_all( self::FONT_FACE_REGEX, $code, $fontfaces );
477 if ( isset( $fontfaces[0] ) ) {
478 // Check if we need to cdn fonts or not.
479 $do_font_cdn = apply_filters( 'autoptimize_filter_css_fonts_cdn', false );
480
481 foreach ( $fontfaces[0] as $full_match ) {
482 // Keep original match so we can search/replace it.
483 $match_search = $full_match;
484
485 // Do font cdn if needed.
486 if ( $do_font_cdn ) {
487 $full_match = $this->replace_urls( $full_match );
488 }
489
490 // Replace declaration with its base64 encoded string.
491 $replacement = self::build_marker( 'FONTFACE', $full_match );
492 $code = str_replace( $match_search, $replacement, $code );
493 }
494 }
495
496 return $code;
497 }
498
499 /**
500 * Restores original @font-face declarations that have been "hidden"
501 * using `hide_fontface_and_maybe_cdn()`.
502 *
503 * @param string $code
504 * @return string
505 */
506 public function restore_fontface( $code )
507 {
508 return $this->restore_marked_content( 'FONTFACE', $code );
509 }
510
511 // Re-write (and/or inline) referenced assets.
512 public function rewrite_assets( $code )
513 {
514 // Handle @font-face rules by hiding and processing them separately.
515 $code = $this->hide_fontface_and_maybe_cdn( $code );
516
517 /**
518 * TODO/FIXME:
519 * Certain code parts below are kind-of repeated now in `replace_urls()`, which is not ideal.
520 * There is maybe a way to separate/refactor things and then be able to keep
521 * the ASSETS_REGEX rewriting/handling logic in a single place (along with removing quotes/cruft from matched urls).
522 * See comments in `replace_urls()` regarding this. The idea is to extract the inlining
523 * logic out (which is the only real difference between replace_urls() and the code below), but still
524 * achieve identical results as before.
525 */
526
527 // Re-write (and/or inline) URLs to point them to the CDN host.
528 $url_src_matches = array();
529 $imgreplace = array();
530 // Matches and captures anything specified within the literal `url()` and excludes those containing data: URIs.
531 preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
532 if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
533 foreach ( $url_src_matches[1] as $count => $original_url ) {
534 // Removes quotes and other cruft.
535 $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
536
537 // If datauri inlining is turned on, do it.
538 $inlined = false;
539 if ( $this->datauris ) {
540 $iurl = $url;
541 if ( false !== strpos( $iurl, '?' ) ) {
542 $iurl = strtok( $iurl, '?' );
543 }
544
545 $ipath = $this->getpath( $iurl );
546
547 $excluded = $this->check_datauri_exclude_list( $ipath );
548 if ( ! $excluded ) {
549 $is_datauri_candidate = $this->is_datauri_candidate( $ipath );
550 if ( $is_datauri_candidate ) {
551 $datauri = $this->build_or_get_datauri_image( $ipath );
552 $base64data = $datauri['base64data'];
553 // Add it to the list for replacement.
554 $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
555 $original_url,
556 $datauri['full'],
557 $url_src_matches[1][$count]
558 );
559 $inlined = true;
560 }
561 }
562 }
563
564 /**
565 * Doing CDN URL replacement for every found match (if CDN is
566 * specified). This way we make sure to do it even if
567 * inlining isn't turned on, or if a resource is skipped from
568 * being inlined for whatever reason above.
569 */
570 if ( ! $inlined && ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) ) {
571 // Just do the "simple" CDN replacement.
572 $replacement_url = $this->url_replace_cdn( $url );
573 $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
574 $original_url, $replacement_url, $url_src_matches[1][$count]
575 );
576 }
577 }
578 }
579
580 $code = self::replace_longest_matches_first( $code, $imgreplace );
581
582 // Replace back font-face markers with actual font-face declarations.
583 $code = $this->restore_fontface( $code );
584
585 return $code;
586 }
587
588 // Joins and optimizes CSS.
589 public function minify()
590 {
591 foreach ( $this->css as $group ) {
592 list( $media, $css ) = $group;
593 if ( preg_match( '#^INLINE;#', $css ) ) {
594 // <style>.
595 $css = preg_replace( '#^INLINE;#', '', $css );
596 $css = self::fixurls( ABSPATH . 'index.php', $css ); // ABSPATH already contains a trailing slash.
597 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, '' );
598 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
599 $css = $tmpstyle;
600 $this->alreadyminified = true;
601 }
602 } else {
603 // <link>
604 if ( false !== $css && file_exists( $css ) && is_readable( $css ) ) {
605 $cssPath = $css;
606 $css = self::fixurls( $cssPath, file_get_contents( $cssPath ) );
607 $css = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $css );
608 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, $cssPath );
609 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
610 $css = $tmpstyle;
611 $this->alreadyminified = true;
612 } elseif ( $this->can_inject_late( $cssPath, $css ) ) {
613 $css = self::build_injectlater_marker( $cssPath, md5( $css ) );
614 }
615 } else {
616 // Couldn't read CSS. Maybe getpath isn't working?
617 $css = '';
618 }
619 }
620
621 foreach ( $media as $elem ) {
622 if ( ! empty( $css ) ) {
623 if ( ! isset( $this->csscode[$elem] ) ) {
624 $this->csscode[$elem] = '';
625 }
626 $this->csscode[$elem] .= "\n/*FILESTART*/" . $css;
627 }
628 }
629 }
630
631 // Check for duplicate code.
632 $md5list = array();
633 $tmpcss = $this->csscode;
634 foreach ( $tmpcss as $media => $code ) {
635 $md5sum = md5( $code );
636 $medianame = $media;
637 foreach ( $md5list as $med => $sum ) {
638 // If same code.
639 if ( $sum === $md5sum ) {
640 // Add the merged code.
641 $medianame = $med . ', ' . $media;
642 $this->csscode[$medianame] = $code;
643 $md5list[$medianame] = $md5list[$med];
644 unset( $this->csscode[$med], $this->csscode[$media], $med5list[$med] );
645 }
646 }
647 $md5list[$medianame] = $md5sum;
648 }
649 unset( $tmpcss );
650
651 // Manage @imports, while is for recursive import management.
652 foreach ( $this->csscode as &$thiscss ) {
653 // Flag to trigger import reconstitution and var to hold external imports.
654 $fiximports = false;
655 $external_imports = '';
656
657 // remove comments to avoid importing commented-out imports.
658 $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
659 while ( preg_match_all( '#@import +(?:url)?(?:(?:\((["\']?)(?:[^"\')]+)\1\)|(["\'])(?:[^"\']+)\2)(?:[^,;"\']+(?:,[^,;"\']+)*)?)(?:;)#mi', $thiscss_nocomments, $matches ) ) {
660 foreach ( $matches[0] as $import ) {
661 if ( $this->isremovable( $import, $this->cssremovables ) ) {
662 $thiscss = str_replace( $import, '', $thiscss );
663 $import_ok = true;
664 } else {
665 $url = trim( preg_replace( '#^.*((?:https?:|ftp:)?//.*\.css).*$#', '$1', trim( $import ) ), " \t\n\r\0\x0B\"'" );
666 $path = $this->getpath( $url );
667 $import_ok = false;
668 if ( file_exists( $path ) && is_readable( $path ) ) {
669 $code = addcslashes( self::fixurls( $path, file_get_contents( $path ) ), "\\" );
670 $code = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $code );
671 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $code, '' );
672 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
673 $code = $tmpstyle;
674 $this->alreadyminified = true;
675 } elseif ( $this->can_inject_late( $path, $code ) ) {
676 $code = self::build_injectlater_marker( $path, md5( $code ) );
677 }
678
679 if ( ! empty( $code ) ) {
680 $tmp_thiscss = preg_replace( '#(/\*FILESTART\*/.*)' . preg_quote( $import, '#' ) . '#Us', '/*FILESTART2*/' . $code . '$1', $thiscss );
681 if ( ! empty( $tmp_thiscss ) ) {
682 $thiscss = $tmp_thiscss;
683 $import_ok = true;
684 unset( $tmp_thiscss );
685 }
686 }
687 unset( $code );
688 }
689 }
690 if ( ! $import_ok ) {
691 // External imports and general fall-back.
692 $external_imports .= $import;
693
694 $thiscss = str_replace( $import, '', $thiscss );
695 $fiximports = true;
696 }
697 }
698 $thiscss = preg_replace( '#/\*FILESTART\*/#', '', $thiscss );
699 $thiscss = preg_replace( '#/\*FILESTART2\*/#', '/*FILESTART*/', $thiscss );
700
701 // and update $thiscss_nocomments before going into next iteration in while loop.
702 $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
703 }
704 unset( $thiscss_nocomments );
705
706 // Add external imports to top of aggregated CSS.
707 if ( $fiximports ) {
708 $thiscss = $external_imports . $thiscss;
709 }
710 }
711 unset( $thiscss );
712
713 // $this->csscode has all the uncompressed code now.
714 foreach ( $this->csscode as &$code ) {
715 // Check for already-minified code.
716 $hash = md5( $code );
717 do_action( 'autoptimize_action_css_hash', $hash );
718 $ccheck = new autoptimizeCache( $hash, 'css' );
719 if ( $ccheck->check() ) {
720 $code = $ccheck->retrieve();
721 $this->hashmap[md5( $code )] = $hash;
722 continue;
723 }
724 unset( $ccheck );
725
726 // Rewrite and/or inline referenced assets.
727 $code = $this->rewrite_assets( $code );
728
729 // Minify.
730 $code = $this->run_minifier_on( $code );
731
732 // Bring back INJECTLATER stuff.
733 $code = $this->inject_minified( $code );
734
735 // Filter results.
736 $tmp_code = apply_filters( 'autoptimize_css_after_minify', $code );
737 if ( ! empty( $tmp_code ) ) {
738 $code = $tmp_code;
739 unset( $tmp_code );
740 }
741
742 $this->hashmap[md5( $code )] = $hash;
743 }
744
745 unset( $code );
746 return true;
747 }
748
749 public function run_minifier_on( $code )
750 {
751 if ( ! $this->alreadyminified ) {
752 $do_minify = apply_filters( 'autoptimize_css_do_minify', true );
753
754 if ( $do_minify ) {
755 $cssmin = new autoptimizeCSSmin();
756 $tmp_code = trim( $cssmin->run( $code ) );
757
758 if ( ! empty( $tmp_code ) ) {
759 $code = $tmp_code;
760 unset( $tmp_code );
761 }
762 }
763 }
764
765 return $code;
766 }
767
768 // Caches the CSS in uncompressed, deflated and gzipped form.
769 public function cache()
770 {
771 // CSS cache.
772 foreach ( $this->csscode as $media => $code ) {
773 $md5 = $this->hashmap[md5( $code )];
774 $cache = new autoptimizeCache( $md5, 'css' );
775 if ( ! $cache->check() ) {
776 // Cache our code.
777 $cache->cache( $code, 'text/css' );
778 }
779 $this->url[$media] = AUTOPTIMIZE_CACHE_URL . $cache->getname();
780 }
781 }
782
783 // Returns the content.
784 public function getcontent()
785 {
786 // restore comments.
787 $this->content = $this->restore_comments( $this->content );
788
789 // restore IE hacks.
790 $this->content = $this->restore_iehacks( $this->content );
791
792 // restore (no)script.
793 $this->content = $this->restore_marked_content( 'SCRIPT', $this->content );
794
795 // Restore noptimize.
796 $this->content = $this->restore_noptimize( $this->content );
797
798 // Restore the full content.
799 if ( ! empty( $this->restofcontent ) ) {
800 $this->content .= $this->restofcontent;
801 $this->restofcontent = '';
802 }
803
804 // Inject the new stylesheets.
805 $replaceTag = array( '<title', 'before' );
806 $replaceTag = apply_filters( 'autoptimize_filter_css_replacetag', $replaceTag, $this->content );
807
808 if ( $this->inline ) {
809 foreach ( $this->csscode as $media => $code ) {
810 $this->inject_in_html( '<style type="text/css" media="' . $media . '">' . $code . '</style>', $replaceTag );
811 }
812 } else {
813 if ( $this->defer ) {
814 $preloadCssBlock = '';
815 $noScriptCssBlock = "<noscript id=\"aonoscrcss\">";
816
817 $defer_inline_code = $this->defer_inline;
818 if ( ! empty( $defer_inline_code ) ) {
819 if ( apply_filters( 'autoptimize_filter_css_critcss_minify', true ) ) {
820 $iCssHash = md5( $defer_inline_code );
821 $iCssCache = new autoptimizeCache( $iCssHash, 'css' );
822 if ( $iCssCache->check() ) {
823 // we have the optimized inline CSS in cache.
824 $defer_inline_code = $iCssCache->retrieve();
825 } else {
826 $cssmin = new autoptimizeCSSmin();
827 $tmp_code = trim( $cssmin->run( $defer_inline_code ) );
828
829 if ( ! empty( $tmp_code ) ) {
830 $defer_inline_code = $tmp_code;
831 $iCssCache->cache( $defer_inline_code, 'text/css' );
832 unset( $tmp_code );
833 }
834 }
835 }
836 $code_out = '<style type="text/css" id="aoatfcss" media="all">' . $defer_inline_code . '</style>';
837 $this->inject_in_html( $code_out, $replaceTag );
838 }
839 }
840
841 foreach ( $this->url as $media => $url ) {
842 $url = $this->url_replace_cdn( $url );
843
844 // Add the stylesheet either deferred (import at bottom) or normal links in head.
845 if ( $this->defer ) {
846 $preloadOnLoad = autoptimizeConfig::get_ao_css_preload_onload();
847
848 $preloadCssBlock .= '<link rel="preload" as="style" media="' . $media . '" href="' . $url . '" onload="' . $preloadOnLoad . '" />';
849 $noScriptCssBlock .= '<link type="text/css" media="' . $media . '" href="' . $url . '" rel="stylesheet" />';
850 } else {
851 // $this->inject_in_html('<link type="text/css" media="' . $media . '" href="' . $url . '" rel="stylesheet" />', $replaceTag);
852 if ( strlen( $this->csscode[$media] ) > $this->cssinlinesize ) {
853 $this->inject_in_html( '<link type="text/css" media="' . $media . '" href="' . $url . '" rel="stylesheet" />', $replaceTag );
854 } elseif ( strlen( $this->csscode[$media] ) > 0 ) {
855 $this->inject_in_html( '<style type="text/css" media="' . $media . '">' . $this->csscode[$media] . '</style>', $replaceTag );
856 }
857 }
858 }
859
860 if ( $this->defer ) {
861 $preload_polyfill = autoptimizeConfig::get_ao_css_preload_polyfill();
862 $noScriptCssBlock .= '</noscript>';
863 $this->inject_in_html( $preloadCssBlock . $noScriptCssBlock, $replaceTag );
864
865 // Adds preload polyfill at end of body tag.
866 $this->inject_in_html(
867 apply_filters( 'autoptimize_css_preload_polyfill', $preload_polyfill ),
868 array( '</body>', 'before' )
869 );
870 }
871 }
872
873 // Return the modified stylesheet.
874 return $this->content;
875 }
876
877 static function fixurls( $file, $code )
878 {
879 // Switch all imports to the url() syntax.
880 $code = preg_replace( '#@import ("|\')(.+?)\.css.*?("|\')#', '@import url("${2}.css")', $code );
881
882 if ( preg_match_all( self::ASSETS_REGEX, $code, $matches ) ) {
883 $file = str_replace( WP_ROOT_DIR, '/', $file );
884 /**
885 * rollback as per https://github.com/futtta/autoptimize/issues/94
886 * $file = str_replace( AUTOPTIMIZE_WP_CONTENT_NAME, '', $file );
887 */
888 $dir = dirname( $file ); // Like /themes/expound/css.
889
890 /**
891 * $dir should not contain backslashes, since it's used to replace
892 * urls, but it can contain them when running on Windows because
893 * fixurls() is sometimes called with `ABSPATH . 'index.php'`
894 */
895 $dir = str_replace( '\\', '/', $dir );
896 unset( $file ); // not used below at all.
897
898 $replace = array();
899 foreach ( $matches[1] as $k => $url ) {
900 // Remove quotes.
901 $url = trim( $url, " \t\n\r\0\x0B\"'" );
902 $noQurl = trim( $url, "\"'" );
903 if ( $url !== $noQurl ) {
904 $removedQuotes = true;
905 } else {
906 $removedQuotes = false;
907 }
908
909 if ( '' === $noQurl ) {
910 continue;
911 }
912
913 $url = $noQurl;
914 if ( '/' === $url{0} || preg_match( '#^(https?://|ftp://|data:)#i', $url ) ) {
915 // URL is protocol-relative, host-relative or something we don't touch.
916 continue;
917 } else {
918 // Relative URL.
919 /**
920 * rollback as per https://github.com/futtta/autoptimize/issues/94
921 * $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_CONTENT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
922 */
923 $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_ROOT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
924 $newurl = apply_filters( 'autoptimize_filter_css_fixurl_newurl', $newurl );
925
926 /**
927 * Hash the url + whatever was behind potentially for replacement
928 * We must do this, or different css classes referencing the same bg image (but
929 * different parts of it, say, in sprites and such) loose their stuff...
930 */
931 $hash = md5( $url . $matches[2][$k] );
932 $code = str_replace( $matches[0][$k], $hash, $code );
933
934 if ( $removedQuotes ) {
935 $replace[$hash] = "url('" . $newurl . "')" . $matches[2][$k];
936 } else {
937 $replace[$hash] = 'url(' . $newurl . ')' . $matches[2][$k];
938 }
939 }
940 }
941
942 $code = self::replace_longest_matches_first( $code, $replace );
943 }
944
945 return $code;
946 }
947
948 private function ismovable( $tag )
949 {
950 if ( ! $this->aggregate ) {
951 return false;
952 }
953
954 if ( ! empty( $this->whitelist ) ) {
955 foreach ( $this->whitelist as $match ) {
956 if ( false !== strpos( $tag, $match ) ) {
957 return true;
958 }
959 }
960 // no match with whitelist.
961 return false;
962 } else {
963 if ( is_array( $this->dontmove ) && ! empty( $this->dontmove ) ) {
964 foreach ( $this->dontmove as $match ) {
965 if ( false !== strpos( $tag, $match ) ) {
966 // Matched something.
967 return false;
968 }
969 }
970 }
971
972 // If we're here it's safe to move.
973 return true;
974 }
975 }
976
977 private function can_inject_late( $cssPath, $css )
978 {
979 $consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false, $cssPath );
980 if ( true !== $this->inject_min_late ) {
981 // late-inject turned off.
982 return false;
983 } elseif ( ( false === strpos( $cssPath, 'min.css' ) ) && ( str_replace( $consider_minified_array, '', $cssPath ) === $cssPath ) ) {
984 // file not minified based on filename & filter.
985 return false;
986 } elseif ( false !== strpos( $css, '@import' ) ) {
987 // can't late-inject files with imports as those need to be aggregated.
988 return false;
989 } elseif ( ( false !== strpos( $css, '@font-face' ) ) && ( apply_filters( 'autoptimize_filter_css_fonts_cdn', false ) === true ) && ( ! empty( $this->cdn_url ) ) ) {
990 // don't late-inject CSS with font-src's if fonts are set to be CDN'ed.
991 return false;
992 } elseif ( ( ( $this->datauris == true ) || ( ! empty( $this->cdn_url ) ) ) && preg_match( '#background[^;}]*url\(#Ui', $css ) ) {
993 // don't late-inject CSS with images if CDN is set OR if image inlining is on.
994 return false;
995 } else {
996 // phew, all is safe, we can late-inject.
997 return true;
998 }
999 }
1000
1001 /**
1002 * Minifies (and cdn-replaces) a single local css file
1003 * and returns its (cached) url.
1004 *
1005 * @param string $filepath Filepath.
1006 * @param bool $cache_miss Optional. Force a cache miss. Default false.
1007 *
1008 * @return bool|string Url pointing to the minified css file or false.
1009 */
1010 public function minify_single( $filepath, $cache_miss = false )
1011 {
1012 $contents = $this->prepare_minify_single( $filepath );
1013
1014 if ( empty( $contents ) ) {
1015 return false;
1016 }
1017
1018 // Check cache.
1019 $hash = 'single_' . md5( $contents );
1020 $cache = new autoptimizeCache( $hash, 'css' );
1021
1022 // If not in cache already, minify...
1023 if ( ! $cache->check() || $cache_miss ) {
1024 // Fixurls...
1025 $contents = self::fixurls( $filepath, $contents );
1026 // CDN-replace any referenced assets if needed...
1027 $contents = $this->replace_urls( $contents );
1028 // Now minify...
1029 $cssmin = new autoptimizeCSSmin();
1030 $contents = trim( $cssmin->run( $contents ) );
1031 // Store in cache.
1032 $cache->cache( $contents, 'text/css' );
1033 }
1034
1035 $url = $this->build_minify_single_url( $cache );
1036
1037 return $url;
1038 }
1039
1040 /**
1041 * Returns whether we're doing aggregation or not.
1042 *
1043 * @return bool
1044 */
1045 public function aggregating()
1046 {
1047 return $this->aggregate;
1048 }
1049
1050 public function getOptions()
1051 {
1052 return $this->options;
1053 }
1054
1055 public function replaceOptions( $options )
1056 {
1057 $this->options = $options;
1058 }
1059
1060 public function setOption( $name, $value )
1061 {
1062 $this->options[$name] = $value;
1063 $this->$name = $value;
1064 }
1065
1066 public function getOption( $name )
1067 {
1068 return $this->options[$name];
1069 }
1070 }
1071