diff --git a/amp.php b/amp.php index 84172c1664c..064f006bafc 100644 --- a/amp.php +++ b/amp.php @@ -95,6 +95,7 @@ function amp_after_setup_theme() { add_action( 'wp_loaded', 'amp_add_options_menu' ); add_action( 'parse_query', 'amp_correct_query_when_is_front_page' ); AMP_Post_Type_Support::add_post_type_support(); + AMP_Validation_Utils::init(); } add_action( 'after_setup_theme', 'amp_after_setup_theme', 5 ); diff --git a/includes/class-amp-autoloader.php b/includes/class-amp-autoloader.php index 16880ed1325..7bf991c8d54 100644 --- a/includes/class-amp-autoloader.php +++ b/includes/class-amp-autoloader.php @@ -83,6 +83,7 @@ class AMP_Autoloader { 'AMP_DOM_Utils' => 'includes/utils/class-amp-dom-utils', 'AMP_HTML_Utils' => 'includes/utils/class-amp-html-utils', 'AMP_Image_Dimension_Extractor' => 'includes/utils/class-amp-image-dimension-extractor', + 'AMP_Validation_Utils' => 'includes/utils/class-amp-validation-utils', 'AMP_String_Utils' => 'includes/utils/class-amp-string-utils', 'AMP_WP_Utils' => 'includes/utils/class-amp-wp-utils', 'AMP_Widget_Archives' => 'includes/widgets/class-amp-widget-archives', diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index 94d7bffc5da..0d955ea0084 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -760,13 +760,27 @@ public static function finish_output_buffering() { * * @since 0.7 * - * @param string $response HTML document response. + * @param string $response HTML document response. By default it expects a complete document. + * @param array $args { + * Args to send to the preprocessor/sanitizer. + * + * @type callable $remove_invalid_callback Function to call whenever a node is removed due to being invalid. + * } * @return string AMP document response. * @global int $content_width */ - public static function prepare_response( $response ) { + public static function prepare_response( $response, $args = array() ) { global $content_width; + $args = array_merge( + array( + 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, // Back-compat. + 'use_document_element' => true, + 'remove_invalid_callback' => null, + ), + $args + ); + /* * Make sure that is present in output prior to parsing. * Note that the meta charset is supposed to appear within the first 1024 bytes. @@ -780,11 +794,7 @@ public static function prepare_response( $response ) { 1 ); } - $dom = AMP_DOM_Utils::get_dom( $response ); - $args = array( - 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, // Back-compat. - 'use_document_element' => true, - ); + $dom = AMP_DOM_Utils::get_dom( $response ); // First ensure the mandatory amp attribute is present on the html element, as otherwise it will be stripped entirely. if ( ! $dom->documentElement->hasAttribute( 'amp' ) && ! $dom->documentElement->hasAttribute( '⚡️' ) ) { @@ -798,7 +808,7 @@ public static function prepare_response( $response ) { // @todo If 'utf-8' is not the blog charset, then we'll need to do some character encoding conversation or "entityification". if ( 'utf-8' !== strtolower( get_bloginfo( 'charset' ) ) ) { /* translators: %s is the charset of the current site */ - trigger_error( esc_html( sprintf( __( 'The database has the %s encoding when it needs to be utf-8 to work with AMP.', 'amp' ), get_bloginfo( 'charset' ) ), E_USER_WARNING ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error + trigger_error( esc_html( sprintf( __( 'The database has the %s encoding when it needs to be utf-8 to work with AMP.', 'amp' ), get_bloginfo( 'charset' ) ) ), E_USER_WARNING ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error } $response = "\n"; diff --git a/includes/sanitizers/class-amp-audio-sanitizer.php b/includes/sanitizers/class-amp-audio-sanitizer.php index 749fed7f1c3..2854603e6bf 100644 --- a/includes/sanitizers/class-amp-audio-sanitizer.php +++ b/includes/sanitizers/class-amp-audio-sanitizer.php @@ -81,7 +81,7 @@ public function sanitize() { * @see: https://github.com/ampproject/amphtml/issues/2261 */ if ( 0 === $new_node->childNodes->length && empty( $new_attributes['src'] ) ) { - $node->parentNode->removeChild( $node ); + $this->remove_invalid_child( $node ); } else { $node->parentNode->replaceChild( $new_node, $node ); } diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index ab0695fd9c9..8e7a936afb7 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -304,4 +304,42 @@ public function maybe_enforce_https_src( $src, $force_https = false ) { return $src; } + + /** + * Removes an invalid child of a node. + * + * Also, calls the mutation callback for it. + * This tracks all the nodes that were removed. + * + * @since 0.7 + * + * @param DOMElement $child The node to remove. + * @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 ); + } + } + + /** + * Removes an invalid attribute of a node. + * + * Also, calls the mutation callback for it. + * This tracks all the attributes that were removed. + * + * @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. + */ + 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 ); + } + } + } diff --git a/includes/sanitizers/class-amp-blacklist-sanitizer.php b/includes/sanitizers/class-amp-blacklist-sanitizer.php index af23b884740..1b2eb9a9872 100644 --- a/includes/sanitizers/class-amp-blacklist-sanitizer.php +++ b/includes/sanitizers/class-amp-blacklist-sanitizer.php @@ -73,13 +73,13 @@ private function strip_attributes_recursive( $node, $bad_attributes, $bad_protoc $attribute = $node->attributes->item( $i ); $attribute_name = strtolower( $attribute->name ); if ( in_array( $attribute_name, $bad_attributes, true ) ) { - $node->removeAttribute( $attribute_name ); + $this->remove_invalid_attribute( $node, $attribute_name ); continue; } // The on* attributes (like onclick) are a special case. if ( 0 === stripos( $attribute_name, 'on' ) && 'on' !== $attribute_name ) { - $node->removeAttribute( $attribute_name ); + $this->remove_invalid_attribute( $node, $attribute_name ); continue; } elseif ( 'a' === $node_name ) { $this->sanitize_a_attribute( $node, $attribute ); @@ -112,10 +112,10 @@ private function strip_tags( $node, $tag_names ) { for ( $i = $length - 1; $i >= 0; $i-- ) { $element = $elements->item( $i ); $parent_node = $element->parentNode; - $parent_node->removeChild( $element ); + $this->remove_invalid_child( $element ); if ( 'body' !== $parent_node->nodeName && AMP_DOM_Utils::is_node_empty( $parent_node ) ) { - $parent_node->parentNode->removeChild( $parent_node ); + $this->remove_invalid_child( $parent_node ); } } } @@ -134,13 +134,13 @@ private function sanitize_a_attribute( $node, $attribute ) { $old_value = $attribute->value; $new_value = trim( preg_replace( self::PATTERN_REL_WP_ATTACHMENT, '', $old_value ) ); if ( empty( $new_value ) ) { - $node->removeAttribute( $attribute_name ); + $this->remove_invalid_attribute( $node, $attribute_name ); } elseif ( $old_value !== $new_value ) { $node->setAttribute( $attribute_name, $new_value ); } } elseif ( 'rev' === $attribute_name ) { // rev removed from HTML5 spec, which was used by Jetpack Markdown. - $node->removeAttribute( $attribute_name ); + $this->remove_invalid_attribute( $node, $attribute_name ); } elseif ( 'target' === $attribute_name ) { // _blank is the only allowed value and it must be lowercase. // replace _new with _blank and others should simply be removed. @@ -150,7 +150,7 @@ private function sanitize_a_attribute( $node, $attribute ) { $node->setAttribute( $attribute_name, '_blank' ); } else { // Only _blank is allowed. - $node->removeAttribute( $attribute_name ); + $this->remove_invalid_attribute( $node, $attribute_name ); } } } @@ -219,7 +219,7 @@ private function replace_node_with_children( $node, $bad_attributes, $bad_protoc // Remove the node from the parent, if defined. if ( $node->parentNode ) { - $node->parentNode->removeChild( $node ); + $this->remove_invalid_child( $node ); } } diff --git a/includes/sanitizers/class-amp-iframe-sanitizer.php b/includes/sanitizers/class-amp-iframe-sanitizer.php index e841596e642..9dac35a8a3e 100644 --- a/includes/sanitizers/class-amp-iframe-sanitizer.php +++ b/includes/sanitizers/class-amp-iframe-sanitizer.php @@ -74,7 +74,7 @@ public function sanitize() { * @see: https://github.com/ampproject/amphtml/issues/2261 */ if ( empty( $new_attributes['src'] ) ) { - $node->parentNode->removeChild( $node ); + $this->remove_invalid_child( $node ); continue; } @@ -193,4 +193,5 @@ private function build_placeholder( $parent_attributes ) { return $placeholder_node; } + } diff --git a/includes/sanitizers/class-amp-img-sanitizer.php b/includes/sanitizers/class-amp-img-sanitizer.php index 8007aaa7152..971e8d6cdec 100644 --- a/includes/sanitizers/class-amp-img-sanitizer.php +++ b/includes/sanitizers/class-amp-img-sanitizer.php @@ -74,7 +74,7 @@ public function sanitize() { } if ( ! $node->hasAttribute( 'src' ) || '' === $node->getAttribute( 'src' ) ) { - $node->parentNode->removeChild( $node ); + $this->remove_invalid_child( $node ); continue; } diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 2794b6ba432..67c82686d3a 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -704,6 +704,9 @@ 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 ); + } } } @@ -809,7 +812,7 @@ private function delegated_sanitize_disallowed_attribute_values_in_node( $node, ( true === $attr_spec_list[ $attr_name ][ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) ) { $node->setAttribute( $attr_name, '' ); } else { - $node->removeAttribute( $attr_name ); + $this->remove_invalid_attribute( $node, $attr_name ); } } } @@ -1448,13 +1451,13 @@ private function remove_node( $node ) { */ $parent = $node->parentNode; if ( $node && $parent ) { - $parent->removeChild( $node ); + $this->remove_invalid_child( $node ); } while ( $parent && ! $parent->hasChildNodes() && $this->root_element !== $parent ) { $node = $parent; $parent = $parent->parentNode; if ( $parent ) { - $parent->removeChild( $node ); + $this->remove_invalid_child( $node ); } } } diff --git a/includes/sanitizers/class-amp-video-sanitizer.php b/includes/sanitizers/class-amp-video-sanitizer.php index 87bf0df6924..1dadfcc00eb 100644 --- a/includes/sanitizers/class-amp-video-sanitizer.php +++ b/includes/sanitizers/class-amp-video-sanitizer.php @@ -93,7 +93,7 @@ public function sanitize() { * See: https://github.com/ampproject/amphtml/issues/2261 */ if ( 0 === $new_node->childNodes->length && empty( $new_attributes['src'] ) ) { - $node->parentNode->removeChild( $node ); + $this->remove_invalid_child( $node ); } else { $node->parentNode->replaceChild( $new_node, $node ); } diff --git a/includes/utils/class-amp-validation-utils.php b/includes/utils/class-amp-validation-utils.php new file mode 100644 index 00000000000..c7450248f81 --- /dev/null +++ b/includes/utils/class-amp-validation-utils.php @@ -0,0 +1,355 @@ +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. + */ + public static function increment_count( $histogram, $key ) { + $current_value = isset( $histogram[ $key ] ) ? $histogram[ $key ] : 0; + $histogram[ $key ] = $current_value + 1; + return $histogram; + } + + /** + * Gets whether a node was removed in a sanitizer. + * + * @return boolean. + */ + public static function was_node_removed() { + return ! empty( self::$removed_nodes ); + } + + /** + * Processes markup, to determine AMP validity. + * + * Passes $markup through the AMP sanitizers. + * Also passes a 'remove_invalid_callback' to keep track of stripped attributes and nodes. + * + * @param string $markup The markup to process. + * @return void. + */ + public static function process_markup( $markup ) { + $args = array( + 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, + ); + if ( self::is_authorized() ) { + $args['remove_invalid_callback'] = 'AMP_Validation_Utils::track_removed'; + } + AMP_Content_Sanitizer::sanitize( $markup, amp_get_content_sanitizers(), $args ); + } + + /** + * Registers the REST API endpoint for validation. + * + * @return void. + */ + public static function amp_rest_validation() { + register_rest_route( 'amp-wp/v1', '/validate', array( + 'methods' => 'POST', + 'callback' => array( __CLASS__, 'validate_markup' ), + 'args' => array( + self::MARKUP_KEY => array( + 'validate_callback' => array( __CLASS__, 'validate_arg' ), + ), + ), + 'permission_callback' => array( __CLASS__, 'has_cap' ), + ) ); + } + + /** + * Whether the user has the required capability. + * + * Checks for permissions before validating. + * Also serves as the permission callback for REST requests. + * + * @return boolean $has_cap Whether the current user has the capability. + */ + public static function has_cap() { + return current_user_can( 'edit_posts' ); + } + + /** + * Validate the markup passed to the REST API. + * + * @param WP_REST_Request $request The REST request. + * @return array|WP_Error. + */ + public static function validate_markup( WP_REST_Request $request ) { + $json = $request->get_json_params(); + if ( empty( $json[ self::MARKUP_KEY ] ) ) { + return new WP_Error( 'no_markup', 'No markup passed to validator', array( + 'status' => 404, + ) ); + } + + return self::get_response( $json[ self::MARKUP_KEY ] ); + } + + /** + * Gets the AMP validation response. + * + * If $markup isn't passed, + * It will return the validation errors the sanitizers found in rendering the page. + * + * @param string $markup To validate for AMP compatibility (optional). + * @return array $response The AMP validity of the markup. + */ + public static function get_response( $markup = null ) { + $response = array(); + if ( isset( $markup ) ) { + 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 ); + self::reset_removed(); + + return $response; + } + + /** + * Reset the stored removed nodes and attributes. + * + * After testing if the markup is valid, + * these static values will remain. + * So reset them in case another test is needed. + * + * @return void. + */ + public static function reset_removed() { + self::$removed_nodes = null; + self::$removed_attributes = null; + } + + /** + * Validate the argument in the REST API request. + * + * It would be ideal to simply pass 'is_string' in register_rest_route(). + * But it always returned false. + * + * @param mixed $arg The argument to validate. + * @return boolean $is_valid Whether the argument is valid. + */ + public static function validate_arg( $arg ) { + return is_string( $arg ); + } + + /** + * On updating a post, this checks the AMP validity of the content. + * + * If it's not valid AMP, it adds a query arg to the redirect URL. + * This will cause an error message to appear above the 'Classic' editor. + * + * @param integer $post_id The ID of the updated post. + * @param WP_Post $post The updated post. + * @return void. + */ + public static function validate_content( $post_id, $post ) { + unset( $post_id ); + if ( ! post_supports_amp( $post ) || ! self::is_authorized() ) { + return; + } + /** 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 ); + if ( isset( $response[ self::ERROR_KEY ] ) && ( true === $response[ self::ERROR_KEY ] ) ) { + add_filter( 'redirect_post_location', function( $location ) use ( $response ) { + $location = AMP_Validation_Utils::error_message( $location ); + $location = add_query_arg( + array( + 'removed_elements' => array_keys( $response['removed_nodes'] ), + 'removed_attributes' => array_keys( $response['removed_attributes'] ), + ), + $location + ); + return $location; + } ); + } + } + + /** + * Whether the current user is authorized. + * + * This checks that the user has a certain capability and the nonce is valid. + * It will only return true when updating the post on: + * wp-admin/post.php + * Avoids using check_admin_referer(). + * This function might be called in different places, + * and it can't cause it to die() if the nonce is invalid. + * + * @return boolean $is_valid True if the nonce is valid. + */ + public static function is_authorized() { + $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : ''; // WPCS: CSRF ok. + return ( self::has_cap() && ( false !== wp_verify_nonce( $nonce, 'update-post_' . get_the_ID() ) ) ); + } + + /** + * Adds an error message to the URL if it's not valid AMP. + * + * When redirecting after saving a post, the content was validated for AMP compliance. + * If it wasn't valid AMP, this will add a query arg to the URL. + * And an error message will display on /wp-admin/post.php. + * + * @param string $url The URL of the redirect. + * @return string $url The filtered URL, including the AMP error message query var. + */ + public static function error_message( $url ) { + $args = array( + self::ERROR_QUERY_KEY => self::ERROR_QUERY_VALUE, + self::ERROR_NONCE => wp_create_nonce( self::ERROR_NONCE_ACTION ), + ); + return add_query_arg( $args, $url ); + } + + /** + * Displays an error message on /wp-admin/post.php if the saved content is not valid AMP. + * + * Use $_GET, as get_query_var won't return the value. + * This displays at the top of the 'Classic' editor. + * + * @return void. + */ + public static function display_error() { + if ( ! isset( $_GET[ self::ERROR_QUERY_KEY ] ) ) { + return; + } + check_admin_referer( self::ERROR_NONCE_ACTION, self::ERROR_NONCE ); + $error = isset( $_GET[ self::ERROR_QUERY_KEY ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::ERROR_QUERY_KEY ] ) ) : ''; // WPCS: CSRF ok. + if ( self::ERROR_QUERY_VALUE === $error ) { + echo '
'; + printf( '

%s

', esc_html__( 'Warning: There is content which fails AMP validation; it will be stripped when served as AMP.', 'amp' ) ); + if ( ! empty( $_GET['removed_elements'] ) && is_array( $_GET['removed_elements'] ) ) { + printf( + '

%s %s

', + esc_html__( 'Invalid elements:', 'amp' ), + '' . implode( ', ', array_map( 'esc_html', $_GET['removed_elements'] ) ) . '' + ); + } + if ( ! empty( $_GET['removed_attributes'] ) ) { + printf( + '

%s %s

', + esc_html__( 'Invalid attributes:', 'amp' ), + '' . implode( ', ', array_map( 'esc_html', $_GET['removed_attributes'] ) ) . '' + ); + } + echo '
'; + } + } + +} diff --git a/phpcs.xml b/phpcs.xml index ff30b725a65..312fc87d2aa 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -11,6 +11,9 @@ + + tests/* + tests/test-tag-and-attribute-sanitizer.php @@ -25,7 +28,7 @@ - + diff --git a/tests/test-amp-iframe-sanitizer.php b/tests/test-amp-iframe-sanitizer.php index 1bdda928309..cf67b0f489f 100644 --- a/tests/test-amp-iframe-sanitizer.php +++ b/tests/test-amp-iframe-sanitizer.php @@ -171,4 +171,5 @@ public function test__args__placeholder() { $this->assertEquals( $expected, $content ); } + } diff --git a/tests/test-class-amp-base-sanitizer.php b/tests/test-class-amp-base-sanitizer.php index 4e135d4fbde..a7c6bcd8f08 100644 --- a/tests/test-class-amp-base-sanitizer.php +++ b/tests/test-class-amp-base-sanitizer.php @@ -241,4 +241,68 @@ public function test_enforce_sizes_attribute( $source_params, $expected_value, $ $this->assertEquals( $expected_value, $actual_value ); } + + /** + * Tests remove_child. + * + * @see 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 ] ); + + $parent->appendChild( $child ); + $this->assertEquals( $child, $parent->firstChild ); + $sanitizer->remove_invalid_child( $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() + */ + public function test_remove_attribute() { + AMP_Validation_Utils::reset_removed(); + $video_name = 'amp-video'; + $attribute = 'onload'; + $dom_document = new DOMDocument( '1.0', 'utf-8' ); + $video = $dom_document->createElement( $video_name ); + $video->setAttribute( $attribute, 'someFunction()' ); + + // 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 + AMP_Validation_Utils::reset_removed(); + } + } diff --git a/tests/test-class-amp-validation-utils.php b/tests/test-class-amp-validation-utils.php new file mode 100644 index 00000000000..16f90515019 --- /dev/null +++ b/tests/test-class-amp-validation-utils.php @@ -0,0 +1,358 @@ +'; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript + + /** + * A valid image that sanitizers should not alter. + * + * @var string + */ + public $valid_amp_img = ''; + + /** + * The key in the response for whether it has an AMP error. + * + * @var string + */ + public $error_key = 'has_error'; + + /** + * The name of the tag to test. + * + * @var string + */ + const TAG_NAME = 'img'; + + /** + * Setup. + * + * @inheritdoc + */ + public function setUp() { + parent::setUp(); + $dom_document = new DOMDocument( '1.0', 'utf-8' ); + $this->node = $dom_document->createElement( self::TAG_NAME ); + AMP_Validation_Utils::reset_removed(); + } + + /** + * Test init. + * + * @see AMP_Validation_Utils::init() + */ + public function test_init() { + $this->assertEquals( 10, has_action( 'rest_api_init', 'AMP_Validation_Utils::amp_rest_validation' ) ); + $this->assertEquals( 10, has_action( 'save_post', 'AMP_Validation_Utils::validate_content' ) ); + $this->assertEquals( 10, has_action( 'edit_form_top', 'AMP_Validation_Utils::display_error' ) ); + } + + /** + * Test track_removed. + * + * @see AMP_Validation_Utils::track_removed() + */ + public function test_track_removed() { + $attr_name = 'invalid-attr'; + $expected_removed_attrs = array( + $attr_name => 1, + ); + $expected_removed_nodes = array( + 'img' => 1, + ); + $this->assertEmpty( AMP_Validation_Utils::$removed_attributes ); + $this->assertEmpty( AMP_Validation_Utils::$removed_nodes ); + AMP_Validation_Utils::track_removed( $this->node, AMP_Validation_Utils::ATTRIBUTE_REMOVED, $attr_name ); + $this->assertEquals( $expected_removed_attrs, AMP_Validation_Utils::$removed_attributes ); + AMP_Validation_Utils::track_removed( $this->node, AMP_Validation_Utils::NODE_REMOVED ); + $this->assertEquals( $expected_removed_nodes, AMP_Validation_Utils::$removed_nodes ); + } + + /** + * Test increment_count. + * + * @see AMP_Validation_Utils::increment_count() + */ + public function test_increment_count() { + $attribute = 'script'; + $one_attribute = array( + $attribute => 1, + ); + $expected = array( + $attribute => 2, + ); + $this->assertEquals( $one_attribute, AMP_Validation_Utils::increment_count( array(), $attribute ) ); + $this->assertEquals( $expected, AMP_Validation_Utils::increment_count( $one_attribute, $attribute ) ); + } + + /** + * Test was_node_removed. + * + * @see AMP_Validation_Utils::was_node_removed() + */ + public function test_was_node_removed() { + $attr_name = 'invalid-attr'; + $this->assertFalse( AMP_Validation_Utils::was_node_removed() ); + AMP_Validation_Utils::track_removed( $this->node, AMP_Validation_Utils::NODE_REMOVED ); + $this->assertTrue( AMP_Validation_Utils::was_node_removed() ); + } + + /** + * Test process_markup. + * + * @see AMP_Validation_Utils::process_markup() + */ + public function test_process_markup() { + $this->set_authorized(); + AMP_Validation_Utils::process_markup( $this->valid_amp_img ); + $this->assertEquals( null, AMP_Validation_Utils::$removed_nodes ); + $this->assertEquals( null, AMP_Validation_Utils::$removed_attributes ); + + AMP_Validation_Utils::reset_removed(); + $video = '