PluginProbe
Autoptimize / 2.6.1
Autoptimize v2.6.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.6.1, at classes/autoptimizeStyles.php

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