PluginProbe
Autoptimize / 3.0.3
Autoptimize v3.0.3
2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 All 107 releases
autoptimize / classes / autoptimizeStyles.php

autoptimizeStyles.php in Autoptimize 3.0.3, at classes/autoptimizeStyles.php

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