PluginProbe
Autoptimize / trunk
Autoptimize vtrunk
3.1.16 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 All 108 releases
← All changes | classes/autoptimizeStyles.php +120 -53 2.7.3 → trunk View file →
@@ -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 . '" />';
1022 - $noscript_css_block .= '<link ' . $type_css . 'media="' . $media . '" href="' . $url . '" rel="stylesheet" />';
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_filter_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">';
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 }