From c54a02d6dc9f23025def8b0fde094d5c11815a84 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 8 Feb 2018 21:05:38 -0800 Subject: [PATCH 1/5] Refactor remove_invalid_callback to take single DOMNode arg, whether DOMElement or DOMAttr --- .../sanitizers/class-amp-base-sanitizer.php | 23 +++-- .../class-amp-tag-and-attribute-sanitizer.php | 5 +- includes/utils/class-amp-validation-utils.php | 94 ++++++++----------- tests/test-class-amp-base-sanitizer.php | 21 ++--- tests/test-class-amp-theme-support.php | 16 +++- tests/test-class-amp-validation-utils.php | 82 ++++++---------- 6 files changed, 102 insertions(+), 139 deletions(-) diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 8e7a936afb7..a0331d019e7 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -314,12 +314,12 @@ public function maybe_enforce_https_src( $src, $force_https = false ) { * @since 0.7 * * @param DOMElement $child The node to remove. - * @return void. + * @return void */ public function remove_invalid_child( $child ) { $child->parentNode->removeChild( $child ); if ( isset( $this->args['remove_invalid_callback'] ) ) { - call_user_func( $this->args['remove_invalid_callback'], $child, AMP_Validation_Utils::NODE_REMOVED ); + call_user_func( $this->args['remove_invalid_callback'], $child ); } } @@ -331,14 +331,23 @@ public function remove_invalid_child( $child ) { * * @since 0.7 * - * @param DOMElement $element The node for which to remove the attribute. - * @param string $attribute The attribute to remove from the node. - * @return void. + * @param DOMElement $element The node for which to remove the attribute. + * @param DOMAttr|string $attribute The attribute to remove from the element. + * @return void */ public function remove_invalid_attribute( $element, $attribute ) { - $element->removeAttribute( $attribute ); if ( isset( $this->args['remove_invalid_callback'] ) ) { - call_user_func( $this->args['remove_invalid_callback'], $element, AMP_Validation_Utils::ATTRIBUTE_REMOVED, $attribute ); + if ( is_string( $attribute ) ) { + $attribute = $element->getAttributeNode( $attribute ); + } + if ( $attribute ) { + $element->removeAttributeNode( $attribute ); + call_user_func( $this->args['remove_invalid_callback'], $attribute ); + } + } elseif ( is_string( $attribute ) ) { + $element->removeAttribute( $attribute ); + } else { + $element->removeAttributeNode( $attribute ); } } diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index b4c0faeee31..bdc9aa54e72 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -729,10 +729,7 @@ private function sanitize_disallowed_attributes_in_node( $node, $attr_spec_list } foreach ( $attrs_to_remove as $attr ) { - $node->removeAttributeNode( $attr ); - if ( isset( $this->args['remove_invalid_callback'], $attr->name ) ) { - call_user_func( $this->args['remove_invalid_callback'], $node, AMP_Validation_Utils::ATTRIBUTE_REMOVED, $attr->name ); - } + $this->remove_invalid_attribute( $node, $attr ); } } diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index c7450248f81..986665a4a4c 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -12,20 +12,6 @@ */ class AMP_Validation_Utils { - /** - * The argument if an attribute was removed. - * - * @var array. - */ - const ATTRIBUTE_REMOVED = 'removed_attr'; - - /** - * The argument if a node was removed. - * - * @var array. - */ - const NODE_REMOVED = 'removed'; - /** * Key for the markup value in the REST API endpoint. * @@ -68,24 +54,17 @@ class AMP_Validation_Utils { */ const ERROR_NONCE_ACTION = 'invalid_amp_message'; - /** - * The attributes that the sanitizer removed. - * - * @var array. - */ - public static $removed_attributes; - /** * The nodes that the sanitizer removed. * - * @var array. + * @var DOMNode[] */ - public static $removed_nodes; + public static $removed_nodes = array(); /** * Add the actions. * - * @return void. + * @return void */ public static function init() { add_action( 'rest_api_init', array( __CLASS__, 'amp_rest_validation' ) ); @@ -94,32 +73,13 @@ public static function init() { } /** - * Tracks when a sanitizer removes an attribute or node. + * Tracks when a sanitizer removes an node (element or attribute). * - * @param DOMNode|DOMElement $node The node in which there was a removal. - * @param string $removal_type The removal: 'removed_attr' for an attribute, or 'removed' for a node or element. - * @param string $attr_name The name of the attribute removed (optional). - * @return void. - */ - public static function track_removed( $node, $removal_type, $attr_name = null ) { - if ( ( self::ATTRIBUTE_REMOVED === $removal_type ) && isset( $attr_name ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar - self::$removed_attributes = self::increment_count( self::$removed_attributes, $attr_name ); - } elseif ( ( self::NODE_REMOVED === $removal_type ) && isset( $node->nodeName ) ) { - self::$removed_nodes = self::increment_count( self::$removed_nodes, $node->nodeName ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar - } - } - - /** - * Tracks when a sanitizer removes an attribute or node. - * - * @param array $histogram The count of attributes or nodes removed. - * @param string $key The attribute or node name removed. - * @return array $histogram The incremented histogram. + * @param DOMNode $node The node which was removed. + * @return void */ - public static function increment_count( $histogram, $key ) { - $current_value = isset( $histogram[ $key ] ) ? $histogram[ $key ] : 0; - $histogram[ $key ] = $current_value + 1; - return $histogram; + public static function track_removed( $node ) { + self::$removed_nodes[] = $node; } /** @@ -212,11 +172,34 @@ public static function get_response( $markup = null ) { self::process_markup( $markup ); $response['processed_markup'] = $markup; } - $response = array_merge( array( - self::ERROR_KEY => self::was_node_removed(), - 'removed_nodes' => self::$removed_nodes, - 'removed_attributes' => self::$removed_attributes, - ), $response ); + + $removed_elements = array(); + $removed_attributes = array(); + foreach ( self::$removed_nodes as $node ) { + if ( $node instanceof DOMAttr ) { + if ( ! isset( $removed_attributes[ $node->nodeName ] ) ) { + $removed_attributes[ $node->nodeName ] = 1; + } else { + $removed_attributes[ $node->nodeName ]++; + } + } elseif ( $node instanceof DOMElement ) { + if ( ! isset( $removed_elements[ $node->nodeName ] ) ) { + $removed_elements[ $node->nodeName ] = 1; + } else { + $removed_elements[ $node->nodeName ]++; + } + } + } + + $response = array_merge( + array( + self::ERROR_KEY => self::was_node_removed(), + ), + compact( + 'removed_elements', + 'removed_attributes' + ), + $response ); self::reset_removed(); return $response; @@ -232,8 +215,7 @@ public static function get_response( $markup = null ) { * @return void. */ public static function reset_removed() { - self::$removed_nodes = null; - self::$removed_attributes = null; + self::$removed_nodes = array(); } /** @@ -272,7 +254,7 @@ public static function validate_content( $post_id, $post ) { $location = AMP_Validation_Utils::error_message( $location ); $location = add_query_arg( array( - 'removed_elements' => array_keys( $response['removed_nodes'] ), + 'removed_elements' => array_keys( $response['removed_elements'] ), 'removed_attributes' => array_keys( $response['removed_attributes'] ), ), $location diff --git a/tests/test-class-amp-base-sanitizer.php b/tests/test-class-amp-base-sanitizer.php index a7c6bcd8f08..ef4bab3beb5 100644 --- a/tests/test-class-amp-base-sanitizer.php +++ b/tests/test-class-amp-base-sanitizer.php @@ -245,26 +245,23 @@ public function test_enforce_sizes_attribute( $source_params, $expected_value, $ /** * Tests remove_child. * - * @see AMP_Base_Sanitizer::remove_invalid_child() + * @covers AMP_Base_Sanitizer::remove_invalid_child() */ public function test_remove_child() { $parent_tag_name = 'div'; - $child_tag_name = 'h1'; $dom_document = new DOMDocument( '1.0', 'utf-8' ); $parent = $dom_document->createElement( $parent_tag_name ); $child = $dom_document->createElement( 'h1' ); $parent->appendChild( $child ); - // To ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar. - // @codingStandardsIgnoreStart - $this->assertEquals( $child, $parent->firstChild ); $sanitizer = new AMP_Iframe_Sanitizer( $dom_document, array( 'remove_invalid_callback' => 'AMP_Validation_Utils::track_removed', ) ); $sanitizer->remove_invalid_child( $child ); $this->assertEquals( null, $parent->firstChild ); - $this->assertEquals( 1, AMP_Validation_Utils::$removed_nodes[ $child_tag_name ] ); + $this->assertCount( 1, AMP_Validation_Utils::$removed_nodes ); + $this->assertEquals( $child, AMP_Validation_Utils::$removed_nodes[0] ); $parent->appendChild( $child ); $this->assertEquals( $child, $parent->firstChild ); @@ -272,14 +269,13 @@ public function test_remove_child() { $this->assertEquals( null, $parent->firstChild ); $this->assertEquals( null, $child->parentNode ); - // @codingStandardsIgnoreEnd AMP_Validation_Utils::$removed_nodes = null; } /** * Tests remove_child. * - * @see AMP_Base_Sanitizer::remove_invalid_child() + * @covers AMP_Base_Sanitizer::remove_invalid_child() */ public function test_remove_attribute() { AMP_Validation_Utils::reset_removed(); @@ -288,20 +284,15 @@ public function test_remove_attribute() { $dom_document = new DOMDocument( '1.0', 'utf-8' ); $video = $dom_document->createElement( $video_name ); $video->setAttribute( $attribute, 'someFunction()' ); + $attr_node = $video->getAttributeNode( $attribute ); - // To ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar. - // @codingStandardsIgnoreStart $args = array( 'remove_invalid_callback' => 'AMP_Validation_Utils::track_removed', ); - $expected_removed = array( - $attribute => 1, - ); $sanitizer = new AMP_Video_Sanitizer( $dom_document, $args ); $sanitizer->remove_invalid_attribute( $video, $attribute ); $this->assertEquals( null, $video->getAttribute( $attribute ) ); - $this->assertEquals( $expected_removed, AMP_Validation_Utils::$removed_attributes ); - // @codingStandardsIgnoreEnd + $this->assertEquals( array( $attr_node ), AMP_Validation_Utils::$removed_nodes ); AMP_Validation_Utils::reset_removed(); } diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php index b71c4d41228..eacee79e26f 100644 --- a/tests/test-class-amp-theme-support.php +++ b/tests/test-class-amp-theme-support.php @@ -101,7 +101,7 @@ public function test_prepare_response() { > - + @@ -114,12 +114,19 @@ public function test_prepare_response() { data-aax_src="302"> + + function( $node ) use ( &$removed_nodes ) { + $removed_nodes[ $node->nodeName ] = $node; + }, + ) ); $this->assertContains( '', $sanitized_html ); $this->assertContains( '', $sanitized_html ); @@ -136,6 +143,11 @@ public function test_prepare_response() { $this->assertContains( '

It seemed to have removed the

tag, As it contained a disallowed element. But it's not needed to report that the

is removed. So remove 'wpautop' as a callback for 'the_content.' Gutenberg also does this, unless the post has no block. @see gutenberg_wpautop(). --- includes/utils/class-amp-validation-utils.php | 8 +++++++- tests/test-class-amp-validation-utils.php | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index b8d96e75cd8..dd539573ab6 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -72,7 +72,12 @@ public static function was_node_removed() { * @return void. */ public static function process_markup( $markup ) { - $args = array( + AMP_Theme_Support::register_content_embed_handlers(); + remove_filter( 'the_content', 'wpautop' ); + + /** This filter is documented in wp-includes/post-template.php */ + $markup = apply_filters( 'the_content', $markup ); + $args = array( 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, ); if ( self::has_cap() ) { @@ -215,6 +220,7 @@ public static function validate_content( $post ) { if ( ! post_supports_amp( $post ) || ! self::has_cap() ) { return; } + AMP_Theme_Support::register_content_embed_handlers(); /** This filter is documented in wp-includes/post-template.php */ $filtered_content = apply_filters( 'the_content', $post->post_content, $post->ID ); $response = self::get_response( $filtered_content ); diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php index 98ac9f54e43..0be1cbcf39f 100644 --- a/tests/test-class-amp-validation-utils.php +++ b/tests/test-class-amp-validation-utils.php @@ -277,8 +277,6 @@ public function test_validate_content() { $this->assertNotContains( 'notice notice-warning', $output ); $this->assertNotContains( 'Warning:', $output ); - $post_id = $this->factory()->post->create(); - $post = get_post( $post_id ); $post->post_content = $this->disallowed_tag; ob_start(); AMP_Validation_Utils::validate_content( $post ); @@ -287,6 +285,18 @@ public function test_validate_content() { $this->assertContains( 'notice notice-warning', $output ); $this->assertContains( 'Warning:', $output ); $this->assertContains( 'script', $output ); + AMP_Validation_Utils::reset_removed(); + + $youtube = 'https://www.youtube.com/watch?v=GGS-tKTXw4Y'; + $post->post_content = $youtube; + ob_start(); + AMP_Validation_Utils::validate_content( $post ); + $output = ob_get_clean(); + + // The YouTube embed handler should convert the URL into a valid AMP element. + $this->assertNotContains( 'notice notice-warning', $output ); + $this->assertNotContains( 'Warning:', $output ); + AMP_Validation_Utils::reset_removed(); } /** From 18a877e369263884109a800cfe3075b71de6fb8d Mon Sep 17 00:00:00 2001 From: Ryan Kienstra Date: Tue, 13 Feb 2018 14:48:38 -0600 Subject: [PATCH 5/5] Issue #843: Exit early if ! has_cap(). Instead of wrapping the 'invalid_callback' in this, simply exist process_markup(). If that callback isn't added, there's no need for the rest of the function. --- includes/utils/class-amp-validation-utils.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php index dd539573ab6..6b79504e7f8 100644 --- a/includes/utils/class-amp-validation-utils.php +++ b/includes/utils/class-amp-validation-utils.php @@ -72,17 +72,19 @@ public static function was_node_removed() { * @return void. */ public static function process_markup( $markup ) { + if ( ! self::has_cap() ) { + return; + } + AMP_Theme_Support::register_content_embed_handlers(); remove_filter( 'the_content', 'wpautop' ); /** This filter is documented in wp-includes/post-template.php */ $markup = apply_filters( 'the_content', $markup ); $args = array( - 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, + 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, + 'remove_invalid_callback' => 'AMP_Validation_Utils::track_removed', ); - if ( self::has_cap() ) { - $args['remove_invalid_callback'] = 'AMP_Validation_Utils::track_removed'; - } AMP_Content_Sanitizer::sanitize( $markup, amp_get_content_sanitizers(), $args ); }