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

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