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

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