PluginProbe
Autoptimize / 3.1.7
Autoptimize v3.1.7
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.1.7, at classes/autoptimizeStyles.php

1,338 lines 53.0 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,
643 $replacement_url,
644 $url_src_matches[1][ $count ]
645 );
646 }
647 }
648 }
649
650 $code = self::replace_longest_matches_first( $code, $replacements );
651
652 return $code;
653 }
654
655 /**
656 * "Hides" @font-face declarations by replacing them with `%%FONTFACE%%` markers.
657 * Also does CDN replacement of any font-urls within those declarations if the `autoptimize_filter_css_fonts_cdn`
658 * filter is used.
659 *
660 * @param string $code HTML being processed to hide fonts.
661 * @return string
662 */
663 public function hide_fontface_and_maybe_cdn( $code )
664 {
665 // Proceed only if @font-face declarations exist within $code.
666 preg_match_all( self::FONT_FACE_REGEX, $code, $fontfaces );
667 if ( isset( $fontfaces[0] ) ) {
668 // Check if we need to cdn fonts or not.
669 $do_font_cdn = apply_filters( 'autoptimize_filter_css_fonts_cdn', false );
670
671 foreach ( $fontfaces[0] as $full_match ) {
672 // Keep original match so we can search/replace it.
673 $match_search = $full_match;
674
675 // Do font cdn if needed.
676 if ( $do_font_cdn ) {
677 $full_match = $this->replace_urls( $full_match );
678 }
679
680 // Replace declaration with its base64 encoded string.
681 $replacement = self::build_marker( 'FONTFACE', $full_match );
682 $code = str_replace( $match_search, $replacement, $code );
683 }
684 }
685
686 return $code;
687 }
688
689 /**
690 * Restores original @font-face declarations that have been "hidden"
691 * using `hide_fontface_and_maybe_cdn()`.
692 *
693 * @param string $code HTML being processed to unhide fonts.
694 * @return string
695 */
696 public function restore_fontface( $code )
697 {
698 return $this->restore_marked_content( 'FONTFACE', $code );
699 }
700
701 /**
702 * Re-write (and/or inline) referenced assets.
703 *
704 * @param string $code HTML being processed rewrite assets.
705 * @return string
706 */
707 public function rewrite_assets( $code )
708 {
709 // Handle @font-face rules by hiding and processing them separately.
710 $code = $this->hide_fontface_and_maybe_cdn( $code );
711
712 /**
713 * TODO/FIXME:
714 * Certain code parts below are kind-of repeated now in `replace_urls()`, which is not ideal.
715 * There is maybe a way to separate/refactor things and then be able to keep
716 * the ASSETS_REGEX rewriting/handling logic in a single place (along with removing quotes/cruft from matched urls).
717 * See comments in `replace_urls()` regarding this. The idea is to extract the inlining
718 * logic out (which is the only real difference between replace_urls() and the code below), but still
719 * achieve identical results as before.
720 */
721
722 // Re-write (and/or inline) URLs to point them to the CDN host.
723 $url_src_matches = array();
724 $imgreplace = array();
725
726 // Matches and captures anything specified within the literal `url()` and excludes those containing data: URIs.
727 preg_match_all( self::ASSETS_REGEX, $code, $url_src_matches );
728 if ( is_array( $url_src_matches ) && ! empty( $url_src_matches ) ) {
729 foreach ( $url_src_matches[1] as $count => $original_url ) {
730 // Removes quotes and other cruft.
731 $url = trim( $original_url, " \t\n\r\0\x0B\"'" );
732
733 // If datauri inlining is turned on, do it.
734 $inlined = false;
735 if ( $this->datauris ) {
736 $iurl = $url;
737 if ( false !== strpos( $iurl, '?' ) ) {
738 $iurl = strtok( $iurl, '?' );
739 }
740
741 $ipath = $this->getpath( $iurl );
742
743 $excluded = $this->check_datauri_exclude_list( $ipath );
744 if ( ! $excluded ) {
745 $is_datauri_candidate = $this->is_datauri_candidate( $ipath );
746 if ( $is_datauri_candidate ) {
747 $datauri = $this->build_or_get_datauri_image( $ipath );
748 $base64data = $datauri['base64data'];
749 // Add it to the list for replacement.
750 $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
751 $original_url,
752 $datauri['full'],
753 $url_src_matches[1][ $count ]
754 );
755 $inlined = true;
756 }
757 }
758 }
759
760 /**
761 * Doing CDN URL replacement for every found match (if CDN is
762 * specified). This way we make sure to do it even if
763 * inlining isn't turned on, or if a resource is skipped from
764 * being inlined for whatever reason above.
765 */
766 if ( ! $inlined && ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) ) {
767 // Just do the "simple" CDN replacement.
768 $replacement_url = $this->url_replace_cdn( $url );
769 $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace(
770 $original_url,
771 $replacement_url,
772 $url_src_matches[1][ $count ]
773 );
774 }
775 }
776 }
777
778 $code = self::replace_longest_matches_first( $code, $imgreplace );
779
780 // Replace back font-face markers with actual font-face declarations.
781 $code = $this->restore_fontface( $code );
782
783 return $code;
784 }
785
786 /**
787 * Joins and optimizes CSS.
788 */
789 public function minify()
790 {
791 foreach ( $this->css as $group ) {
792 list( $media, $css ) = $group;
793 if ( preg_match( '#^INLINE;#', $css ) ) {
794 // <style>.
795 $css = preg_replace( '#^INLINE;#', '', $css );
796 $css = self::fixurls( ABSPATH . 'index.php', $css ); // ABSPATH already contains a trailing slash.
797 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, '' );
798 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
799 $css = $tmpstyle;
800 $this->alreadyminified = true;
801 }
802 } else {
803 // <link>
804 if ( false !== $css && file_exists( $css ) && is_readable( $css ) ) {
805 $css_path = $css;
806 $css = self::fixurls( $css_path, file_get_contents( $css_path ) );
807 $css = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $css );
808 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $css, $css_path );
809 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
810 $css = $tmpstyle;
811 $this->alreadyminified = true;
812 } elseif ( $this->can_inject_late( $css_path, $css ) ) {
813 $css = self::build_injectlater_marker( $css_path, md5( $css ) );
814 }
815 } else {
816 // Couldn't read CSS. Maybe getpath isn't working?
817 $css = '';
818 }
819 }
820
821 foreach ( $media as $elem ) {
822 if ( ! empty( $css ) ) {
823 if ( ! isset( $this->csscode[ $elem ] ) ) {
824 $this->csscode[ $elem ] = '';
825 }
826 $this->csscode[ $elem ] .= "\n/*FILESTART*/" . $css;
827 }
828 }
829 }
830
831 // Check for duplicate code.
832 $md5list = array();
833 $tmpcss = $this->csscode;
834 foreach ( $tmpcss as $media => $code ) {
835 $md5sum = md5( $code );
836 $medianame = $media;
837 foreach ( $md5list as $med => $sum ) {
838 // If same code.
839 if ( $sum === $md5sum ) {
840 // Add the merged code.
841 $medianame = $med . ', ' . $media;
842 $this->csscode[ $medianame ] = $code;
843 $md5list[ $medianame ] = $md5list[ $med ];
844 unset( $this->csscode[ $med ], $this->csscode[ $media ], $md5list[ $med ] );
845 }
846 }
847 $md5list[ $medianame ] = $md5sum;
848 }
849 unset( $tmpcss );
850
851 // Manage @imports, while is for recursive import management.
852 foreach ( $this->csscode as &$thiscss ) {
853 // Flag to trigger import reconstitution and var to hold external imports.
854 $fiximports = false;
855 $external_imports = '';
856
857 // remove comments to avoid importing commented-out imports.
858 $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
859 while ( preg_match_all( '#@import +(?:url)?(?:(?:\((["\']?)(?:[^"\')]+)\1\)|(["\'])(?:[^"\']+)\2)(?:[^,;"\']+(?:,[^,;"\']+)*)?)(?:;)#mi', $thiscss_nocomments, $matches ) ) {
860 foreach ( $matches[0] as $import ) {
861 if ( $this->isremovable( $import, $this->cssremovables ) ) {
862 $thiscss = str_replace( $import, '', $thiscss );
863 $import_ok = true;
864 } else {
865 $url = trim( preg_replace( '#^.*((?:https?:|ftp:)?//.*\.css).*$#', '$1', trim( $import ) ), " \t\n\r\0\x0B\"'" );
866 $path = $this->getpath( $url );
867 $import_ok = false;
868 if ( file_exists( $path ) && is_readable( $path ) ) {
869 $code = addcslashes( self::fixurls( $path, file_get_contents( $path ) ), '\\' );
870 $code = preg_replace( '/\x{EF}\x{BB}\x{BF}/', '', $code );
871 $tmpstyle = apply_filters( 'autoptimize_css_individual_style', $code, '' );
872 if ( has_filter( 'autoptimize_css_individual_style' ) && ! empty( $tmpstyle ) ) {
873 $code = $tmpstyle;
874 $this->alreadyminified = true;
875 } elseif ( $this->can_inject_late( $path, $code ) ) {
876 $code = self::build_injectlater_marker( $path, md5( $code ) );
877 }
878
879 if ( ! empty( $code ) ) {
880 $tmp_thiscss = str_replace( $import, stripcslashes( $code ), $thiscss );
881 if ( ! empty( $tmp_thiscss ) ) {
882 $thiscss = $tmp_thiscss;
883 $import_ok = true;
884 unset( $tmp_thiscss );
885 }
886 }
887 unset( $code );
888 }
889 }
890 if ( ! $import_ok ) {
891 // External imports and general fall-back.
892 $external_imports .= $import;
893
894 $thiscss = str_replace( $import, '', $thiscss );
895 $fiximports = true;
896 }
897 }
898 $thiscss = preg_replace( '#/\*FILESTART\*/#', '', $thiscss );
899 $thiscss = preg_replace( '#/\*FILESTART2\*/#', '/*FILESTART*/', $thiscss );
900
901 // and update $thiscss_nocomments before going into next iteration in while loop.
902 $thiscss_nocomments = preg_replace( '#/\*.*\*/#Us', '', $thiscss );
903 }
904 unset( $thiscss_nocomments );
905
906 // Add external imports to top of aggregated CSS.
907 if ( $fiximports ) {
908 $thiscss = $external_imports . $thiscss;
909 }
910 }
911 unset( $thiscss );
912
913 // $this->csscode has all the uncompressed code now.
914 foreach ( $this->csscode as &$code ) {
915 // Check for already-minified code.
916 $hash = md5( $code );
917 do_action( 'autoptimize_action_css_hash', $hash );
918 $ccheck = new autoptimizeCache( $hash, 'css' );
919 if ( $ccheck->check() ) {
920 $code = $ccheck->retrieve();
921 $this->hashmap[ md5( $code ) ] = $hash;
922 continue;
923 }
924 unset( $ccheck );
925
926 // Rewrite and/or inline referenced assets.
927 $code = $this->rewrite_assets( $code );
928
929 // Minify.
930 $code = $this->run_minifier_on( $code );
931
932 // Bring back INJECTLATER stuff.
933 $code = $this->inject_minified( $code );
934
935 // Filter results.
936 $tmp_code = apply_filters( 'autoptimize_css_after_minify', $code );
937 if ( ! empty( $tmp_code ) ) {
938 $code = $tmp_code;
939 unset( $tmp_code );
940 }
941
942 $this->hashmap[ md5( $code ) ] = $hash;
943 }
944
945 unset( $code );
946 return true;
947 }
948
949 public function run_minifier_on( $code )
950 {
951 if ( ! $this->alreadyminified ) {
952 $do_minify = apply_filters( 'autoptimize_css_do_minify', true );
953
954 if ( $do_minify ) {
955 $cssmin = new autoptimizeCSSmin();
956 $tmp_code = trim( $cssmin->run( $code ) );
957
958 if ( ! empty( $tmp_code ) ) {
959 $code = $tmp_code;
960 unset( $tmp_code );
961 }
962 }
963 }
964
965 return $code;
966 }
967
968 /**
969 * Caches the CSS in uncompressed, deflated and gzipped form.
970 */
971 public function cache()
972 {
973 // CSS cache.
974 foreach ( $this->csscode as $media => $code ) {
975 if ( empty( $code ) ) {
976 continue;
977 }
978
979 $md5 = $this->hashmap[ md5( $code ) ];
980 $cache = new autoptimizeCache( $md5, 'css' );
981 if ( ! $cache->check() ) {
982 // Cache our code.
983 $cache->cache( $code, 'text/css' );
984 }
985 $this->url[ $media ] = AUTOPTIMIZE_CACHE_URL . $cache->getname();
986 }
987 }
988
989 /**
990 * Returns the content.
991 */
992 public function getcontent()
993 {
994 // Restore the full content (only applies when "autoptimize_filter_css_justhead" filter is true).
995 if ( ! empty( $this->restofcontent ) ) {
996 $this->content .= $this->restofcontent;
997 $this->restofcontent = '';
998 }
999
1000 // type is not added by default.
1001 $type_css = '';
1002 if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
1003 $type_css = 'type="text/css" ';
1004 }
1005
1006 // Inject the new stylesheets, if possible after SEO stuff, but we need to
1007 // already restore script to be able to inject before ld+json instead of title
1008 // this should be safe here as all has been extracted already but behind a filter anyway.
1009 if ( $this->inline && true === apply_filters( 'autoptimize_filter_css_restore_js_early', true ) ) {
1010 $this->content = $this->restore_marked_content( 'SCRIPT', $this->content );
1011 }
1012 $_strpos_ldjson = strpos( $this->content, '<script type="application/ld+json"' );
1013 if ( false !== $_strpos_ldjson && $_strpos_ldjson < strpos( $this->content, '</head' ) ) {
1014 $replace_tag = array( '<script type="application/ld+json"', 'before' );
1015 } else {
1016 $replace_tag = array( '<title', 'before' );
1017 }
1018 $replace_tag = apply_filters( 'autoptimize_filter_css_replacetag', $replace_tag, $this->content );
1019
1020 if ( $this->inline ) {
1021 foreach ( $this->csscode as $media => $code ) {
1022 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $code . '</style>' ), $replace_tag );
1023 }
1024 } else {
1025 if ( $this->defer ) {
1026 $preload_css_block = '';
1027 $inlined_ccss_block = '';
1028 $noscript_css_block = '<noscript id="aonoscrcss">';
1029
1030 $defer_inline_code = $this->defer_inline;
1031 if ( ! empty( $defer_inline_code ) ) {
1032 if ( apply_filters( 'autoptimize_filter_css_critcss_minify', true ) ) {
1033 $icss_hash = md5( $defer_inline_code );
1034 $icss_cache = new autoptimizeCache( $icss_hash, 'css' );
1035 if ( $icss_cache->check() ) {
1036 // we have the optimized inline CSS in cache.
1037 $defer_inline_code = $icss_cache->retrieve();
1038 } else {
1039 $cssmin = new autoptimizeCSSmin();
1040 $tmp_code = trim( $cssmin->run( $defer_inline_code ) );
1041
1042 if ( ! empty( $tmp_code ) ) {
1043 $defer_inline_code = $tmp_code;
1044 $icss_cache->cache( $defer_inline_code, 'text/css' );
1045 unset( $tmp_code );
1046 }
1047 }
1048 }
1049 // inlined critical css set here, but injected when full CSS is injected
1050 // to avoid CSS containing SVG with <title tag receiving the full CSS link.
1051 $inlined_ccss_block = '<style ' . $type_css . 'id="aoatfcss" media="all">' . $defer_inline_code . '</style>';
1052 }
1053 }
1054
1055 foreach ( $this->url as $media => $url ) {
1056 $url = $this->url_replace_cdn( $url );
1057
1058 // Add the stylesheet either deferred (import at bottom) or normal links in head.
1059 if ( $this->defer && 'print' !== $media ) {
1060 $preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $media );
1061
1062 $preload_css_block .= apply_filters( 'autoptimize_filter_css_single_deferred_link', '<link rel="stylesheet" media="print" href="' . $url . '" onload="' . $preload_onload . '">' );
1063 if ( apply_filters( 'autoptimize_fitler_css_preload_and_print', false ) ) {
1064 $preload_css_block = '<link rel="preload" as="stylesheet" href="' . $url . '"/>' . $preload_css_block;
1065 }
1066 $noscript_css_block .= '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet">';
1067 } else {
1068 if ( strlen( $this->csscode[ $media ] ) > $this->cssinlinesize ) {
1069 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet">' ), $replace_tag );
1070 } elseif ( strlen( $this->csscode[ $media ] ) > 0 ) {
1071 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $this->csscode[ $media ] . '</style>' ), $replace_tag );
1072 }
1073 }
1074 }
1075
1076 if ( $this->defer ) {
1077 $noscript_css_block .= '</noscript>';
1078 // Inject inline critical CSS, the preloaded full CSS and the noscript-CSS.
1079 $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', $inlined_ccss_block . $preload_css_block . $noscript_css_block ), $replace_tag );
1080 }
1081 }
1082
1083 // restore comments.
1084 $this->content = $this->restore_comments( $this->content );
1085
1086 // restore IE hacks.
1087 $this->content = $this->restore_iehacks( $this->content );
1088
1089 // restore (no)script.
1090 $this->content = $this->restore_marked_content( 'SCRIPT', $this->content );
1091
1092 // Restore noptimize.
1093 $this->content = $this->restore_noptimize( $this->content );
1094
1095 // Return the modified stylesheet.
1096 return $this->content;
1097 }
1098
1099 /**
1100 * Make sure URL's are absolute iso relative to original CSS location.
1101 *
1102 * @param string $file filename of optimized CSS-file.
1103 * @param string $code CSS-code in which to fix URL's.
1104 */
1105 static function fixurls( $file, $code )
1106 {
1107 // Switch all imports to the url() syntax.
1108 $code = preg_replace( '#@import ("|\')(.+?)\.css.*?("|\')#', '@import url("${2}.css")', $code );
1109
1110 if ( preg_match_all( self::ASSETS_REGEX, $code, $matches ) ) {
1111 $file = str_replace( WP_ROOT_DIR, '/', $file );
1112 /**
1113 * Rollback as per https://github.com/futtta/autoptimize/issues/94
1114 * $file = str_replace( AUTOPTIMIZE_WP_CONTENT_NAME, '', $file );
1115 */
1116 $dir = dirname( $file ); // Like /themes/expound/css.
1117
1118 /**
1119 * $dir should not contain backslashes, since it's used to replace
1120 * urls, but it can contain them when running on Windows because
1121 * fixurls() is sometimes called with `ABSPATH . 'index.php'`
1122 */
1123 $dir = str_replace( '\\', '/', $dir );
1124 unset( $file ); // not used below at all.
1125
1126 $replace = array();
1127 foreach ( $matches[1] as $k => $url ) {
1128 // Remove quotes.
1129 $url = trim( $url, " \t\n\r\0\x0B\"'" );
1130 $no_q_url = trim( $url, "\"'" );
1131 if ( $url !== $no_q_url ) {
1132 $removed_quotes = true;
1133 } else {
1134 $removed_quotes = false;
1135 }
1136
1137 if ( '' === $no_q_url ) {
1138 continue;
1139 }
1140
1141 $url = $no_q_url;
1142 if ( '/' === $url[0] || preg_match( '#^(https?://|ftp://|data:)#i', $url ) ) {
1143 // URL is protocol-relative, host-relative or something we don't touch.
1144 continue;
1145 } else { // Relative URL.
1146
1147 /*
1148 * rollback as per https://github.com/futtta/autoptimize/issues/94
1149 * $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_CONTENT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1150 */
1151 $newurl = preg_replace( '/https?:/', '', str_replace( ' ', '%20', AUTOPTIMIZE_WP_ROOT_URL . str_replace( '//', '/', $dir . '/' . $url ) ) );
1152 $newurl = apply_filters( 'autoptimize_filter_css_fixurl_newurl', $newurl );
1153
1154 /**
1155 * Hash the url + whatever was behind potentially for replacement
1156 * We must do this, or different css classes referencing the same bg image (but
1157 * different parts of it, say, in sprites and such) loose their stuff...
1158 */
1159 $hash = md5( $url . $matches[2][ $k ] );
1160 $code = str_replace( $matches[0][ $k ], $hash, $code );
1161
1162 if ( $removed_quotes ) {
1163 $replace[ $hash ] = "url('" . $newurl . "')" . $matches[2][ $k ];
1164 } else {
1165 $replace[ $hash ] = 'url(' . $newurl . ')' . $matches[2][ $k ];
1166 }
1167 }
1168 }
1169
1170 $code = self::replace_longest_matches_first( $code, $replace );
1171 }
1172
1173 return $code;
1174 }
1175
1176 private function ismovable( $tag )
1177 {
1178 if ( ! $this->aggregate ) {
1179 return false;
1180 }
1181
1182 if ( ! empty( $this->allowlist ) ) {
1183 foreach ( $this->allowlist as $match ) {
1184 if ( false !== strpos( $tag, $match ) ) {
1185 return true;
1186 }
1187 }
1188 // no match with allowlist.
1189 return false;
1190 } else {
1191 if ( is_array( $this->dontmove ) && ! empty( $this->dontmove ) ) {
1192 foreach ( $this->dontmove as $match ) {
1193 if ( false !== strpos( $tag, $match ) ) {
1194 // Matched something.
1195 return false;
1196 }
1197 }
1198 }
1199
1200 // If we're here it's safe to move.
1201 return true;
1202 }
1203 }
1204
1205 private function can_inject_late( $css_path, $css )
1206 {
1207 $consider_minified_array = apply_filters( 'autoptimize_filter_css_consider_minified', false, $css_path );
1208 if ( true !== $this->inject_min_late ) {
1209 // late-inject turned off.
1210 return false;
1211 } elseif ( ( false === strpos( $css_path, 'min.css' ) ) && ( str_replace( $consider_minified_array, '', $css_path ) === $css_path ) ) {
1212 // file not minified based on filename & filter.
1213 return false;
1214 } elseif ( false !== strpos( $css, '@import' ) ) {
1215 // can't late-inject files with imports as those need to be aggregated.
1216 return false;
1217 } elseif ( ( false !== strpos( $css, '@font-face' ) ) && ( apply_filters( 'autoptimize_filter_css_fonts_cdn', false ) === true ) && ( ! empty( $this->cdn_url ) ) ) {
1218 // don't late-inject CSS with font-src's if fonts are set to be CDN'ed.
1219 return false;
1220 } elseif ( ( ( true == $this->datauris ) || ( ! empty( $this->cdn_url ) ) ) && preg_match( '#background[^;}]*url\(#Ui', $css ) ) {
1221 // don't late-inject CSS with images if CDN is set OR if image inlining is on.
1222 return false;
1223 } else {
1224 // phew, all is safe, we can late-inject.
1225 return true;
1226 }
1227 }
1228
1229 /**
1230 * Minifies (and cdn-replaces) a single local css file
1231 * and returns its (cached) url.
1232 *
1233 * @param string $filepath Filepath.
1234 * @param bool $cache_miss Optional. Force a cache miss. Default false.
1235 *
1236 * @return bool|string Url pointing to the minified css file or false.
1237 */
1238 public function minify_single( $filepath, $cache_miss = false )
1239 {
1240 $contents = $this->prepare_minify_single( $filepath );
1241
1242 if ( empty( $contents ) ) {
1243 // if aggregate is off and CCSS is used but all files are minified already, then we
1244 // must make sure the autoptimize_action_css_hash action still fires for CCSS's sake.
1245 $ao_ccss_key = get_option( 'autoptimize_ccss_key', '' );
1246 if ( false === $this->aggregate && isset( $ao_ccss_key ) && ! empty( $ao_ccss_key ) ) {
1247 $hash = 'single_' . md5( file_get_contents( $filepath ) );
1248 do_action( 'autoptimize_action_css_hash', $hash );
1249 }
1250 return false;
1251 }
1252
1253 // Check cache.
1254 $hash = 'single_' . md5( $contents );
1255 $cache = new autoptimizeCache( $hash, 'css' );
1256 do_action( 'autoptimize_action_css_hash', $hash );
1257
1258 // If not in cache already, minify...
1259 if ( ! $cache->check() || $cache_miss ) {
1260 // Fixurls...
1261 $contents = self::fixurls( $filepath, $contents );
1262 // CDN-replace any referenced assets if needed...
1263 $contents = $this->hide_fontface_and_maybe_cdn( $contents );
1264 $contents = $this->replace_urls( $contents );
1265 $contents = $this->restore_fontface( $contents );
1266 // Now minify...
1267 $cssmin = new autoptimizeCSSmin();
1268 $contents = trim( $cssmin->run( $contents ) );
1269
1270 // Check if minified cache content is empty.
1271 if ( empty( $contents ) ) {
1272 return false;
1273 }
1274
1275 // Filter contents of excluded minified CSS.
1276 $contents = apply_filters( 'autoptimize_filter_css_single_after_minify', $contents );
1277
1278 // Store in cache.
1279 $cache->cache( $contents, 'text/css' );
1280 }
1281
1282 $url = $this->build_minify_single_url( $cache );
1283
1284 return $url;
1285 }
1286
1287 /**
1288 * Returns whether we're doing aggregation or not.
1289 *
1290 * @return bool
1291 */
1292 public function aggregating()
1293 {
1294 return $this->aggregate;
1295 }
1296
1297 public function getOptions()
1298 {
1299 return $this->options;
1300 }
1301
1302 public function replaceOptions( $options )
1303 {
1304 $this->options = $options;
1305 }
1306
1307 public function setOption( $name, $value )
1308 {
1309 $this->options[ $name ] = $value;
1310 $this->$name = $value;
1311 }
1312
1313 public function getOption( $name )
1314 {
1315 return $this->options[ $name ];
1316 }
1317
1318 /**
1319 * Sanitize user-provided CSS.
1320 *
1321 * 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:
1322 * 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
1323 * https://github.com/twigphp/Twig/blob/3.x/src/Extension/EscaperExtension.php#L300-L319
1324 * https://github.com/laminas/laminas-escaper/blob/2.8.x/src/Escaper.php#L205-L221
1325 *
1326 * @param string $css the to be sanitized CSS.
1327 * @return string sanitized CSS.
1328 */
1329 public static function sanitize_css( $css )
1330 {
1331 $css = wp_strip_all_tags( $css );
1332 if ( strpos( $css, '<' ) !== false ) {
1333 $css = preg_replace( '#<(\/?\w+)#', '\00003C$1', $css );
1334 }
1335 return $css;
1336 }
1337 }
1338