| @@ -93,13 +93,13 @@ | ||
| 93 | 93 | */ |
| 94 | 94 | private $defer_inline = ''; |
| 95 | 95 | |
| 96 | 96 | /** |
| 97 | - * Setting for whitelist of what should be aggregated. | |
| 97 | + * Setting for allowlist of what should be aggregated. | |
| 98 | 98 | * |
| 99 | 99 | * @var string |
| 100 | 100 | */ |
| 101 | - private $whitelist = ''; | |
| 101 | + private $allowlist = ''; | |
| 102 | 102 | |
| 103 | 103 | /** |
| 104 | 104 | * Setting (only filter) for size under which CSS should be inlined instead of linked. |
| 105 | 105 | * |
| @@ -163,15 +163,15 @@ | ||
| 163 | 163 | */ |
| 164 | 164 | public function read( $options ) |
| 165 | 165 | { |
| 166 | 166 | $noptimize_css = apply_filters( 'autoptimize_filter_css_noptimize', false, $this->content ); |
| 167 | - if ( $noptimize_css ) { | |
| 167 | + if ( $noptimize_css || false === autoptimizeConfig::get_post_meta_ao_settings( 'ao_post_css_optimize' ) ) { | |
| 168 | 168 | return false; |
| 169 | 169 | } |
| 170 | 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 ) ) ); | |
| 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 | 174 | } |
| 175 | 175 | |
| 176 | 176 | $removable_css = apply_filters( 'autoptimize_filter_css_removables', '' ); |
| 177 | 177 | if ( ! empty( $removable_css ) ) { |
| @@ -197,8 +197,10 @@ | ||
| 197 | 197 | // Returning true for "dontaggregate" turns off aggregation. |
| 198 | 198 | if ( $this->aggregate && apply_filters( 'autoptimize_filter_css_dontaggregate', false ) ) { |
| 199 | 199 | $this->aggregate = false; |
| 200 | 200 | } |
| 201 | + // and the filter that should have been there to begin with. | |
| 202 | + $this->aggregate = apply_filters( 'autoptimize_filter_css_aggregate', $this->aggregate ); | |
| 201 | 203 | |
| 202 | 204 | // include inline? |
| 203 | 205 | if ( apply_filters( 'autoptimize_css_include_inline', $options['include_inline'] ) ) { |
| 204 | 206 | $this->include_inline = true; |
| @@ -214,16 +216,24 @@ | ||
| 214 | 216 | |
| 215 | 217 | // forcefully exclude CSS with data-noptimize attrib. |
| 216 | 218 | $this->dontmove[] = 'data-noptimize'; |
| 217 | 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 | + | |
| 218 | 223 | // Should we defer css? |
| 219 | 224 | // value: true / false. |
| 220 | 225 | $this->defer = $options['defer']; |
| 221 | 226 | $this->defer = apply_filters( 'autoptimize_filter_css_defer', $this->defer, $this->content ); |
| 222 | 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 | + | |
| 223 | 233 | // Should we inline while deferring? |
| 224 | 234 | // value: inlined CSS. |
| 225 | - $this->defer_inline = apply_filters( 'autoptimize_filter_css_defer_inline', $options['defer_inline'], $this->content ); | |
| 235 | + $this->defer_inline = apply_filters( 'autoptimize_filter_css_defer_inline', $this->sanitize_css( $options['defer_inline'] ), $this->content ); | |
| 226 | 236 | |
| 227 | 237 | // Should we inline? |
| 228 | 238 | // value: true / false. |
| 229 | 239 | $this->inline = $options['inline']; |
| @@ -270,16 +280,20 @@ | ||
| 270 | 280 | } elseif ( $this->ismovable( $tag ) ) { |
| 271 | 281 | // Get the media. |
| 272 | 282 | if ( false !== strpos( $tag, 'media=' ) ) { |
| 273 | 283 | 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'; | |
| 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; | |
| 279 | 293 | } |
| 280 | - | |
| 281 | - $media[] = $elem; | |
| 294 | + } else { | |
| 295 | + $media = array( 'all' ); | |
| 282 | 296 | } |
| 283 | 297 | } else { |
| 284 | 298 | // No media specified - applies to all. |
| 285 | 299 | $media = array( 'all' ); |
| @@ -353,8 +367,13 @@ | ||
| 353 | 367 | |
| 354 | 368 | if ( '' !== $new_tag ) { |
| 355 | 369 | // Optionally defer (preload) non-aggregated CSS. |
| 356 | 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 | + } | |
| 357 | 376 | } |
| 358 | 377 | |
| 359 | 378 | // And replace! |
| 360 | 379 | if ( ( '' !== $new_tag && $new_tag !== $tag ) || ( '' === $new_tag && apply_filters( 'autoptimize_filter_css_remove_empty_files', false ) ) ) { |
| @@ -382,25 +401,33 @@ | ||
| 382 | 401 | private function optionally_defer_excluded( $tag, $url = '' ) |
| 383 | 402 | { |
| 384 | 403 | // Defer single CSS if "inline & defer" is ON and there is inline CSS. |
| 385 | 404 | if ( ! empty( $tag ) && false === strpos( $tag, ' onload=' ) && $this->defer && ! empty( $this->defer_inline ) && apply_filters( 'autoptimize_filter_css_defer_excluded', true, $tag ) ) { |
| 386 | - // Get/ set (via filter) the JS to be triggers onload of the preloaded CSS. | |
| 387 | - $_preload_onload = apply_filters( | |
| 388 | - 'autoptimize_filter_css_preload_onload', | |
| 389 | - "this.onload=null;this.rel='stylesheet'", | |
| 390 | - $url | |
| 391 | - ); | |
| 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 | + } | |
| 392 | 409 | |
| 393 | - // Adapt original <link> element for CSS to be preloaded and add <noscript>-version for fallback. | |
| 394 | - $new_tag = '<noscript>' . autoptimizeUtils::remove_id_from_node( $tag ) . '</noscript>' . str_replace( | |
| 395 | - array( | |
| 396 | - "rel='stylesheet'", | |
| 397 | - 'rel="stylesheet"', | |
| 398 | - ), | |
| 399 | - "rel='preload' as='style' onload=\"" . $_preload_onload . '"', | |
| 400 | - $tag | |
| 401 | - ); | |
| 410 | + preg_match( '#media=(?:"|\')([^>]*)(?:"|\')#Ui', $tag, $_medias ); | |
| 411 | + $_media = $_medias[1]; | |
| 412 | + $_preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $_media ); | |
| 402 | 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 | + | |
| 403 | 430 | return $new_tag; |
| 404 | 431 | } |
| 405 | 432 | |
| 406 | 433 | // Return unchanged $tag. |
| @@ -463,9 +490,9 @@ | ||
| 463 | 490 | |
| 464 | 491 | private function check_datauri_exclude_list( $url ) |
| 465 | 492 | { |
| 466 | 493 | static $exclude_list = null; |
| 467 | - $no_datauris = array(); | |
| 494 | + static $no_datauris = array(); | |
| 468 | 495 | |
| 469 | 496 | // Again, skip doing certain stuff repeatedly when loop-called. |
| 470 | 497 | if ( null === $exclude_list ) { |
| 471 | 498 | $exclude_list = apply_filters( 'autoptimize_filter_css_datauri_exclude', '' ); |
| @@ -611,9 +638,11 @@ | ||
| 611 | 638 | if ( ! empty( $this->cdn_url ) ) { |
| 612 | 639 | $replacement_url = $this->url_replace_cdn( $url ); |
| 613 | 640 | // Prepare replacements array. |
| 614 | 641 | $replacements[ $url_src_matches[1][ $count ] ] = str_replace( |
| 615 | - $original_url, $replacement_url, $url_src_matches[1][ $count ] | |
| 642 | + $original_url, | |
| 643 | + $replacement_url, | |
| 644 | + $url_src_matches[1][ $count ] | |
| 616 | 645 | ); |
| 617 | 646 | } |
| 618 | 647 | } |
| 619 | 648 | } |
| @@ -737,9 +766,11 @@ | ||
| 737 | 766 | if ( ! $inlined && ( ! empty( $this->cdn_url ) || has_filter( 'autoptimize_filter_base_replace_cdn' ) ) ) { |
| 738 | 767 | // Just do the "simple" CDN replacement. |
| 739 | 768 | $replacement_url = $this->url_replace_cdn( $url ); |
| 740 | 769 | $imgreplace[ $url_src_matches[1][ $count ] ] = str_replace( |
| 741 | - $original_url, $replacement_url, $url_src_matches[1][ $count ] | |
| 770 | + $original_url, | |
| 771 | + $replacement_url, | |
| 772 | + $url_src_matches[1][ $count ] | |
| 742 | 773 | ); |
| 743 | 774 | } |
| 744 | 775 | } |
| 745 | 776 | } |
| @@ -845,9 +876,9 @@ | ||
| 845 | 876 | $code = self::build_injectlater_marker( $path, md5( $code ) ); |
| 846 | 877 | } |
| 847 | 878 | |
| 848 | 879 | if ( ! empty( $code ) ) { |
| 849 | - $tmp_thiscss = preg_replace( '#(/\*FILESTART\*/.*)' . preg_quote( $import, '#' ) . '#Us', '/*FILESTART2*/' . $code . '$1', $thiscss ); | |
| 880 | + $tmp_thiscss = str_replace( $import, stripcslashes( $code ), $thiscss ); | |
| 850 | 881 | if ( ! empty( $tmp_thiscss ) ) { |
| 851 | 882 | $thiscss = $tmp_thiscss; |
| 852 | 883 | $import_ok = true; |
| 853 | 884 | unset( $tmp_thiscss ); |
| @@ -971,15 +1002,25 @@ | ||
| 971 | 1002 | if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) { |
| 972 | 1003 | $type_css = 'type="text/css" '; |
| 973 | 1004 | } |
| 974 | 1005 | |
| 975 | - // Inject the new stylesheets. | |
| 976 | - $replace_tag = array( '<title', 'before' ); | |
| 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 | + } | |
| 977 | 1018 | $replace_tag = apply_filters( 'autoptimize_filter_css_replacetag', $replace_tag, $this->content ); |
| 978 | 1019 | |
| 979 | 1020 | if ( $this->inline ) { |
| 980 | 1021 | foreach ( $this->csscode as $media => $code ) { |
| 981 | - $this->inject_in_html( '<style ' . $type_css . 'media="' . $media . '">' . $code . '</style>', $replace_tag ); | |
| 1022 | + $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $code . '</style>' ), $replace_tag ); | |
| 982 | 1023 | } |
| 983 | 1024 | } else { |
| 984 | 1025 | if ( $this->defer ) { |
| 985 | 1026 | $preload_css_block = ''; |
| @@ -1014,33 +1055,29 @@ | ||
| 1014 | 1055 | foreach ( $this->url as $media => $url ) { |
| 1015 | 1056 | $url = $this->url_replace_cdn( $url ); |
| 1016 | 1057 | |
| 1017 | 1058 | // Add the stylesheet either deferred (import at bottom) or normal links in head. |
| 1018 | - if ( $this->defer ) { | |
| 1019 | - $preload_onload = autoptimizeConfig::get_ao_css_preload_onload(); | |
| 1059 | + if ( $this->defer && 'print' !== $media ) { | |
| 1060 | + $preload_onload = autoptimizeConfig::get_ao_css_preload_onload( $media ); | |
| 1020 | 1061 | |
| 1021 | - $preload_css_block .= '<link rel="preload" as="style" media="' . $media . '" href="' . $url . '" onload="' . $preload_onload . '" />'; | |
| 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 | + } | |
| 1022 | 1066 | $noscript_css_block .= '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />'; |
| 1023 | 1067 | } else { |
| 1024 | 1068 | if ( strlen( $this->csscode[ $media ] ) > $this->cssinlinesize ) { |
| 1025 | - $this->inject_in_html( '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />', $replace_tag ); | |
| 1069 | + $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />' ), $replace_tag ); | |
| 1026 | 1070 | } elseif ( strlen( $this->csscode[ $media ] ) > 0 ) { |
| 1027 | - $this->inject_in_html( '<style ' . $type_css . 'media="' . $media . '">' . $this->csscode[ $media ] . '</style>', $replace_tag ); | |
| 1071 | + $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', '<style ' . $type_css . 'media="' . $media . '">' . $this->csscode[ $media ] . '</style>' ), $replace_tag ); | |
| 1028 | 1072 | } |
| 1029 | 1073 | } |
| 1030 | 1074 | } |
| 1031 | 1075 | |
| 1032 | 1076 | if ( $this->defer ) { |
| 1033 | - $preload_polyfill = autoptimizeConfig::get_ao_css_preload_polyfill(); | |
| 1034 | 1077 | $noscript_css_block .= '</noscript>'; |
| 1035 | 1078 | // Inject inline critical CSS, the preloaded full CSS and the noscript-CSS. |
| 1036 | - $this->inject_in_html( $inlined_ccss_block . $preload_css_block . $noscript_css_block, $replace_tag ); | |
| 1037 | - | |
| 1038 | - // Adds preload polyfill at end of body tag. | |
| 1039 | - $this->inject_in_html( | |
| 1040 | - apply_filters( 'autoptimize_css_preload_polyfill', $preload_polyfill ), | |
| 1041 | - apply_filters( 'autoptimize_css_preload_polyfill_injectat', array( '</body>', 'before' ) ) | |
| 1042 | - ); | |
| 1079 | + $this->inject_in_html( apply_filters( 'autoptimize_filter_css_bodyreplacementpayload', $inlined_ccss_block . $preload_css_block . $noscript_css_block ), $replace_tag ); | |
| 1043 | 1080 | } |
| 1044 | 1081 | } |
| 1045 | 1082 | |
| 1046 | 1083 | // restore comments. |
| @@ -1141,15 +1178,15 @@ | ||
| 1141 | 1178 | if ( ! $this->aggregate ) { |
| 1142 | 1179 | return false; |
| 1143 | 1180 | } |
| 1144 | 1181 | |
| 1145 | - if ( ! empty( $this->whitelist ) ) { | |
| 1146 | - foreach ( $this->whitelist as $match ) { | |
| 1182 | + if ( ! empty( $this->allowlist ) ) { | |
| 1183 | + foreach ( $this->allowlist as $match ) { | |
| 1147 | 1184 | if ( false !== strpos( $tag, $match ) ) { |
| 1148 | 1185 | return true; |
| 1149 | 1186 | } |
| 1150 | 1187 | } |
| 1151 | - // no match with whitelist. | |
| 1188 | + // no match with allowlist. | |
| 1152 | 1189 | return false; |
| 1153 | 1190 | } else { |
| 1154 | 1191 | if ( is_array( $this->dontmove ) && ! empty( $this->dontmove ) ) { |
| 1155 | 1192 | foreach ( $this->dontmove as $match ) { |
| @@ -1202,8 +1239,15 @@ | ||
| 1202 | 1239 | { |
| 1203 | 1240 | $contents = $this->prepare_minify_single( $filepath ); |
| 1204 | 1241 | |
| 1205 | 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 | + } | |
| 1206 | 1250 | return false; |
| 1207 | 1251 | } |
| 1208 | 1252 | |
| 1209 | 1253 | // Check cache. |
| @@ -1227,8 +1271,11 @@ | ||
| 1227 | 1271 | if ( empty( $contents ) ) { |
| 1228 | 1272 | return false; |
| 1229 | 1273 | } |
| 1230 | 1274 | |
| 1275 | + // Filter contents of excluded minified CSS. | |
| 1276 | + $contents = apply_filters( 'autoptimize_filter_css_single_after_minify', $contents ); | |
| 1277 | + | |
| 1231 | 1278 | // Store in cache. |
| 1232 | 1279 | $cache->cache( $contents, 'text/css' ); |
| 1233 | 1280 | } |
| 1234 | 1281 | |
| @@ -1265,6 +1312,26 @@ | ||
| 1265 | 1312 | |
| 1266 | 1313 | public function getOption( $name ) |
| 1267 | 1314 | { |
| 1268 | 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; | |
| 1269 | 1336 | } |
| 1270 | 1337 | } |