diff --git a/includes/sanitizers/class-amp-meta-sanitizer.php b/includes/sanitizers/class-amp-meta-sanitizer.php index 57868edeec6..55207f0a7e9 100644 --- a/includes/sanitizers/class-amp-meta-sanitizer.php +++ b/includes/sanitizers/class-amp-meta-sanitizer.php @@ -34,6 +34,7 @@ class AMP_Meta_Sanitizer extends AMP_Base_Sanitizer { * Tags array keys. */ const TAG_CHARSET = 'charset'; + const TAG_HTTP_EQUIV = 'http-equiv'; const TAG_VIEWPORT = 'viewport'; const TAG_AMP_SCRIPT_SRC = 'amp_script_src'; const TAG_OTHER = 'other'; @@ -54,6 +55,7 @@ class AMP_Meta_Sanitizer extends AMP_Base_Sanitizer { */ protected $meta_tags = [ self::TAG_CHARSET => [], + self::TAG_HTTP_EQUIV => [], self::TAG_VIEWPORT => [], self::TAG_AMP_SCRIPT_SRC => [], self::TAG_OTHER => [], @@ -66,19 +68,41 @@ class AMP_Meta_Sanitizer extends AMP_Base_Sanitizer { */ const AMP_VIEWPORT = 'width=device-width'; + /** + * Spec name for the tag spec for meta elements that are allowed in the body. + * + * @since 1.5.2 + * @var string + */ + const BODY_ANCESTOR_META_TAG_SPEC_NAME = 'meta name= and content='; + + /** + * Get tag spec for meta tags which are allowed in the body. + * + * @since 1.5.2 + * @return string Deny pattern. + */ + private function get_body_meta_tag_name_attribute_deny_pattern() { + static $pattern = null; + if ( null === $pattern ) { + $tag_spec = current( + array_filter( + AMP_Allowed_Tags_Generated::get_allowed_tag( 'meta' ), + static function ( $spec ) { + return isset( $spec['tag_spec']['spec_name'] ) && self::BODY_ANCESTOR_META_TAG_SPEC_NAME === $spec['tag_spec']['spec_name']; + } + ) + ); + $pattern = '/' . $tag_spec['attr_spec_list']['name']['blacklisted_value_regex'] . '/'; + } + return $pattern; + } + /** * Sanitize. */ public function sanitize() { - $meta_elements = $this->dom->getElementsByTagName( static::$tag ); - - // Remove all nodes for easy reordering later on. - $meta_elements = array_map( - static function ( $element ) { - return $element->parentNode->removeChild( $element ); - }, - iterator_to_array( $meta_elements, false ) - ); + $meta_elements = iterator_to_array( $this->dom->getElementsByTagName( static::$tag ), false ); foreach ( $meta_elements as $meta_element ) { @@ -96,13 +120,19 @@ static function ( $element ) { * @var DOMElement $meta_element */ if ( $meta_element->hasAttribute( Attribute::CHARSET ) ) { - $this->meta_tags[ self::TAG_CHARSET ][] = $meta_element; + $this->meta_tags[ self::TAG_CHARSET ][] = $meta_element->parentNode->removeChild( $meta_element ); + } elseif ( $meta_element->hasAttribute( Attribute::HTTP_EQUIV ) ) { + $this->meta_tags[ self::TAG_HTTP_EQUIV ][] = $meta_element->parentNode->removeChild( $meta_element ); } elseif ( Attribute::VIEWPORT === $meta_element->getAttribute( Attribute::NAME ) ) { - $this->meta_tags[ self::TAG_VIEWPORT ][] = $meta_element; + $this->meta_tags[ self::TAG_VIEWPORT ][] = $meta_element->parentNode->removeChild( $meta_element ); } elseif ( Attribute::AMP_SCRIPT_SRC === $meta_element->getAttribute( Attribute::NAME ) ) { - $this->meta_tags[ self::TAG_AMP_SCRIPT_SRC ][] = $meta_element; - } else { - $this->meta_tags[ self::TAG_OTHER ][] = $meta_element; + $this->meta_tags[ self::TAG_AMP_SCRIPT_SRC ][] = $meta_element->parentNode->removeChild( $meta_element ); + } elseif ( + $meta_element->hasAttribute( 'name' ) + && + preg_match( $this->get_body_meta_tag_name_attribute_deny_pattern(), $meta_element->getAttribute( 'name' ) ) + ) { + $this->meta_tags[ self::TAG_OTHER ][] = $meta_element->parentNode->removeChild( $meta_element ); } } diff --git a/tests/php/test-class-amp-meta-sanitizer.php b/tests/php/test-class-amp-meta-sanitizer.php index 8cba767ce2c..fbbc0f86e78 100644 --- a/tests/php/test-class-amp-meta-sanitizer.php +++ b/tests/php/test-class-amp-meta-sanitizer.php @@ -12,6 +12,37 @@ */ class Test_AMP_Meta_Sanitizer extends WP_UnitTestCase { + /** + * Test that the expected tag specs exist for the body. + */ + public function test_expected_meta_tags() { + $named_specs = array_filter( + AMP_Allowed_Tags_Generated::get_allowed_tag( 'meta' ), + static function ( $spec ) { + return isset( $spec['tag_spec']['spec_name'] ) && AMP_Meta_Sanitizer::BODY_ANCESTOR_META_TAG_SPEC_NAME === $spec['tag_spec']['spec_name']; + } + ); + $this->assertCount( 1, $named_specs ); + + $body_ok_specs = array_filter( + AMP_Allowed_Tags_Generated::get_allowed_tag( 'meta' ), + static function ( $spec ) { + $head_required = ( + ( isset( $spec['tag_spec']['mandatory_parent'] ) && 'head' === $spec['tag_spec']['mandatory_parent'] ) + || + ( isset( $spec['tag_spec']['mandatory_ancestor'] ) && 'head' === $spec['tag_spec']['mandatory_ancestor'] ) + ); + return ! $head_required; + } + ); + + $this->assertEquals( $named_specs, $body_ok_specs ); + + $spec = current( $named_specs ); + $this->assertArrayHasKey( 'name', $spec['attr_spec_list'] ); + $this->assertEquals( [ 'blacklisted_value_regex' ], array_keys( $spec['attr_spec_list']['name'] ) ); + } + /** * Provide data to the test_sanitize method. * @@ -30,7 +61,42 @@ public function get_data_for_sanitize() { $amp_boilerplate = amp_get_boilerplate_code(); - return [ + $meta_charset = ''; + $meta_viewport = ''; + + $meta_tags_allowed_in_body = ' + + + + + + + + + + + + + + + + +
+

Name: Amanda

+
+
+

Band: Jazz Band

+

Size: 12 players

+
+ + + + + '; + + $data = [ 'Do not break the correct charset tag' => [ '' . $amp_boilerplate . '', '' . $amp_boilerplate . '', @@ -65,7 +131,79 @@ public function get_data_for_sanitize() { '' . $amp_boilerplate . '', '' . $amp_boilerplate . '', ], + + 'Remove legacy meta http-equiv=Content-Type' => [ + '' . $amp_boilerplate . '', + '' . $amp_boilerplate . '', + ], + + 'Process invalid meta http-equiv value' => [ + // Note the AMP_Tag_And_Attribute_Sanitizer removes the http-equiv attribute because the content is invalid. + '' . $amp_boilerplate . '', + '' . $amp_boilerplate . '', + ], + + 'Disallowed meta=content-deposition' => [ + '' . $amp_boilerplate . '', + '' . $amp_boilerplate . '', + ], + + 'Disallowed meta=revisit-after' => [ + '' . $amp_boilerplate . '', + '' . $amp_boilerplate . '', + ], + + 'Disallowed meta=amp-bogus' => [ + '' . $amp_boilerplate . '', + '' . $amp_boilerplate . '', + ], + + 'Ignore generic meta tags' => [ + '' . $amp_boilerplate . '' . $meta_tags_allowed_in_body . '', + '' . $amp_boilerplate . '' . $meta_tags_allowed_in_body . '', + ], + ]; + + $http_equiv_specs = [ + 'meta http-equiv=X-UA-Compatible' => '', + 'meta http-equiv=content-language' => '', + 'meta http-equiv=pics-label' => '', + 'meta http-equiv=imagetoolbar' => '', + 'meta http-equiv=Content-Style-Type' => '', + 'meta http-equiv=Content-Script-Type' => '', + 'meta http-equiv=origin-trial' => '', + 'meta http-equiv=resource-type' => '', + 'meta http-equiv=x-dns-prefetch-control' => '', + ]; + foreach ( $http_equiv_specs as $equiv_spec => $tag ) { + $data[ "Verify http-equiv moved: $equiv_spec" ] = [ + "{$meta_charset}{$meta_viewport}{$amp_boilerplate}{$tag}", + "{$meta_charset}{$tag}{$meta_viewport}{$amp_boilerplate}", + ]; + } + + $named_specs = [ + 'meta name=apple-itunes-app' => '', + 'meta name=amp-experiments-opt-in' => '', + 'meta name=amp-3p-iframe-src' => '', + 'meta name=amp-consent-blocking' => '', + 'meta name=amp-experiment-token' => '', + 'meta name=amp-link-variable-allowed-origin' => '', + 'meta name=amp-google-clientid-id-api' => '', + 'meta name=amp-ad-doubleclick-sra' => '', + 'meta name=amp-list-load-more' => '', + 'meta name=amp-recaptcha-input' => '', + 'meta name=amp-ad-enable-refresh' => '', + 'meta name=amp-to-amp-navigation' => '', ]; + foreach ( $named_specs as $named_spec => $tag ) { + $data[ "Verify meta[name] moved: $named_spec" ] = [ + "{$meta_charset}{$meta_viewport}{$amp_boilerplate}{$tag}", + "{$meta_charset}{$meta_viewport}{$tag}{$amp_boilerplate}", + ]; + } + + return $data; } /** @@ -82,7 +220,10 @@ public function test_sanitize( $source_content, $expected_content ) { $sanitizer = new AMP_Meta_Sanitizer( $dom ); $sanitizer->sanitize(); - $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom ); + $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( + $dom, + [ 'use_document_element' => true ] + ); $sanitizer->sanitize(); $this->assertEqualMarkup( $expected_content, $dom->saveHTML() ); diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 9acc2491ead..48dd2c2d1bc 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -2975,6 +2975,12 @@ public function get_html_data() { null, [ 'amp-subscriptions' ], ], + 'bad http-equiv meta tag' => [ + '', + '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], + ], ]; $bad_dev_mode_document = sprintf(