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