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