diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 961ead0b663..f99ad2bceb2 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -133,6 +133,14 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { */ private $used_class_names = array(); + /** + * Tag names used in document. + * + * @since 1.0 + * @var array + */ + private $used_tag_names = array(); + /** * XPath. * @@ -245,6 +253,24 @@ private function get_used_class_names() { return $this->used_class_names; } + + /** + * Get list of all the tag names used in the document. + * + * @since 1.0 + * @return array Used tag names. + */ + private function get_used_tag_names() { + if ( empty( $this->used_tag_names ) ) { + $used_tag_names = array(); + foreach ( $this->dom->getElementsByTagName( '*' ) as $el ) { + $used_tag_names[ $el->tagName ] = true; + } + $this->used_tag_names = array_keys( $used_tag_names ); + } + return $this->used_tag_names; + } + /** * Sanitize CSS styles within the HTML contained in this instance's DOMDocument. * @@ -531,7 +557,7 @@ private function process_stylesheet( $stylesheet, $node, $options = array() ) { $cache_key = md5( $stylesheet . serialize( $cache_impacting_options ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize - $cache_group = 'amp-parsed-stylesheet-v2'; + $cache_group = 'amp-parsed-stylesheet-v4'; if ( wp_using_ext_object_cache() ) { $parsed = wp_cache_get( $cache_key, $cache_group ); } else { @@ -614,6 +640,7 @@ function ( $value ) { $validation_errors = $this->process_css_list( $css_document, $options ); $output_format = Sabberworm\CSS\OutputFormat::createCompact(); + $output_format->setSemicolonAfterLastRule( false ); $before_declaration_block = '/*AMP_WP_BEFORE_DECLARATION_BLOCK*/'; $between_selectors = '/*AMP_WP_BETWEEN_SELECTORS*/'; @@ -644,18 +671,34 @@ function ( $value ) { $selectors_parsed = array(); foreach ( $selectors as $selector ) { - $classes = array(); + $selectors_parsed[ $selector ] = array(); - // Remove :not() to eliminate false negatives, such as with `body:not(.title-tagline-hidden) .site-branding-text`. - $reduced_selector = preg_replace( '/:not\(.+?\)/', '', $selector ); + // Remove :not() and pseudo selectors to eliminate false negatives, such as with `body:not(.title-tagline-hidden) .site-branding-text`. + $reduced_selector = preg_replace( '/:[a-zA-Z0-9_-]+(\(.+?\))?/', '', $selector ); // Remove attribute selectors to eliminate false negative, such as with `.social-navigation a[href*="example.com"]:before`. $reduced_selector = preg_replace( '/\[\w.*?\]/', '', $reduced_selector ); - if ( preg_match_all( '/(?<=\.)([a-zA-Z0-9_-]+)/', $reduced_selector, $matches ) ) { - $classes = $matches[0]; + $reduced_selector = preg_replace_callback( + '/\.([a-zA-Z0-9_-]+)/', + function( $matches ) use ( $selector, &$selectors_parsed ) { + $selectors_parsed[ $selector ]['classes'][] = $matches[1]; + return ''; + }, + $reduced_selector + ); + $reduced_selector = preg_replace_callback( + '/#([a-zA-Z0-9_-]+)/', + function( $matches ) use ( $selector, &$selectors_parsed ) { + $selectors_parsed[ $selector ]['ids'][] = $matches[1]; + return ''; + }, + $reduced_selector + ); + + if ( preg_match_all( '/[a-zA-Z0-9_-]+/', $reduced_selector, $matches ) ) { + $selectors_parsed[ $selector ]['tags'] = $matches[0]; } - $selectors_parsed[ $selector ] = $classes; } // Restore calc() functions that were replaced with placeholders. @@ -1403,6 +1446,7 @@ function( $selector ) { $stylesheet_set['processed_nodes'] = array(); $final_size = 0; + $dom = $this->dom; foreach ( $stylesheet_set['pending_stylesheets'] as &$pending_stylesheet ) { $stylesheet = ''; foreach ( $pending_stylesheet['stylesheet'] as $stylesheet_part ) { @@ -1412,12 +1456,34 @@ function( $selector ) { list( $selectors_parsed, $declaration_block ) = $stylesheet_part; if ( $should_tree_shake ) { $selectors = array(); - foreach ( $selectors_parsed as $selector => $class_names ) { + foreach ( $selectors_parsed as $selector => $parsed_selector ) { $should_include = ( ( $dynamic_selector_pattern && preg_match( $dynamic_selector_pattern, $selector ) ) || - // If all class names are used in the doc. - 0 === count( array_diff( $class_names, $this->get_used_class_names() ) ) + ( + // If all class names are used in the doc. + ( + empty( $parsed_selector['classes'] ) + || + 0 === count( array_diff( $parsed_selector['classes'], $this->get_used_class_names() ) ) + ) + && + // If all IDs are used in the doc. + ( + empty( $parsed_selector['ids'] ) + || + 0 === count( array_filter( $parsed_selector['ids'], function( $id ) use ( $dom ) { + return ! $dom->getElementById( $id ); + } ) ) + ) + && + // If tag names are present in the doc. + ( + empty( $parsed_selector['tags'] ) + || + 0 === count( array_diff( $parsed_selector['tags'], $this->get_used_tag_names() ) ) + ) + ) ); if ( $should_include ) { $selectors[] = $selector; diff --git a/tests/test-amp-style-sanitizer.php b/tests/test-amp-style-sanitizer.php index d5f82cd0d5e..7726fba5a8d 100644 --- a/tests/test-amp-style-sanitizer.php +++ b/tests/test-amp-style-sanitizer.php @@ -29,7 +29,7 @@ public function get_body_style_attribute_data() { 'This is green.', 'This is green.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0}', ), ), @@ -37,7 +37,7 @@ public function get_body_style_attribute_data() { 'This is green.', 'This is green.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-0837823{color:#0f0;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-0837823{color:#0f0}', ), ), @@ -45,7 +45,7 @@ public function get_body_style_attribute_data() { 'This is green.', 'This is green.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-c71affe{color:#0f0;background-color:#000;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-c71affe{color:#0f0;background-color:#000}', ), ), @@ -53,7 +53,7 @@ public function get_body_style_attribute_data() { ' ', 'Kses-banned properties are allowed since Kses will have already applied if user does not have unfiltered_html.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-224b51a{display:none;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-224b51a{display:none}', ), ), @@ -61,7 +61,7 @@ public function get_body_style_attribute_data() { '!important is converted.', '!important is converted.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{padding:1px;outline:3px;}:root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{margin:2px;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{padding:1px;outline:3px}:root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-6a75598{margin:2px}', ), ), @@ -69,7 +69,7 @@ public function get_body_style_attribute_data() { '!important is converted.', '!important is converted.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-952600b{color:red;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-952600b{color:red}', ), ), @@ -77,7 +77,7 @@ public function get_body_style_attribute_data() { '!important is converted.', '!important is converted.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-1e2bfaa{color:red;background:blue;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-1e2bfaa{color:red;background:blue}', ), ), @@ -85,8 +85,8 @@ public function get_body_style_attribute_data() { 'This is red.', 'This is red.', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0;}', - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cc68ddc{color:#f00;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-bb01159{color:#0f0}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-cc68ddc{color:#f00}', ), ), @@ -94,7 +94,7 @@ public function get_body_style_attribute_data() { '', '', array( - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-2864855{background:#000;}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-2864855{background:#000}', ), ), @@ -102,7 +102,7 @@ public function get_body_style_attribute_data() { '
Hello
', array( - 'body.foo:not(.bar) > p{color:blue;}body.foo:not(.bar) p:not(.baz){color:green;}body.foo p{color:yellow;}', + 'body.foo:not(.bar) > p{color:blue}body.foo:not(.bar) p:not(.baz){color:green}body.foo p{color:yellow}', ), array(), ), 'style_with_attribute_selectors' => array( '', array( - '.social-navigation a[href*="example.com"]{color:red;}', + '.social-navigation a[href*="example.com"]{color:red}', ), array(), ), 'style_on_root_element' => array( 'Hi', array( - 'html:not(#_):not(#_){background-color:blue;}', - ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-10b06ba{color:red;}', + 'html:not(#_):not(#_){background-color:blue}', + ':root:not(#_):not(#_):not(#_):not(#_):not(#_) .amp-wp-10b06ba{color:red}', ), array(), ), @@ -286,9 +286,9 @@ public function get_link_and_style_test_data() { '