From bf281991e156ff41712e1fc2186905b77b53019f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 31 Oct 2019 10:54:01 -0700 Subject: [PATCH 01/39] Eliminate premature/redundant attribute checks from validate_attr_spec_list_for_node --- .../sanitizers/class-amp-base-sanitizer.php | 2 +- .../class-amp-tag-and-attribute-sanitizer.php | 202 +++++++----------- ...nd-attribute-sanitizer-private-methods.php | 24 +-- .../php/test-tag-and-attribute-sanitizer.php | 13 +- 4 files changed, 98 insertions(+), 143 deletions(-) diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 15a77d42323..88ce842dd35 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -485,7 +485,7 @@ public function remove_invalid_attribute( $element, $attribute, $validation_erro $node = $attribute; } $should_remove = $this->should_sanitize_validation_error( $validation_error, compact( 'node' ) ); - if ( $should_remove ) { + if ( $should_remove && $element === $node->parentNode ) { $element->removeAttributeNode( $node ); } return $should_remove; diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index e699dcbefd0..8b0fa86eba5 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -476,6 +476,7 @@ private function process_node( DOMElement $node ) { // If no attribute spec lists match, then the element must be removed. if ( empty( $attr_spec_scores ) ) { + // @todo How can we tell the reason for removal if none of the tag specs matched? Ignore attribute values? $this->remove_node( $node ); return null; } @@ -514,6 +515,8 @@ private function process_node( DOMElement $node ) { } } + // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. + // @todo Will the is_missing_mandatory_attribute check here even be needed because it will be checked if ( ! empty( $attr_spec_list ) && $this->is_missing_mandatory_attribute( $attr_spec_list, $node ) ) { $this->remove_node( $node ); return null; @@ -756,9 +759,38 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { /** * Checks to see if a spec is potentially valid. * - * Checks the given node based on the attributes present in the node. + * Checks the given node based on the attributes present in the node. This does not check every possible constraint + * imposed by the validator spec. It only performs the checks that are used to narrow down which set of attribute + * specs is most aligned with the given node. As of AMPHTML v1910161528000, the frequency of attribute spec + * constraints looks as follows: * - * @note This can be a very expensive function. Use it sparingly. + * 433: value + * 400: mandatory + * 222: value_casei + * 147: blacklisted_value_regex + * 115: value_regex + * 101: value_url + * 77: dispatch_key + * 17: value_regex_casei + * 15: requires_extension + * 12: alternative_names + * 2: value_properties + * + * The constraints that should be the most likely to differentiate one tag spec from another are: + * + * - value + * - mandatory + * - value_casei + * + * For example, there are two tag specs, one that has a mandatory lightbox attribute and another that + * lacks the lightbox attribute altogether. If an has the lightbox attribute, then we can rule out + * the tag spec without the lightbox attribute via the mandatory constraint. + * + * Additionally, there are multiple tag specs, each which vary by the value of the 'type' attribute. + * By validating the type 'value' and 'value_casei' constraints here, we can narrow down the tag specs that should + * then be used to later validate and sanitize the element (in the sanitize_disallowed_attribute_values_in_node method). + * + * @see AMP_Tag_And_Attribute_Sanitizer::sanitize_disallowed_attribute_values_in_node() * * @param DOMElement $node Node. * @param array[] $attr_spec_list Attribute Spec list. @@ -776,7 +808,7 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ return 0; } } - return 0.5; + return 1; } foreach ( $node->attributes as $attr_name => $attr_node ) { @@ -807,16 +839,22 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ // If attr spec rule is empty, then it allows anything. if ( empty( $attr_spec_rule ) && $node->hasAttribute( $attr_name ) ) { - $score++; + $score += 2; continue; } + // Merely having the attribute counts for something, though it may get sanitized out later. + if ( $node->hasAttribute( $attr_name ) ) { + $score += 2; + } + // If a mandatory attribute is required, and attribute exists, pass. if ( isset( $attr_spec_rule[ AMP_Rule_Spec::MANDATORY ] ) ) { - $mandatory_count++; + $mandatory_count ++; + $result = $this->check_attr_spec_rule_mandatory( $node, $attr_name, $attr_spec_rule ); if ( AMP_Rule_Spec::PASS === $result ) { - $score++; + $score += 2; } elseif ( AMP_Rule_Spec::FAIL === $result ) { return 0; } @@ -829,21 +867,7 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE ] ) ) { $result = $this->check_attr_spec_rule_value( $node, $attr_name, $attr_spec_rule ); if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * Check 'value_regex' - case sensitive regex match - * Given attribute's value must be a case insensitive match to regex pattern - * specified by the value of rule to pass. - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_REGEX ] ) ) { - $result = $this->check_attr_spec_rule_value_regex( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; + $score += 2; } elseif ( AMP_Rule_Spec::FAIL === $result ) { return 0; } @@ -857,118 +881,16 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_CASEI ] ) ) { $result = $this->check_attr_spec_rule_value_casei( $node, $attr_name, $attr_spec_rule ); if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * Check 'value_regex_casei' - case insensitive regex match - * Given attribute's value must be a case insensitive match to the regex - * pattern specified by the value of the rule to pass. - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_REGEX_CASEI ] ) ) { - $result = $this->check_attr_spec_rule_value_regex_casei( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * If given attribute's value is a URL with a protocol, the protocol must - * be in the array specified by the rule's value to pass. - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOWED_PROTOCOL ] ) ) { - $result = $this->check_attr_spec_rule_allowed_protocol( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * If given attribute's value is a URL with a host, the host must - * be valid - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ] ) ) { - $result = $this->check_attr_spec_rule_valid_url( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * If the given attribute's value is *not* a relative path, and the rule's - * value is `false`, then pass. - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_RELATIVE ] ) ) { - $result = $this->check_attr_spec_rule_disallowed_relative( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * If the given attribute's value exists, is non-empty and the rule's value - * is false, then pass. - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) ) { - $result = $this->check_attr_spec_rule_disallowed_empty( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * If the given attribute's value is a URL and does not match any of the list - * of domains in the value of the rule, then pass. - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::DISALLOWED_DOMAIN ] ) ) { - $result = $this->check_attr_spec_rule_disallowed_domain( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - /* - * If the attribute's value exists and does not match the regex specified - * by the rule's value, then pass. - */ - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::BLACKLISTED_VALUE_REGEX ] ) ) { - $result = $this->check_attr_spec_rule_blacklisted_value_regex( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; - } elseif ( AMP_Rule_Spec::FAIL === $result ) { - return 0; - } - } - - // If the attribute's value exists and it matches the value properties spec. - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_PROPERTIES ] ) && $node->hasAttribute( $attr_name ) ) { - $result = $this->check_attr_spec_rule_value_properties( $node, $attr_name, $attr_spec_rule ); - if ( AMP_Rule_Spec::PASS === $result ) { - $score++; + $score += 2; } elseif ( AMP_Rule_Spec::FAIL === $result ) { return 0; } } } - // Give the spec a score if it doesn't have any mandatory attributes. + // Give the spec a score if it doesn't have any mandatory attributes, since they could all be removed during sanitization. if ( 0 === $mandatory_count && 0 === $score ) { - $score = 0.5; + $score = 1; } return $score; @@ -1002,6 +924,8 @@ private function get_disallowed_attributes_in_node( DOMElement $node, $attr_spec * * Allowed values are found $this->globally_allowed_attributes and in parameter $attr_spec_list * + * @see \AMP_Tag_And_Attribute_Sanitizer::validate_attr_spec_list_for_node() + * * @param DOMElement $node Node. * @param array[][] $attr_spec_list Attribute spec list. * @param DOMAttr[] $attributes_pending_removal Attributes pending removal. @@ -1033,6 +957,19 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, $should_remove_node = false; $attr_spec_rule = $attr_spec_list[ $attr_name ]; + /* + * Note that the following checks may have been previously done in validate_attr_spec_list_for_node(): + * + * - check_attr_spec_rule_mandatory + * - check_attr_spec_rule_value + * - check_attr_spec_rule_value_casei + * + * They have already been checked because the tag spec should only be considered a candidate for a given + * node if if passes those checks, that is, if the shape of the node matches the spec close enough. + * + * However, if there was only one spec for a given tag, then then validate_attr_spec_list_for_node() would + * not have been called. and thus these checks need to be performed here as well. + */ if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value( $node, $attr_name, $attr_spec_rule ) ) { $should_remove_node = true; @@ -1063,8 +1000,13 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::BLACKLISTED_VALUE_REGEX ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_blacklisted_value_regex( $node, $attr_name, $attr_spec_rule ) ) { $should_remove_node = true; + } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_PROPERTIES ] ) && + AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_properties( $node, $attr_name, $attr_spec_rule ) ) { + $should_remove_node = true; } + // @todo Mandatory check is done where?? + if ( $should_remove_node ) { $is_mandatory = isset( $attr_spec_rule[ AMP_Rule_Spec::MANDATORY ] ) @@ -1072,7 +1014,13 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, : false; if ( $is_mandatory ) { - $this->remove_node( $node ); + $this->remove_invalid_child( + $node, + [ + 'code' => 'invalid_mandatory_attribute', + 'attr_name' => $attr_name, + ] + ); return false; } diff --git a/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php b/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php index b4ceb056e4c..01cc655d35d 100644 --- a/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php +++ b/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php @@ -872,7 +872,7 @@ public function get_validate_attr_spec_list_for_node_data() { 'node_tag_name' => 'div', 'attr_spec_list' => [], ], - 0.5, // Because there are no mandatory attributes. + 1, // Because there are no mandatory attributes. ], 'attributes_no_spec' => [ [ @@ -880,7 +880,7 @@ public function get_validate_attr_spec_list_for_node_data() { 'node_tag_name' => 'div', 'attr_spec_list' => [], ], - 0.5, // Because there are no mandatory attributes. + 1, // Because there are no mandatory attributes. ], 'attributes_alternative_names' => [ [ @@ -896,7 +896,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 0.5, // Because there are no mandatory attributes. + 2, // Because there are no mandatory attributes. ], 'attributes_mandatory' => [ [ @@ -908,7 +908,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 1, + 4, ], 'attributes_mandatory_alternative_name' => [ [ @@ -925,7 +925,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 1, + 2, ], 'attributes_value' => [ [ @@ -937,7 +937,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 1, + 4, ], 'attributes_value_regex' => [ [ @@ -949,7 +949,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 1, + 2, ], 'attributes_value_casei' => [ [ @@ -961,7 +961,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 1, + 4, ], 'attributes_value_regex_casei' => [ [ @@ -973,7 +973,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 1, + 2, ], 'attributes_allow_relative_false_pass' => [ [ @@ -1001,7 +1001,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 0, + 2, // Still passes because relative URL is not checked until sanitization. ], 'attributes_allow_empty_false_pass' => [ [ @@ -1029,7 +1029,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 0, + 2, // Allow empty is not not used until sanitization. ], 'attributes_blacklisted_regex' => [ [ @@ -1046,7 +1046,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 0, + 2, ], ]; } diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 247b6a4e391..ea768ee740a 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -1809,18 +1809,26 @@ public function get_html_data() { 'bad_external_font' => [ '', // phpcs:ignore '', + [], + [ 'invalid_mandatory_attribute' ], ], 'bad_meta_ua_compatible' => [ '', - '', // Note the http-equiv is removed because the content violates its attribute spec. + '', + [], + [ 'invalid_mandatory_attribute' ], ], 'bad_meta_charset' => [ 'Mojibake?', 'Mojibake?', // Note the charset attribute is removed because it violates the attribute spec, but the entire element is not removed because charset is not mandatory. + [], + [ 'invalid_attribute' ], // @todo Should actually be invalid_mandatory_attribute? ], 'bad_meta_viewport' => [ '', '', + [], + [ 'invalid_mandatory_attribute' ], ], 'edge_meta_ua_compatible' => [ '', @@ -1989,13 +1997,12 @@ public function test_html_sanitizer( $source, $expected = null, $expected_script ); $sanitizer->sanitize(); $content = AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement ); + $this->assertEqualMarkup( $expected, $content ); if ( is_array( $expected_error_codes ) ) { $this->assertEqualSets( $expected_error_codes, $actual_error_codes ); } - $this->assertEqualMarkup( $expected, $content ); - if ( is_array( $expected_scripts ) ) { $this->assertEqualSets( $expected_scripts, array_keys( $sanitizer->get_scripts() ) ); } From 656ebb03215aa33b691c361225e0de5067534041 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 15 Nov 2019 20:25:18 -0800 Subject: [PATCH 02/39] Remove redundant is_missing_mandatory_attribute check --- .../class-amp-tag-and-attribute-sanitizer.php | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 8b0fa86eba5..ca1091697e6 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -496,16 +496,16 @@ private function process_node( DOMElement $node ) { // If we're here, then we're not sure which spec should // be used. Let's use the top scoring ones. foreach ( $spec_ids_sorted as $id ) { - $spec_list = isset( $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::ATTR_SPEC_LIST ] ) ? $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::ATTR_SPEC_LIST ] : null; - if ( ! $this->is_missing_mandatory_attribute( $spec_list, $node ) ) { - $attr_spec_list = array_merge( $attr_spec_list, $spec_list ); - $tag_spec = array_merge( - $tag_spec, - $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::TAG_SPEC ] - ); - if ( isset( $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::CDATA ] ) ) { - $cdata = array_merge( $cdata, $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::CDATA ] ); - } + $attr_spec_list = array_merge( + $attr_spec_list, + $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::ATTR_SPEC_LIST ] + ); + $tag_spec = array_merge( + $tag_spec, + $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::TAG_SPEC ] + ); + if ( isset( $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::CDATA ] ) ) { + $cdata = array_merge( $cdata, $rule_spec_list_to_validate[ $id ][ AMP_Rule_Spec::CDATA ] ); } } $first_spec = reset( $rule_spec_list_to_validate ); @@ -650,9 +650,6 @@ private function process_node( DOMElement $node ) { * @return boolean $is_missing boolean Whether a required attribute is missing. */ public function is_missing_mandatory_attribute( $attr_spec, DOMElement $node ) { - if ( ! is_array( $attr_spec ) ) { - return false; - } foreach ( $attr_spec as $attr_name => $attr_spec_rule_value ) { if ( '\u' === substr( $attr_name, 0, 2 ) ) { $attr_name = html_entity_decode( '&#x' . substr( $attr_name, 2 ) . ';' ); // Probably ⚡. From a7f275098de5af4b6f973a48fcf6e731055740cb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 15 Nov 2019 20:27:05 -0800 Subject: [PATCH 03/39] Consolidate removal of elements without mandatory attributes --- .../class-amp-tag-and-attribute-sanitizer.php | 43 +++++-------------- .../php/test-tag-and-attribute-sanitizer.php | 6 +-- 2 files changed, 13 insertions(+), 36 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index ca1091697e6..f70e2d77b28 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -515,18 +515,11 @@ private function process_node( DOMElement $node ) { } } - // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. - // @todo Will the is_missing_mandatory_attribute check here even be needed because it will be checked - if ( ! empty( $attr_spec_list ) && $this->is_missing_mandatory_attribute( $attr_spec_list, $node ) ) { - $this->remove_node( $node ); - return null; - } - // Remove element if it has illegal CDATA. if ( ! empty( $cdata ) && $node instanceof DOMElement ) { $validity = $this->validate_cdata_for_node( $node, $cdata ); if ( is_wp_error( $validity ) ) { - $this->remove_node( $node ); + $this->remove_invalid_child( $node ); // @todo Code: illegal_cdata. return null; } } @@ -569,12 +562,6 @@ private function process_node( DOMElement $node ) { // Identify attribute values that don't conform to the attr_spec. $disallowed_attributes = $this->sanitize_disallowed_attribute_values_in_node( $node, $merged_attr_spec_list, $disallowed_attributes ); - // If $disallowed_attributes is false then the entire element should be removed. - if ( false === $disallowed_attributes ) { - $this->remove_node( $node ); - return null; - } - // Remove all invalid attributes. if ( ! empty( $disallowed_attributes ) ) { /* @@ -608,6 +595,13 @@ private function process_node( DOMElement $node ) { } } + // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. + // After attributes have been sanitized (and potentially removed), if mandatory attribute(s) are missing, remove the element. + if ( $this->is_missing_mandatory_attribute( $merged_attr_spec_list, $node ) ) { + $this->remove_invalid_child( $node, [ 'code' => 'missing_mandatory_attribute' ] ); + return null; + } + // Add required AMP component scripts. $script_components = []; if ( ! empty( $tag_spec['requires_extension'] ) ) { @@ -847,7 +841,7 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ // If a mandatory attribute is required, and attribute exists, pass. if ( isset( $attr_spec_rule[ AMP_Rule_Spec::MANDATORY ] ) ) { - $mandatory_count ++; + $mandatory_count++; $result = $this->check_attr_spec_rule_mandatory( $node, $attr_name, $attr_spec_rule ); if ( AMP_Rule_Spec::PASS === $result ) { @@ -1002,29 +996,12 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, $should_remove_node = true; } - // @todo Mandatory check is done where?? - if ( $should_remove_node ) { - $is_mandatory = - isset( $attr_spec_rule[ AMP_Rule_Spec::MANDATORY ] ) - ? (bool) $attr_spec_rule[ AMP_Rule_Spec::MANDATORY ] - : false; - - if ( $is_mandatory ) { - $this->remove_invalid_child( - $node, - [ - 'code' => 'invalid_mandatory_attribute', - 'attr_name' => $attr_name, - ] - ); - return false; - } - $attrs_to_remove[] = $attr_node; } } + // @todo Do not actually mutate the attributes here, but rather pass back what should be done? // Remove the disallowed values. foreach ( $attrs_to_remove as $attr_node ) { if ( isset( $attr_spec_list[ $attr_node->nodeName ][ AMP_Rule_Spec::VALUE_URL ] ) && diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index ea768ee740a..a35a10b3f3a 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -1810,13 +1810,13 @@ public function get_html_data() { '', // phpcs:ignore '', [], - [ 'invalid_mandatory_attribute' ], + [ 'invalid_attribute', 'missing_mandatory_attribute' ], ], 'bad_meta_ua_compatible' => [ '', '', [], - [ 'invalid_mandatory_attribute' ], + [ 'invalid_attribute', 'missing_mandatory_attribute' ], ], 'bad_meta_charset' => [ 'Mojibake?', @@ -1828,7 +1828,7 @@ public function get_html_data() { '', '', [], - [ 'invalid_mandatory_attribute' ], + [ 'invalid_attribute', 'missing_mandatory_attribute' ], ], 'edge_meta_ua_compatible' => [ '', From 6475b70d74e3694735dd5d0ae93288736102cd82 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 15 Nov 2019 20:32:33 -0800 Subject: [PATCH 04/39] Introduce illegal_cdata error code --- includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php | 2 +- includes/validation/class-amp-validation-error-taxonomy.php | 2 ++ tests/php/test-tag-and-attribute-sanitizer.php | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index f70e2d77b28..642ac90b954 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -519,7 +519,7 @@ private function process_node( DOMElement $node ) { if ( ! empty( $cdata ) && $node instanceof DOMElement ) { $validity = $this->validate_cdata_for_node( $node, $cdata ); if ( is_wp_error( $validity ) ) { - $this->remove_invalid_child( $node ); // @todo Code: illegal_cdata. + $this->remove_invalid_child( $node, [ 'code' => 'illegal_cdata' ] ); return null; } } diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index cde6cea55ae..ac43a215f8b 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -2936,6 +2936,8 @@ public static function get_error_title_from_code( $validation_error ) { $title .= sprintf( ': %s', esc_html( $validation_error['property_name'] ) ); } return $title; + case 'illegal_cdata': + return esc_html__( 'Illegal text content', 'amp' ); case 'illegal_css_important': $title = esc_html__( 'Illegal CSS !important property', 'amp' ); if ( isset( $validation_error['property_name'] ) ) { diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index a35a10b3f3a..41fc43eb29d 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -1915,10 +1915,10 @@ public function get_html_data() { ', [ 'amp-bind' ], [ + 'illegal_cdata', 'invalid_attribute', 'invalid_element', 'invalid_element', - 'invalid_element', ], ]; From ad440fab7cc8f77b4305719b0eb9df77a99fd743 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 22 Nov 2019 15:27:15 -0800 Subject: [PATCH 05/39] Fix return value docs for validate_attr_spec_list_for_node --- includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 642ac90b954..fe658b193ea 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -786,7 +786,7 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { * @param DOMElement $node Node. * @param array[] $attr_spec_list Attribute Spec list. * - * @return float Number of times the attribute spec list matched. If there was a mismatch, then 0 is returned. 0.5 is returned if there is an implicit match. + * @return int Score for how well the attribute spec list patched. */ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_list ) { /* From 3f2f50e98b46514de312e453dfbb275f0666ea01 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 22 Nov 2019 16:32:20 -0800 Subject: [PATCH 06/39] Reuse check_attr_spec_rule_mandatory in is_missing_mandatory_attribute --- .../class-amp-tag-and-attribute-sanitizer.php | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index fe658b193ea..9ce6a39f5fe 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -648,20 +648,7 @@ public function is_missing_mandatory_attribute( $attr_spec, DOMElement $node ) { if ( '\u' === substr( $attr_name, 0, 2 ) ) { $attr_name = html_entity_decode( '&#x' . substr( $attr_name, 2 ) . ';' ); // Probably ⚡. } - $is_mandatory = isset( $attr_spec_rule_value[ AMP_Rule_Spec::MANDATORY ] ) ? ( true === $attr_spec_rule_value[ AMP_Rule_Spec::MANDATORY ] ) : false; - $attribute_exists = false; - if ( method_exists( $node, 'hasAttribute' ) ) { - $attribute_exists = $node->hasAttribute( $attr_name ); - if ( ! $attribute_exists && ! empty( $attr_spec_rule_value[ AMP_Rule_Spec::ALTERNATIVE_NAMES ] ) ) { - foreach ( $attr_spec_rule_value[ AMP_Rule_Spec::ALTERNATIVE_NAMES ] as $alternative_attr_name ) { - if ( $node->hasAttribute( $alternative_attr_name ) ) { - $attribute_exists = true; - break; - } - } - } - } - if ( $is_mandatory && ! $attribute_exists ) { + if ( ! $node->hasAttribute( $attr_name ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_mandatory( $node, $attr_name, $attr_spec_rule_value ) ) { return true; } } From 3bb3d8e5be22651e71bd7a653dff039a004c63c8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 22 Nov 2019 16:37:06 -0800 Subject: [PATCH 07/39] Introduce specific error codes for attribute value violations --- .../sanitizers/class-amp-base-sanitizer.php | 20 +++- .../class-amp-tag-and-attribute-sanitizer.php | 111 ++++++++---------- 2 files changed, 63 insertions(+), 68 deletions(-) diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 88ce842dd35..ac46f50b203 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -472,9 +472,10 @@ public function remove_invalid_child( $node, $validation_error = [] ) { * @param DOMElement $element The node for which to remove the attribute. * @param DOMAttr|string $attribute The attribute to remove from the element. * @param array $validation_error Validation error details. + * @param array $attr_spec Attribute spec. * @return bool Whether the node should have been removed, that is, that the node was sanitized for validity. */ - public function remove_invalid_attribute( $element, $attribute, $validation_error = [] ) { + public function remove_invalid_attribute( $element, $attribute, $validation_error = [], $attr_spec = [] ) { if ( $this->is_exempt_from_validation( $element ) ) { return false; } @@ -484,10 +485,23 @@ public function remove_invalid_attribute( $element, $attribute, $validation_erro } else { $node = $attribute; } + + // Catch edge condition (no known possible way to reach). + if ( ! ( $node instanceof DOMAttr ) || $element !== $node->parentNode ) { + return false; + } + $should_remove = $this->should_sanitize_validation_error( $validation_error, compact( 'node' ) ); - if ( $should_remove && $element === $node->parentNode ) { - $element->removeAttributeNode( $node ); + if ( $should_remove ) { + $allow_empty = ! empty( $attr_spec[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ); + $is_href_attr = ( isset( $attr_spec[ AMP_Rule_Spec::VALUE_URL ] ) && 'href' === $node->nodeName ); + if ( $allow_empty && ! $is_href_attr ) { + $node->nodeValue = ''; + } else { + $element->removeAttributeNode( $node ); + } } + return $should_remove; } diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 9ce6a39f5fe..caede09b0e4 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -556,11 +556,8 @@ private function process_node( DOMElement $node ) { } } - // Identify any remaining disallowed attributes. - $disallowed_attributes = $this->get_disallowed_attributes_in_node( $node, $merged_attr_spec_list ); - // Identify attribute values that don't conform to the attr_spec. - $disallowed_attributes = $this->sanitize_disallowed_attribute_values_in_node( $node, $merged_attr_spec_list, $disallowed_attributes ); + $disallowed_attributes = $this->sanitize_disallowed_attribute_values_in_node( $node, $merged_attr_spec_list ); // Remove all invalid attributes. if ( ! empty( $disallowed_attributes ) ) { @@ -575,9 +572,20 @@ private function process_node( DOMElement $node ) { $validation_error['element_attributes'][ $attribute->nodeName ] = $attribute->nodeValue; } $removed_attributes = []; + + // @todo First needing to iterate over the $disallowed_attributes and if any are mandatory, skip removing attributes and instead remove the element. foreach ( $disallowed_attributes as $disallowed_attribute ) { - if ( $this->remove_invalid_attribute( $node, $disallowed_attribute, $validation_error ) ) { - $removed_attributes[] = $disallowed_attribute; + /** + * Returned vars. + * + * @var DOMAttr $attr_node + * @var string $error_code + */ + list( $attr_node, $error_code ) = $disallowed_attribute; + $validation_error['code'] = $error_code; + + if ( $this->remove_invalid_attribute( $node, $attr_node, $validation_error, $merged_attr_spec_list ) ) { + $removed_attributes[] = $attr_node; } } @@ -874,29 +882,6 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ return $score; } - /** - * Remove attributes from $node that are not listed in $allowed_attrs. - * - * @param DOMElement $node Node. - * @param array[] $attr_spec_list Attribute spec list. - * @return DOMAttr[] Attributes to remove. - */ - private function get_disallowed_attributes_in_node( DOMElement $node, $attr_spec_list ) { - /* - * We can't remove attributes inside the 'foreach' loop without - * breaking the iteration. So we keep track of the attributes to - * remove in the first loop, then remove them in the second loop. - */ - $attrs_to_remove = []; - foreach ( $node->attributes as $attr_name => $attr_node ) { - if ( ! $this->is_amp_allowed_attribute( $attr_node, $attr_spec_list ) ) { - $attrs_to_remove[] = $attr_node; - } - } - - return $attrs_to_remove; - } - /** * Remove invalid AMP attributes values from $node that have been implicitly disallowed. * @@ -904,12 +889,11 @@ private function get_disallowed_attributes_in_node( DOMElement $node, $attr_spec * * @see \AMP_Tag_And_Attribute_Sanitizer::validate_attr_spec_list_for_node() * - * @param DOMElement $node Node. - * @param array[][] $attr_spec_list Attribute spec list. - * @param DOMAttr[] $attributes_pending_removal Attributes pending removal. - * @return DOMAttr[]|false Attributes to remove, or false if the element itself should be removed. + * @param DOMElement $node Node. + * @param array[] $attr_spec_list Attribute spec list. + * @return array Tuples containing attribute to remove and error code. */ - private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, $attr_spec_list, $attributes_pending_removal ) { + private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, $attr_spec_list ) { $attrs_to_remove = []; foreach ( $attr_spec_list as $attr_name => $attr_val ) { @@ -921,8 +905,18 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, } foreach ( $node->attributes as $attr_name => $attr_node ) { + /* + * We can't remove attributes inside the 'foreach' loop without + * breaking the iteration. So we keep track of the attributes to + * remove in the first loop, then remove them in the second loop. + */ + if ( ! $this->is_amp_allowed_attribute( $attr_node, $attr_spec_list ) ) { + $attrs_to_remove[] = [ $attr_node, AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE ]; + continue; + } - if ( ! isset( $attr_spec_list[ $attr_name ] ) || in_array( $attr_node, $attributes_pending_removal, true ) ) { + // Skip unspecified attribute, likely being data-* attribute. + if ( ! isset( $attr_spec_list[ $attr_name ] ) ) { continue; } @@ -932,8 +926,8 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, continue; } - $should_remove_node = false; - $attr_spec_rule = $attr_spec_list[ $attr_name ]; + $error_code = null; + $attr_spec_rule = $attr_spec_list[ $attr_name ]; /* * Note that the following checks may have been previously done in validate_attr_spec_list_for_node(): @@ -948,61 +942,48 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, * However, if there was only one spec for a given tag, then then validate_attr_spec_list_for_node() would * not have been called. and thus these checks need to be performed here as well. */ + // @todo Actual AMP validator error codes should be used whenever possible. if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_value'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_CASEI ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_casei( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_case_insensitive_value'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_REGEX ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_regex( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_value_for_pattern'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_REGEX_CASEI ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_regex_casei( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_value_for_case_insensitive_pattern'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOWED_PROTOCOL ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_allowed_protocol( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_url_protocol'; // @todo A javascript: protocol could be treated differently. } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_valid_url( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'invalid_url'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_RELATIVE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_relative( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_relative_url'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_empty( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_empty_value'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::DISALLOWED_DOMAIN ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_domain( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_url_host'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::BLACKLISTED_VALUE_REGEX ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_blacklisted_value_regex( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_value_by_pattern'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_PROPERTIES ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_properties( $node, $attr_name, $attr_spec_rule ) ) { - $should_remove_node = true; + $error_code = 'illegal_value_properties'; // @todo Which property(s) in particular? } - if ( $should_remove_node ) { - $attrs_to_remove[] = $attr_node; + if ( isset( $error_code ) ) { + $attrs_to_remove[] = [ $attr_node, $error_code ]; } } - // @todo Do not actually mutate the attributes here, but rather pass back what should be done? - // Remove the disallowed values. - foreach ( $attrs_to_remove as $attr_node ) { - if ( isset( $attr_spec_list[ $attr_node->nodeName ][ AMP_Rule_Spec::VALUE_URL ] ) && - 'href' === $attr_node->nodeName ) { - $attributes_pending_removal[] = $attr_node; - } elseif ( isset( $attr_spec_list[ $attr_node->nodeName ][ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) && - ( true === $attr_spec_list[ $attr_node->nodeName ][ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) ) { - $attr_node->nodeValue = ''; - } else { - $attributes_pending_removal[] = $attr_node; - } - } - - return $attributes_pending_removal; + return $attrs_to_remove; } /** From ad66b8f195167083581682c97e32f151e0a6e3e2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 22 Nov 2019 17:08:48 -0800 Subject: [PATCH 08/39] Include list of missing mandatory attributes in validation error --- .../class-amp-tag-and-attribute-sanitizer.php | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index caede09b0e4..17328bc6483 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -605,8 +605,15 @@ private function process_node( DOMElement $node ) { // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. // After attributes have been sanitized (and potentially removed), if mandatory attribute(s) are missing, remove the element. - if ( $this->is_missing_mandatory_attribute( $merged_attr_spec_list, $node ) ) { - $this->remove_invalid_child( $node, [ 'code' => 'missing_mandatory_attribute' ] ); + $missing_mandatory_attributes = $this->get_missing_mandatory_attributes( $merged_attr_spec_list, $node ); + if ( ! empty( $missing_mandatory_attributes ) ) { + $this->remove_invalid_child( + $node, + [ + 'code' => 'missing_mandatory_attribute', + 'attributes' => $missing_mandatory_attributes, + ] + ); return null; } @@ -649,18 +656,30 @@ private function process_node( DOMElement $node ) { * * @param array $attr_spec The attribute specification. * @param DOMElement $node The DOMElement of the node to check. - * @return boolean $is_missing boolean Whether a required attribute is missing. + * @return bool $is_missing boolean Whether a required attribute is missing. */ public function is_missing_mandatory_attribute( $attr_spec, DOMElement $node ) { + return 0 === count( $this->get_missing_mandatory_attributes( $attr_spec, $node ) ); + } + + /** + * Get list of mandatory missing mandatory attributes. + * + * @param array $attr_spec The attribute specification. + * @param DOMElement $node The DOMElement of the node to check. + * @return string[] Names of missing attributes. + */ + private function get_missing_mandatory_attributes( $attr_spec, DOMElement $node ) { + $missing_attributes = []; foreach ( $attr_spec as $attr_name => $attr_spec_rule_value ) { if ( '\u' === substr( $attr_name, 0, 2 ) ) { $attr_name = html_entity_decode( '&#x' . substr( $attr_name, 2 ) . ';' ); // Probably ⚡. } if ( ! $node->hasAttribute( $attr_name ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_mandatory( $node, $attr_name, $attr_spec_rule_value ) ) { - return true; + $missing_attributes[] = $attr_name; } } - return false; + return $missing_attributes; } /** @@ -942,7 +961,8 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, * However, if there was only one spec for a given tag, then then validate_attr_spec_list_for_node() would * not have been called. and thus these checks need to be performed here as well. */ - // @todo Actual AMP validator error codes should be used whenever possible. + // @todo Actual AMP validator error codes should be used whenever possible. And constants should be used. + // @todo The check methods should return an array of validation error data when failure. if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value( $node, $attr_name, $attr_spec_rule ) ) { $error_code = 'illegal_value'; @@ -957,7 +977,7 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, $error_code = 'illegal_value_for_case_insensitive_pattern'; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOWED_PROTOCOL ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_allowed_protocol( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_url_protocol'; // @todo A javascript: protocol could be treated differently. + $error_code = 'illegal_url_protocol'; // @todo A javascript: protocol could be treated differently. It should have a JS error type. } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_valid_url( $node, $attr_name, $attr_spec_rule ) ) { $error_code = 'invalid_url'; From 35638c528199cbc8c6705af9f380297d2003f3ba Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 22 Nov 2019 23:35:25 -0800 Subject: [PATCH 09/39] Extract AMP validator error codes and messages from spec --- bin/amphtml-update.py | 48 ++- .../class-amp-allowed-tags-generated.php | 335 ++++++++++++++++++ 2 files changed, 381 insertions(+), 2 deletions(-) diff --git a/bin/amphtml-update.py b/bin/amphtml-update.py index c3a0eaf0d45..c9d4c669722 100644 --- a/bin/amphtml-update.py +++ b/bin/amphtml-update.py @@ -94,7 +94,7 @@ def GeneratePHP(out_dir): """ logging.info('entering ...') - allowed_tags, attr_lists, descendant_lists, reference_points, versions = ParseRules(out_dir) + allowed_tags, attr_lists, descendant_lists, reference_points, versions, error_formats = ParseRules(out_dir) #Generate the output out = [] @@ -105,6 +105,7 @@ def GeneratePHP(out_dir): GenerateLayoutAttributesPHP(out, attr_lists) GenerateGlobalAttributesPHP(out, attr_lists) GenerateReferencePointsPHP(out, reference_points) + GenerateErrorFormatsPHP(out, error_formats) GenerateFooterPHP(out) # join out array into a single string and remove unneeded whitespace @@ -196,6 +197,40 @@ def GenerateReferencePointsPHP(out, reference_points): out.append('') logging.info('... done') +def GenerateErrorFormatsPHP(out, error_formats): + logging.info('entering ...') + + # TODO: Delete the error formats which are not used! + for code in error_formats.keys(): + out.append('\tconst %s = \'%s\';' % ( code, code )) + + out.append(''' + /** + * Get error message format. + * + * @since 1.5.0 + * @internal + * + * @param string $code Error code. + * @return string Error message format. + */ + public static function get_error_message_format( $code ) { + switch ( $code ) {''') + + for ( code, message_format ) in error_formats.items(): + out.append('\t\t\tcase \'%s\':' % code) + message_format = re.sub( r'%(\d+)', r'%\1$s', message_format ) + out.append('\t\t\t\treturn __( %s, \'amp\' );' % Phpize( message_format )) + + out.append(''' + default: + return __( 'Unknown error', 'amp' ); + } + }''') + + logging.info('... done') + + def GenerateFooterPHP(out): logging.info('entering ...') @@ -333,6 +368,8 @@ def ParseRules(out_dir): descendant_lists = {} reference_points = {} versions = {} + error_formats = {} # TODO: Include some additional error codes not accounted for in AMP Validator. + validation_error_code_lookup = {} specfile='%s/validator.protoascii' % out_dir @@ -340,6 +377,10 @@ def ParseRules(out_dir): rules = validator_pb2.ValidatorRules() text_format.Merge(open(specfile).read(), rules) + validationError = validator_pb2.ValidationError() + for ( name, code ) in validationError.Code.items(): + validation_error_code_lookup[ code ] = name + # Record the version of this specfile and the corresponding validator version. if rules.HasField('spec_file_revision'): versions['spec_file_revision'] = rules.spec_file_revision @@ -404,9 +445,12 @@ def ParseRules(out_dir): continue descendant_lists[list.name].append( val.lower() ) + elif 'error_formats' == field_desc.name: + for list in field_val: + error_formats[ validation_error_code_lookup[list.code] ] = list.format logging.info('... done') - return allowed_tags, attr_lists, descendant_lists, reference_points, versions + return allowed_tags, attr_lists, descendant_lists, reference_points, versions, error_formats def GetTagSpec(tag_spec, attr_lists): diff --git a/includes/sanitizers/class-amp-allowed-tags-generated.php b/includes/sanitizers/class-amp-allowed-tags-generated.php index 65d3e7d65be..782e50ad99e 100644 --- a/includes/sanitizers/class-amp-allowed-tags-generated.php +++ b/includes/sanitizers/class-amp-allowed-tags-generated.php @@ -17584,6 +17584,341 @@ class AMP_Allowed_Tags_Generated { ), ); + const CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR = 'CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR'; + const WARNING_EXTENSION_UNUSED = 'WARNING_EXTENSION_UNUSED'; + const CSS_SYNTAX_NOT_A_SELECTOR_START = 'CSS_SYNTAX_NOT_A_SELECTOR_START'; + const DOCUMENT_TOO_COMPLEX = 'DOCUMENT_TOO_COMPLEX'; + const MANDATORY_TAG_ANCESTOR_WITH_HINT = 'MANDATORY_TAG_ANCESTOR_WITH_HINT'; + const CSS_SYNTAX_BAD_URL = 'CSS_SYNTAX_BAD_URL'; + const MANDATORY_ATTR_MISSING = 'MANDATORY_ATTR_MISSING'; + const DEPRECATED_ATTR = 'DEPRECATED_ATTR'; + const TAG_REFERENCE_POINT_CONFLICT = 'TAG_REFERENCE_POINT_CONFLICT'; + const CSS_SYNTAX_STRAY_TRAILING_BACKSLASH = 'CSS_SYNTAX_STRAY_TRAILING_BACKSLASH'; + const STYLESHEET_AND_INLINE_STYLE_TOO_LONG = 'STYLESHEET_AND_INLINE_STYLE_TOO_LONG'; + const UNESCAPED_TEMPLATE_IN_ATTR_VALUE = 'UNESCAPED_TEMPLATE_IN_ATTR_VALUE'; + const GENERAL_DISALLOWED_TAG = 'GENERAL_DISALLOWED_TAG'; + const SPECIFIED_LAYOUT_INVALID = 'SPECIFIED_LAYOUT_INVALID'; + const CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR = 'CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR'; + const CDATA_VIOLATES_BLACKLIST = 'CDATA_VIOLATES_BLACKLIST'; + const CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE = 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE'; + const MANDATORY_TAG_ANCESTOR = 'MANDATORY_TAG_ANCESTOR'; + const WARNING_TAG_REQUIRED_BY_MISSING = 'WARNING_TAG_REQUIRED_BY_MISSING'; + const TAG_EXCLUDED_BY_TAG = 'TAG_EXCLUDED_BY_TAG'; + const INVALID_PROPERTY_VALUE_IN_ATTR_VALUE = 'INVALID_PROPERTY_VALUE_IN_ATTR_VALUE'; + const MANDATORY_REFERENCE_POINT_MISSING = 'MANDATORY_REFERENCE_POINT_MISSING'; + const MISSING_URL = 'MISSING_URL'; + const MANDATORY_ANYOF_ATTR_MISSING = 'MANDATORY_ANYOF_ATTR_MISSING'; + const DISALLOWED_TAG = 'DISALLOWED_TAG'; + const CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT = 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT'; + const DOCUMENT_SIZE_LIMIT_EXCEEDED = 'DOCUMENT_SIZE_LIMIT_EXCEEDED'; + const INVALID_URL_PROTOCOL = 'INVALID_URL_PROTOCOL'; + const MANDATORY_CDATA_MISSING_OR_INCORRECT = 'MANDATORY_CDATA_MISSING_OR_INCORRECT'; + const INCORRECT_NUM_CHILD_TAGS = 'INCORRECT_NUM_CHILD_TAGS'; + const INVALID_UTF8 = 'INVALID_UTF8'; + const ATTR_VALUE_REQUIRED_BY_LAYOUT = 'ATTR_VALUE_REQUIRED_BY_LAYOUT'; + const DUPLICATE_UNIQUE_TAG = 'DUPLICATE_UNIQUE_TAG'; + const MISSING_REQUIRED_EXTENSION = 'MISSING_REQUIRED_EXTENSION'; + const MUTUALLY_EXCLUSIVE_ATTRS = 'MUTUALLY_EXCLUSIVE_ATTRS'; + const DISALLOWED_FIRST_CHILD_TAG_NAME = 'DISALLOWED_FIRST_CHILD_TAG_NAME'; + const MANDATORY_LAST_CHILD_TAG = 'MANDATORY_LAST_CHILD_TAG'; + const ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT = 'ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT'; + const DISALLOWED_RELATIVE_URL = 'DISALLOWED_RELATIVE_URL'; + const MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE = 'MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE'; + const MANDATORY_TAG_MISSING = 'MANDATORY_TAG_MISSING'; + const CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS = 'CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS'; + const CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE = 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE'; + const CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE = 'CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE'; + const CSS_SYNTAX_DISALLOWED_DOMAIN = 'CSS_SYNTAX_DISALLOWED_DOMAIN'; + const TEMPLATE_PARTIAL_IN_ATTR_VALUE = 'TEMPLATE_PARTIAL_IN_ATTR_VALUE'; + const MANDATORY_ONEOF_ATTR_MISSING = 'MANDATORY_ONEOF_ATTR_MISSING'; + const WRONG_PARENT_TAG = 'WRONG_PARENT_TAG'; + const CSS_SYNTAX_UNTERMINATED_COMMENT = 'CSS_SYNTAX_UNTERMINATED_COMMENT'; + const DUPLICATE_ATTRIBUTE = 'DUPLICATE_ATTRIBUTE'; + const DUPLICATE_DIMENSION = 'DUPLICATE_DIMENSION'; + const CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME = 'CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME'; + const CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH = 'CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH'; + const STYLESHEET_TOO_LONG = 'STYLESHEET_TOO_LONG'; + const TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS = 'TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS'; + const INVALID_URL = 'INVALID_URL'; + const CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR = 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR'; + const VALUE_SET_MISMATCH = 'VALUE_SET_MISMATCH'; + const CSS_SYNTAX_INVALID_URL = 'CSS_SYNTAX_INVALID_URL'; + const MISSING_LAYOUT_ATTRIBUTES = 'MISSING_LAYOUT_ATTRIBUTES'; + const CSS_SYNTAX_MISSING_SELECTOR = 'CSS_SYNTAX_MISSING_SELECTOR'; + const CSS_SYNTAX_INVALID_PROPERTY = 'CSS_SYNTAX_INVALID_PROPERTY'; + const CSS_SYNTAX_INVALID_ATTR_SELECTOR = 'CSS_SYNTAX_INVALID_ATTR_SELECTOR'; + const CSS_SYNTAX_INVALID_URL_PROTOCOL = 'CSS_SYNTAX_INVALID_URL_PROTOCOL'; + const DISALLOWED_MANUFACTURED_BODY = 'DISALLOWED_MANUFACTURED_BODY'; + const CSS_SYNTAX_INCOMPLETE_DECLARATION = 'CSS_SYNTAX_INCOMPLETE_DECLARATION'; + const IMPLIED_LAYOUT_INVALID = 'IMPLIED_LAYOUT_INVALID'; + const CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION = 'CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION'; + const DISALLOWED_CHILD_TAG_NAME = 'DISALLOWED_CHILD_TAG_NAME'; + const CSS_SYNTAX_MISSING_URL = 'CSS_SYNTAX_MISSING_URL'; + const CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE = 'CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE'; + const TAG_REQUIRED_BY_MISSING = 'TAG_REQUIRED_BY_MISSING'; + const INCORRECT_MIN_NUM_CHILD_TAGS = 'INCORRECT_MIN_NUM_CHILD_TAGS'; + const DISALLOWED_SCRIPT_TAG = 'DISALLOWED_SCRIPT_TAG'; + const CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME = 'CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME'; + const DISALLOWED_TAG_ANCESTOR = 'DISALLOWED_TAG_ANCESTOR'; + const DUPLICATE_UNIQUE_TAG_WARNING = 'DUPLICATE_UNIQUE_TAG_WARNING'; + const NON_WHITESPACE_CDATA_ENCOUNTERED = 'NON_WHITESPACE_CDATA_ENCOUNTERED'; + const WARNING_EXTENSION_DEPRECATED_VERSION = 'WARNING_EXTENSION_DEPRECATED_VERSION'; + const CSS_SYNTAX_INVALID_AT_RULE = 'CSS_SYNTAX_INVALID_AT_RULE'; + const DISALLOWED_ATTR = 'DISALLOWED_ATTR'; + const CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT = 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT'; + const CSS_SYNTAX_INVALID_DECLARATION = 'CSS_SYNTAX_INVALID_DECLARATION'; + const ATTR_MISSING_REQUIRED_EXTENSION = 'ATTR_MISSING_REQUIRED_EXTENSION'; + const CSS_SYNTAX_DISALLOWED_RELATIVE_URL = 'CSS_SYNTAX_DISALLOWED_RELATIVE_URL'; + const DEPRECATED_TAG = 'DEPRECATED_TAG'; + const BASE_TAG_MUST_PRECEED_ALL_URLS = 'BASE_TAG_MUST_PRECEED_ALL_URLS'; + const CSS_SYNTAX_INVALID_PROPERTY_NOLIST = 'CSS_SYNTAX_INVALID_PROPERTY_NOLIST'; + const ATTR_REQUIRED_BUT_MISSING = 'ATTR_REQUIRED_BUT_MISSING'; + const INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT = 'INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT'; + const UNKNOWN_CODE = 'UNKNOWN_CODE'; + const ATTR_DISALLOWED_BY_IMPLIED_LAYOUT = 'ATTR_DISALLOWED_BY_IMPLIED_LAYOUT'; + const DISALLOWED_PROPERTY_IN_ATTR_VALUE = 'DISALLOWED_PROPERTY_IN_ATTR_VALUE'; + const DUPLICATE_REFERENCE_POINT = 'DUPLICATE_REFERENCE_POINT'; + const TEMPLATE_IN_ATTR_NAME = 'TEMPLATE_IN_ATTR_NAME'; + const DISALLOWED_STYLE_ATTR = 'DISALLOWED_STYLE_ATTR'; + const CSS_EXCESSIVELY_NESTED = 'CSS_EXCESSIVELY_NESTED'; + const CSS_SYNTAX_MALFORMED_MEDIA_QUERY = 'CSS_SYNTAX_MALFORMED_MEDIA_QUERY'; + const CSS_SYNTAX_UNTERMINATED_STRING = 'CSS_SYNTAX_UNTERMINATED_STRING'; + const INVALID_ATTR_VALUE = 'INVALID_ATTR_VALUE'; + const EXTENSION_UNUSED = 'EXTENSION_UNUSED'; + const DISALLOWED_DOMAIN = 'DISALLOWED_DOMAIN'; + const INVALID_JSON_CDATA = 'INVALID_JSON_CDATA'; + const CSS_SYNTAX_DISALLOWED_MEDIA_TYPE = 'CSS_SYNTAX_DISALLOWED_MEDIA_TYPE'; + const INLINE_STYLE_TOO_LONG = 'INLINE_STYLE_TOO_LONG'; + const DEV_MODE_ONLY = 'DEV_MODE_ONLY'; + + /** + * Get error message format. + * + * @since 1.5.0 + * @internal + * + * @param string $code Error code. + * @return string Error message format. + */ + public static function get_error_message_format( $code ) { + switch ( $code ) { + case 'CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR': + return __( 'CSS syntax error in tag \'%1$s\' - invalid pseudo selector.', 'amp' ); + case 'WARNING_EXTENSION_UNUSED': + return __( 'The extension \'%1$s\' was found on this page, but is unused (no \'%2$s\' tag seen). This may become an error in the future.', 'amp' ); + case 'CSS_SYNTAX_NOT_A_SELECTOR_START': + return __( 'CSS syntax error in tag \'%1$s\' - not a selector start.', 'amp' ); + case 'DOCUMENT_TOO_COMPLEX': + return __( 'The document is too complex.', 'amp' ); + case 'MANDATORY_TAG_ANCESTOR_WITH_HINT': + return __( 'The tag \'%1$s\' may only appear as a descendant of tag \'%2$s\'. Did you mean \'%3$s\'?', 'amp' ); + case 'CSS_SYNTAX_BAD_URL': + return __( 'CSS syntax error in tag \'%1$s\' - bad url.', 'amp' ); + case 'MANDATORY_ATTR_MISSING': + return __( 'The mandatory attribute \'%1$s\' is missing in tag \'%2$s\'.', 'amp' ); + case 'DEPRECATED_ATTR': + return __( 'The attribute \'%1$s\' in tag \'%2$s\' is deprecated - use \'%3$s\' instead.', 'amp' ); + case 'TAG_REFERENCE_POINT_CONFLICT': + return __( 'The tag \'%1$s\' conflicts with reference point \'%2$s\' because both define reference points.', 'amp' ); + case 'CSS_SYNTAX_STRAY_TRAILING_BACKSLASH': + return __( 'CSS syntax error in tag \'%1$s\' - stray trailing backslash.', 'amp' ); + case 'STYLESHEET_AND_INLINE_STYLE_TOO_LONG': + return __( 'The author stylesheet specified in tag \'style amp-custom\' and the combined inline styles is too large - document contains %1$s bytes whereas the limit is %2$s bytes.', 'amp' ); + case 'UNESCAPED_TEMPLATE_IN_ATTR_VALUE': + return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to \'%3$s\', which contains unescaped Mustache template syntax.', 'amp' ); + case 'GENERAL_DISALLOWED_TAG': + return __( 'The tag \'%1$s\' is disallowed except in specific forms.', 'amp' ); + case 'SPECIFIED_LAYOUT_INVALID': + return __( 'The specified layout \'%1$s\' is not supported by tag \'%2$s\'.', 'amp' ); + case 'CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR': + return __( 'CSS syntax error in tag \'%1$s\' - unparsed input remains in selector.', 'amp' ); + case 'CDATA_VIOLATES_BLACKLIST': + return __( 'The text inside tag \'%1$s\' contains \'%2$s\', which is disallowed.', 'amp' ); + case 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE': + return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed within @%3$s. Allowed properties: %4$s.', 'amp' ); + case 'MANDATORY_TAG_ANCESTOR': + return __( 'The tag \'%1$s\' may only appear as a descendant of tag \'%2$s\'.', 'amp' ); + case 'WARNING_TAG_REQUIRED_BY_MISSING': + return __( 'The tag \'%1$s\' is missing or incorrect, but required by \'%2$s\'. This will soon be an error.', 'amp' ); + case 'TAG_EXCLUDED_BY_TAG': + return __( 'The tag \'%1$s\' is present, but is excluded by the presence of \'%2$s\'.', 'amp' ); + case 'INVALID_PROPERTY_VALUE_IN_ATTR_VALUE': + return __( 'The property \'%1$s\' in attribute \'%2$s\' in tag \'%3$s\' is set to \'%4$s\', which is invalid.', 'amp' ); + case 'MANDATORY_REFERENCE_POINT_MISSING': + return __( 'The mandatory reference point \'%1$s\' for \'%2$s\' is missing.', 'amp' ); + case 'MISSING_URL': + return __( 'Missing URL for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); + case 'MANDATORY_ANYOF_ATTR_MISSING': + return __( 'The tag \'%1$s\' is missing a mandatory attribute - pick at least one of %2$s.', 'amp' ); + case 'DISALLOWED_TAG': + return __( 'The tag \'%1$s\' is disallowed.', 'amp' ); + case 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT': + return __( 'The tag \'%1$s\', a child tag of \'%2$s\', does not satisfy one of the acceptable reference points: %3$s.', 'amp' ); + case 'DOCUMENT_SIZE_LIMIT_EXCEEDED': + return __( 'Document exceeded %1$s bytes limit. Actual size %2$s bytes.', 'amp' ); + case 'INVALID_URL_PROTOCOL': + return __( 'Invalid URL protocol \'%3$s:\' for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); + case 'MANDATORY_CDATA_MISSING_OR_INCORRECT': + return __( 'The mandatory text inside tag \'%1$s\' is missing or incorrect.', 'amp' ); + case 'INCORRECT_NUM_CHILD_TAGS': + return __( 'Tag \'%1$s\' must have %2$s child tags - saw %3$s child tags.', 'amp' ); + case 'INVALID_UTF8': + return __( 'The document contains invalid UTF8.', 'amp' ); + case 'ATTR_VALUE_REQUIRED_BY_LAYOUT': + return __( 'Invalid value \'%1$s\' for attribute \'%2$s\' in tag \'%3$s\' - for layout \'%4$s\', set the attribute \'%2$s\' to value \'%5$s\'.', 'amp' ); + case 'DUPLICATE_UNIQUE_TAG': + return __( 'The tag \'%1$s\' appears more than once in the document.', 'amp' ); + case 'MISSING_REQUIRED_EXTENSION': + return __( 'The tag \'%1$s\' requires including the \'%2$s\' extension JavaScript.', 'amp' ); + case 'MUTUALLY_EXCLUSIVE_ATTRS': + return __( 'Mutually exclusive attributes encountered in tag \'%1$s\' - pick one of %2$s.', 'amp' ); + case 'DISALLOWED_FIRST_CHILD_TAG_NAME': + return __( 'Tag \'%1$s\' is disallowed as first child of tag \'%2$s\'. First child tag must be one of %3$s.', 'amp' ); + case 'MANDATORY_LAST_CHILD_TAG': + return __( 'Tag \'%1$s\', if present, must be the last child of tag \'%2$s\'.', 'amp' ); + case 'ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT': + return __( 'The attribute \'%1$s\' in tag \'%2$s\' is disallowed by specified layout \'%3$s\'.', 'amp' ); + case 'DISALLOWED_RELATIVE_URL': + return __( 'The relative URL \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\' is disallowed.', 'amp' ); + case 'MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE': + return __( 'The property \'%1$s\' is missing from attribute \'%2$s\' in tag \'%3$s\'.', 'amp' ); + case 'MANDATORY_TAG_MISSING': + return __( 'The mandatory tag \'%1$s\' is missing or incorrect.', 'amp' ); + case 'CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS': + return __( 'CSS syntax error in tag \'%1$s\' - qualified rule \'%2$s\' has no declarations.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE': + return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is set to the disallowed value \'%3$s\'.', 'amp' ); + case 'CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE': + return __( 'CSS syntax error in tag \'%1$s\' - end of stylesheet encountered in prelude of a qualified rule.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_DOMAIN': + return __( 'CSS syntax error in tag \'%1$s\' - invalid domain \'%2$s\'.', 'amp' ); + case 'TEMPLATE_PARTIAL_IN_ATTR_VALUE': + return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to \'%3$s\', which contains a Mustache template partial.', 'amp' ); + case 'MANDATORY_ONEOF_ATTR_MISSING': + return __( 'The tag \'%1$s\' is missing a mandatory attribute - pick one of %2$s.', 'amp' ); + case 'WRONG_PARENT_TAG': + return __( 'The parent tag of tag \'%1$s\' is \'%2$s\', but it can only be \'%3$s\'.', 'amp' ); + case 'CSS_SYNTAX_UNTERMINATED_COMMENT': + return __( 'CSS syntax error in tag \'%1$s\' - unterminated comment.', 'amp' ); + case 'DUPLICATE_ATTRIBUTE': + return __( 'The tag \'%1$s\' contains the attribute \'%2$s\' repeated multiple times.', 'amp' ); + case 'DUPLICATE_DIMENSION': + return __( 'Multiple image candidates with the same width or pixel density found in attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME': + return __( 'CSS syntax error in tag \'%1$s\' - qualified rule \'%2$s\' must be located inside of a keyframe.', 'amp' ); + case 'CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH': + return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed together with \'%3$s\'. Allowed properties: %4$s.', 'amp' ); + case 'STYLESHEET_TOO_LONG': + return __( 'The author stylesheet specified in tag \'%1$s\' is too long - document contains %2$s bytes whereas the limit is %3$s bytes.', 'amp' ); + case 'TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS': + return __( 'Tag \'%1$s\' is not allowed to have any sibling tags (\'%2$s\' should only have 1 child).', 'amp' ); + case 'INVALID_URL': + return __( 'Malformed URL \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); + case 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR': + return __( 'The tag \'%1$s\', a child tag of \'%2$s\', does not satisfy the reference point \'%3$s\'.', 'amp' ); + case 'VALUE_SET_MISMATCH': + return __( 'Attribute \'%1$s\' in tag \'%2$s\' contains a value that does not match any other tags on the page.', 'amp' ); + case 'CSS_SYNTAX_INVALID_URL': + return __( 'CSS syntax error in tag \'%1$s\' - invalid url \'%2$s\'.', 'amp' ); + case 'MISSING_LAYOUT_ATTRIBUTES': + return __( 'Incomplete layout attributes specified for tag \'%1$s\'. For example, provide attributes \'width\' and \'height\'.', 'amp' ); + case 'CSS_SYNTAX_MISSING_SELECTOR': + return __( 'CSS syntax error in tag \'%1$s\' - missing selector.', 'amp' ); + case 'CSS_SYNTAX_INVALID_PROPERTY': + return __( 'CSS syntax error in tag \'%1$s\' - invalid property \'%2$s\'. The only allowed properties are \'%3$s\'.', 'amp' ); + case 'CSS_SYNTAX_INVALID_ATTR_SELECTOR': + return __( 'CSS syntax error in tag \'%1$s\' - invalid attribute selector.', 'amp' ); + case 'CSS_SYNTAX_INVALID_URL_PROTOCOL': + return __( 'CSS syntax error in tag \'%1$s\' - invalid url protocol \'%2$s:\'.', 'amp' ); + case 'DISALLOWED_MANUFACTURED_BODY': + return __( 'Tag or text which is only allowed inside the body section found outside of the body section.', 'amp' ); + case 'CSS_SYNTAX_INCOMPLETE_DECLARATION': + return __( 'CSS syntax error in tag \'%1$s\' - incomplete declaration.', 'amp' ); + case 'IMPLIED_LAYOUT_INVALID': + return __( 'The implied layout \'%1$s\' is not supported by tag \'%2$s\'.', 'amp' ); + case 'CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION': + return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed unless the enclosing rule is prefixed with the \'%3$s\' qualification.', 'amp' ); + case 'DISALLOWED_CHILD_TAG_NAME': + return __( 'Tag \'%1$s\' is disallowed as child of tag \'%2$s\'. Child tag must be one of %3$s.', 'amp' ); + case 'CSS_SYNTAX_MISSING_URL': + return __( 'CSS syntax error in tag \'%1$s\' - missing url.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE': + return __( 'CSS syntax error in tag \'%1$s\' - disallowed media feature \'%2$s\'.', 'amp' ); + case 'TAG_REQUIRED_BY_MISSING': + return __( 'The tag \'%1$s\' is missing or incorrect, but required by \'%2$s\'.', 'amp' ); + case 'INCORRECT_MIN_NUM_CHILD_TAGS': + return __( 'Tag \'%1$s\' must have a minimum of %2$s child tags - saw %3$s child tags.', 'amp' ); + case 'DISALLOWED_SCRIPT_TAG': + return __( 'Custom JavaScript is not allowed.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME': + return __( 'CSS syntax error in tag \'%1$s\' - keyframe inside keyframe is not allowed.', 'amp' ); + case 'DISALLOWED_TAG_ANCESTOR': + return __( 'The tag \'%1$s\' may not appear as a descendant of tag \'%2$s\'.', 'amp' ); + case 'DUPLICATE_UNIQUE_TAG_WARNING': + return __( 'The tag \'%1$s\' appears more than once in the document. This will soon be an error.', 'amp' ); + case 'NON_WHITESPACE_CDATA_ENCOUNTERED': + return __( 'The tag \'%1$s\' contains text, which is disallowed.', 'amp' ); + case 'WARNING_EXTENSION_DEPRECATED_VERSION': + return __( 'The extension \'%1$s\' is referenced at version \'%2$s\' which is a deprecated version. Please use a more recent version of this extension. This may become an error in the future.', 'amp' ); + case 'CSS_SYNTAX_INVALID_AT_RULE': + return __( 'CSS syntax error in tag \'%1$s\' - saw invalid at rule \'@%2$s\'.', 'amp' ); + case 'DISALLOWED_ATTR': + return __( 'The attribute \'%1$s\' may not appear in tag \'%2$s\'.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT': + return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is set to the disallowed value \'%3$s\'. Allowed values: %4$s.', 'amp' ); + case 'CSS_SYNTAX_INVALID_DECLARATION': + return __( 'CSS syntax error in tag \'%1$s\' - invalid declaration.', 'amp' ); + case 'ATTR_MISSING_REQUIRED_EXTENSION': + return __( 'The attribute \'%1$s\' requires including the \'%2$s\' extension JavaScript.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_RELATIVE_URL': + return __( 'CSS syntax error in tag \'%1$s\' - disallowed relative url \'%2$s\'.', 'amp' ); + case 'DEPRECATED_TAG': + return __( 'The tag \'%1$s\' is deprecated - use \'%2$s\' instead.', 'amp' ); + case 'BASE_TAG_MUST_PRECEED_ALL_URLS': + return __( 'The tag \'%1$s\', which contains URLs, was found earlier in the document than the BASE element.', 'amp' ); + case 'CSS_SYNTAX_INVALID_PROPERTY_NOLIST': + return __( 'CSS syntax error in tag \'%1$s\' - invalid property \'%2$s\'.', 'amp' ); + case 'ATTR_REQUIRED_BUT_MISSING': + return __( 'The attribute \'%1$s\' in tag \'%2$s\' is missing or incorrect, but required by attribute \'%3$s\'.', 'amp' ); + case 'INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT': + return __( 'Inconsistent units for width and height in tag \'%1$s\' - width is specified in \'%2$s\' whereas height is specified in \'%3$s\'.', 'amp' ); + case 'UNKNOWN_CODE': + return __( 'Unknown error.', 'amp' ); + case 'ATTR_DISALLOWED_BY_IMPLIED_LAYOUT': + return __( 'The attribute \'%1$s\' in tag \'%2$s\' is disallowed by implied layout \'%3$s\'.', 'amp' ); + case 'DISALLOWED_PROPERTY_IN_ATTR_VALUE': + return __( 'The property \'%1$s\' in attribute \'%2$s\' in tag \'%3$s\' is disallowed.', 'amp' ); + case 'DUPLICATE_REFERENCE_POINT': + return __( 'The reference point \'%1$s\' for \'%2$s\' must be unique but a duplicate was encountered.', 'amp' ); + case 'TEMPLATE_IN_ATTR_NAME': + return __( 'Mustache template syntax in attribute name \'%1$s\' in tag \'%2$s\'.', 'amp' ); + case 'DISALLOWED_STYLE_ATTR': + return __( 'The inline \'style\' attribute is not allowed in AMP documents. Use \'style amp-custom\' tag instead.', 'amp' ); + case 'CSS_EXCESSIVELY_NESTED': + return __( 'CSS excessively nested in tag \'%1$s\'.', 'amp' ); + case 'CSS_SYNTAX_MALFORMED_MEDIA_QUERY': + return __( 'CSS syntax error in tag \'%1$s\' - malformed media query.', 'amp' ); + case 'CSS_SYNTAX_UNTERMINATED_STRING': + return __( 'CSS syntax error in tag \'%1$s\' - unterminated string.', 'amp' ); + case 'INVALID_ATTR_VALUE': + return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to the invalid value \'%3$s\'.', 'amp' ); + case 'EXTENSION_UNUSED': + return __( 'The extension \'%1$s\' was found on this page, but is unused. Please remove this extension.', 'amp' ); + case 'DISALLOWED_DOMAIN': + return __( 'The domain \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\' is disallowed.', 'amp' ); + case 'INVALID_JSON_CDATA': + return __( 'The script tag contains invalid JSON that cannot be parsed.', 'amp' ); + case 'CSS_SYNTAX_DISALLOWED_MEDIA_TYPE': + return __( 'CSS syntax error in tag \'%1$s\' - disallowed media type \'%2$s\'.', 'amp' ); + case 'INLINE_STYLE_TOO_LONG': + return __( 'The inline style specified in tag \'%1$s\' is too long - it contains %2$s bytes whereas the limit is %3$s bytes.', 'amp' ); + case 'DEV_MODE_ONLY': + return __( 'Tag \'html\' marked with attribute \'data-ampdevmode\'. Validator will suppress errors regarding any other tag with this attribute.', 'amp' ); + + default: + return __( 'Unknown error', 'amp' ); + } + } /** * Get allowed tags. From a2e20d629cf4ae86bf1738ef2041d6b36c0d095e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 23 Nov 2019 08:10:21 -0800 Subject: [PATCH 10/39] Use constant in switch statement --- bin/amphtml-update.py | 2 +- .../class-amp-allowed-tags-generated.php | 212 +++++++++--------- 2 files changed, 107 insertions(+), 107 deletions(-) diff --git a/bin/amphtml-update.py b/bin/amphtml-update.py index c9d4c669722..7757f8f70ce 100644 --- a/bin/amphtml-update.py +++ b/bin/amphtml-update.py @@ -218,7 +218,7 @@ def GenerateErrorFormatsPHP(out, error_formats): switch ( $code ) {''') for ( code, message_format ) in error_formats.items(): - out.append('\t\t\tcase \'%s\':' % code) + out.append('\t\t\tcase self::%s:' % code) message_format = re.sub( r'%(\d+)', r'%\1$s', message_format ) out.append('\t\t\t\treturn __( %s, \'amp\' );' % Phpize( message_format )) diff --git a/includes/sanitizers/class-amp-allowed-tags-generated.php b/includes/sanitizers/class-amp-allowed-tags-generated.php index 782e50ad99e..b06a5d93158 100644 --- a/includes/sanitizers/class-amp-allowed-tags-generated.php +++ b/includes/sanitizers/class-amp-allowed-tags-generated.php @@ -17702,217 +17702,217 @@ class AMP_Allowed_Tags_Generated { */ public static function get_error_message_format( $code ) { switch ( $code ) { - case 'CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR': + case self::CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR: return __( 'CSS syntax error in tag \'%1$s\' - invalid pseudo selector.', 'amp' ); - case 'WARNING_EXTENSION_UNUSED': + case self::WARNING_EXTENSION_UNUSED: return __( 'The extension \'%1$s\' was found on this page, but is unused (no \'%2$s\' tag seen). This may become an error in the future.', 'amp' ); - case 'CSS_SYNTAX_NOT_A_SELECTOR_START': + case self::CSS_SYNTAX_NOT_A_SELECTOR_START: return __( 'CSS syntax error in tag \'%1$s\' - not a selector start.', 'amp' ); - case 'DOCUMENT_TOO_COMPLEX': + case self::DOCUMENT_TOO_COMPLEX: return __( 'The document is too complex.', 'amp' ); - case 'MANDATORY_TAG_ANCESTOR_WITH_HINT': + case self::MANDATORY_TAG_ANCESTOR_WITH_HINT: return __( 'The tag \'%1$s\' may only appear as a descendant of tag \'%2$s\'. Did you mean \'%3$s\'?', 'amp' ); - case 'CSS_SYNTAX_BAD_URL': + case self::CSS_SYNTAX_BAD_URL: return __( 'CSS syntax error in tag \'%1$s\' - bad url.', 'amp' ); - case 'MANDATORY_ATTR_MISSING': + case self::MANDATORY_ATTR_MISSING: return __( 'The mandatory attribute \'%1$s\' is missing in tag \'%2$s\'.', 'amp' ); - case 'DEPRECATED_ATTR': + case self::DEPRECATED_ATTR: return __( 'The attribute \'%1$s\' in tag \'%2$s\' is deprecated - use \'%3$s\' instead.', 'amp' ); - case 'TAG_REFERENCE_POINT_CONFLICT': + case self::TAG_REFERENCE_POINT_CONFLICT: return __( 'The tag \'%1$s\' conflicts with reference point \'%2$s\' because both define reference points.', 'amp' ); - case 'CSS_SYNTAX_STRAY_TRAILING_BACKSLASH': + case self::CSS_SYNTAX_STRAY_TRAILING_BACKSLASH: return __( 'CSS syntax error in tag \'%1$s\' - stray trailing backslash.', 'amp' ); - case 'STYLESHEET_AND_INLINE_STYLE_TOO_LONG': + case self::STYLESHEET_AND_INLINE_STYLE_TOO_LONG: return __( 'The author stylesheet specified in tag \'style amp-custom\' and the combined inline styles is too large - document contains %1$s bytes whereas the limit is %2$s bytes.', 'amp' ); - case 'UNESCAPED_TEMPLATE_IN_ATTR_VALUE': + case self::UNESCAPED_TEMPLATE_IN_ATTR_VALUE: return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to \'%3$s\', which contains unescaped Mustache template syntax.', 'amp' ); - case 'GENERAL_DISALLOWED_TAG': + case self::GENERAL_DISALLOWED_TAG: return __( 'The tag \'%1$s\' is disallowed except in specific forms.', 'amp' ); - case 'SPECIFIED_LAYOUT_INVALID': + case self::SPECIFIED_LAYOUT_INVALID: return __( 'The specified layout \'%1$s\' is not supported by tag \'%2$s\'.', 'amp' ); - case 'CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR': + case self::CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR: return __( 'CSS syntax error in tag \'%1$s\' - unparsed input remains in selector.', 'amp' ); - case 'CDATA_VIOLATES_BLACKLIST': + case self::CDATA_VIOLATES_BLACKLIST: return __( 'The text inside tag \'%1$s\' contains \'%2$s\', which is disallowed.', 'amp' ); - case 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE': + case self::CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE: return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed within @%3$s. Allowed properties: %4$s.', 'amp' ); - case 'MANDATORY_TAG_ANCESTOR': + case self::MANDATORY_TAG_ANCESTOR: return __( 'The tag \'%1$s\' may only appear as a descendant of tag \'%2$s\'.', 'amp' ); - case 'WARNING_TAG_REQUIRED_BY_MISSING': + case self::WARNING_TAG_REQUIRED_BY_MISSING: return __( 'The tag \'%1$s\' is missing or incorrect, but required by \'%2$s\'. This will soon be an error.', 'amp' ); - case 'TAG_EXCLUDED_BY_TAG': + case self::TAG_EXCLUDED_BY_TAG: return __( 'The tag \'%1$s\' is present, but is excluded by the presence of \'%2$s\'.', 'amp' ); - case 'INVALID_PROPERTY_VALUE_IN_ATTR_VALUE': + case self::INVALID_PROPERTY_VALUE_IN_ATTR_VALUE: return __( 'The property \'%1$s\' in attribute \'%2$s\' in tag \'%3$s\' is set to \'%4$s\', which is invalid.', 'amp' ); - case 'MANDATORY_REFERENCE_POINT_MISSING': + case self::MANDATORY_REFERENCE_POINT_MISSING: return __( 'The mandatory reference point \'%1$s\' for \'%2$s\' is missing.', 'amp' ); - case 'MISSING_URL': + case self::MISSING_URL: return __( 'Missing URL for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case 'MANDATORY_ANYOF_ATTR_MISSING': + case self::MANDATORY_ANYOF_ATTR_MISSING: return __( 'The tag \'%1$s\' is missing a mandatory attribute - pick at least one of %2$s.', 'amp' ); - case 'DISALLOWED_TAG': + case self::DISALLOWED_TAG: return __( 'The tag \'%1$s\' is disallowed.', 'amp' ); - case 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT': + case self::CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT: return __( 'The tag \'%1$s\', a child tag of \'%2$s\', does not satisfy one of the acceptable reference points: %3$s.', 'amp' ); - case 'DOCUMENT_SIZE_LIMIT_EXCEEDED': + case self::DOCUMENT_SIZE_LIMIT_EXCEEDED: return __( 'Document exceeded %1$s bytes limit. Actual size %2$s bytes.', 'amp' ); - case 'INVALID_URL_PROTOCOL': + case self::INVALID_URL_PROTOCOL: return __( 'Invalid URL protocol \'%3$s:\' for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case 'MANDATORY_CDATA_MISSING_OR_INCORRECT': + case self::MANDATORY_CDATA_MISSING_OR_INCORRECT: return __( 'The mandatory text inside tag \'%1$s\' is missing or incorrect.', 'amp' ); - case 'INCORRECT_NUM_CHILD_TAGS': + case self::INCORRECT_NUM_CHILD_TAGS: return __( 'Tag \'%1$s\' must have %2$s child tags - saw %3$s child tags.', 'amp' ); - case 'INVALID_UTF8': + case self::INVALID_UTF8: return __( 'The document contains invalid UTF8.', 'amp' ); - case 'ATTR_VALUE_REQUIRED_BY_LAYOUT': + case self::ATTR_VALUE_REQUIRED_BY_LAYOUT: return __( 'Invalid value \'%1$s\' for attribute \'%2$s\' in tag \'%3$s\' - for layout \'%4$s\', set the attribute \'%2$s\' to value \'%5$s\'.', 'amp' ); - case 'DUPLICATE_UNIQUE_TAG': + case self::DUPLICATE_UNIQUE_TAG: return __( 'The tag \'%1$s\' appears more than once in the document.', 'amp' ); - case 'MISSING_REQUIRED_EXTENSION': + case self::MISSING_REQUIRED_EXTENSION: return __( 'The tag \'%1$s\' requires including the \'%2$s\' extension JavaScript.', 'amp' ); - case 'MUTUALLY_EXCLUSIVE_ATTRS': + case self::MUTUALLY_EXCLUSIVE_ATTRS: return __( 'Mutually exclusive attributes encountered in tag \'%1$s\' - pick one of %2$s.', 'amp' ); - case 'DISALLOWED_FIRST_CHILD_TAG_NAME': + case self::DISALLOWED_FIRST_CHILD_TAG_NAME: return __( 'Tag \'%1$s\' is disallowed as first child of tag \'%2$s\'. First child tag must be one of %3$s.', 'amp' ); - case 'MANDATORY_LAST_CHILD_TAG': + case self::MANDATORY_LAST_CHILD_TAG: return __( 'Tag \'%1$s\', if present, must be the last child of tag \'%2$s\'.', 'amp' ); - case 'ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT': + case self::ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT: return __( 'The attribute \'%1$s\' in tag \'%2$s\' is disallowed by specified layout \'%3$s\'.', 'amp' ); - case 'DISALLOWED_RELATIVE_URL': + case self::DISALLOWED_RELATIVE_URL: return __( 'The relative URL \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\' is disallowed.', 'amp' ); - case 'MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE': + case self::MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE: return __( 'The property \'%1$s\' is missing from attribute \'%2$s\' in tag \'%3$s\'.', 'amp' ); - case 'MANDATORY_TAG_MISSING': + case self::MANDATORY_TAG_MISSING: return __( 'The mandatory tag \'%1$s\' is missing or incorrect.', 'amp' ); - case 'CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS': + case self::CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS: return __( 'CSS syntax error in tag \'%1$s\' - qualified rule \'%2$s\' has no declarations.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE': + case self::CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE: return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is set to the disallowed value \'%3$s\'.', 'amp' ); - case 'CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE': + case self::CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE: return __( 'CSS syntax error in tag \'%1$s\' - end of stylesheet encountered in prelude of a qualified rule.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_DOMAIN': + case self::CSS_SYNTAX_DISALLOWED_DOMAIN: return __( 'CSS syntax error in tag \'%1$s\' - invalid domain \'%2$s\'.', 'amp' ); - case 'TEMPLATE_PARTIAL_IN_ATTR_VALUE': + case self::TEMPLATE_PARTIAL_IN_ATTR_VALUE: return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to \'%3$s\', which contains a Mustache template partial.', 'amp' ); - case 'MANDATORY_ONEOF_ATTR_MISSING': + case self::MANDATORY_ONEOF_ATTR_MISSING: return __( 'The tag \'%1$s\' is missing a mandatory attribute - pick one of %2$s.', 'amp' ); - case 'WRONG_PARENT_TAG': + case self::WRONG_PARENT_TAG: return __( 'The parent tag of tag \'%1$s\' is \'%2$s\', but it can only be \'%3$s\'.', 'amp' ); - case 'CSS_SYNTAX_UNTERMINATED_COMMENT': + case self::CSS_SYNTAX_UNTERMINATED_COMMENT: return __( 'CSS syntax error in tag \'%1$s\' - unterminated comment.', 'amp' ); - case 'DUPLICATE_ATTRIBUTE': + case self::DUPLICATE_ATTRIBUTE: return __( 'The tag \'%1$s\' contains the attribute \'%2$s\' repeated multiple times.', 'amp' ); - case 'DUPLICATE_DIMENSION': + case self::DUPLICATE_DIMENSION: return __( 'Multiple image candidates with the same width or pixel density found in attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME': + case self::CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME: return __( 'CSS syntax error in tag \'%1$s\' - qualified rule \'%2$s\' must be located inside of a keyframe.', 'amp' ); - case 'CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH': + case self::CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH: return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed together with \'%3$s\'. Allowed properties: %4$s.', 'amp' ); - case 'STYLESHEET_TOO_LONG': + case self::STYLESHEET_TOO_LONG: return __( 'The author stylesheet specified in tag \'%1$s\' is too long - document contains %2$s bytes whereas the limit is %3$s bytes.', 'amp' ); - case 'TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS': + case self::TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS: return __( 'Tag \'%1$s\' is not allowed to have any sibling tags (\'%2$s\' should only have 1 child).', 'amp' ); - case 'INVALID_URL': + case self::INVALID_URL: return __( 'Malformed URL \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR': + case self::CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR: return __( 'The tag \'%1$s\', a child tag of \'%2$s\', does not satisfy the reference point \'%3$s\'.', 'amp' ); - case 'VALUE_SET_MISMATCH': + case self::VALUE_SET_MISMATCH: return __( 'Attribute \'%1$s\' in tag \'%2$s\' contains a value that does not match any other tags on the page.', 'amp' ); - case 'CSS_SYNTAX_INVALID_URL': + case self::CSS_SYNTAX_INVALID_URL: return __( 'CSS syntax error in tag \'%1$s\' - invalid url \'%2$s\'.', 'amp' ); - case 'MISSING_LAYOUT_ATTRIBUTES': + case self::MISSING_LAYOUT_ATTRIBUTES: return __( 'Incomplete layout attributes specified for tag \'%1$s\'. For example, provide attributes \'width\' and \'height\'.', 'amp' ); - case 'CSS_SYNTAX_MISSING_SELECTOR': + case self::CSS_SYNTAX_MISSING_SELECTOR: return __( 'CSS syntax error in tag \'%1$s\' - missing selector.', 'amp' ); - case 'CSS_SYNTAX_INVALID_PROPERTY': + case self::CSS_SYNTAX_INVALID_PROPERTY: return __( 'CSS syntax error in tag \'%1$s\' - invalid property \'%2$s\'. The only allowed properties are \'%3$s\'.', 'amp' ); - case 'CSS_SYNTAX_INVALID_ATTR_SELECTOR': + case self::CSS_SYNTAX_INVALID_ATTR_SELECTOR: return __( 'CSS syntax error in tag \'%1$s\' - invalid attribute selector.', 'amp' ); - case 'CSS_SYNTAX_INVALID_URL_PROTOCOL': + case self::CSS_SYNTAX_INVALID_URL_PROTOCOL: return __( 'CSS syntax error in tag \'%1$s\' - invalid url protocol \'%2$s:\'.', 'amp' ); - case 'DISALLOWED_MANUFACTURED_BODY': + case self::DISALLOWED_MANUFACTURED_BODY: return __( 'Tag or text which is only allowed inside the body section found outside of the body section.', 'amp' ); - case 'CSS_SYNTAX_INCOMPLETE_DECLARATION': + case self::CSS_SYNTAX_INCOMPLETE_DECLARATION: return __( 'CSS syntax error in tag \'%1$s\' - incomplete declaration.', 'amp' ); - case 'IMPLIED_LAYOUT_INVALID': + case self::IMPLIED_LAYOUT_INVALID: return __( 'The implied layout \'%1$s\' is not supported by tag \'%2$s\'.', 'amp' ); - case 'CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION': + case self::CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION: return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed unless the enclosing rule is prefixed with the \'%3$s\' qualification.', 'amp' ); - case 'DISALLOWED_CHILD_TAG_NAME': + case self::DISALLOWED_CHILD_TAG_NAME: return __( 'Tag \'%1$s\' is disallowed as child of tag \'%2$s\'. Child tag must be one of %3$s.', 'amp' ); - case 'CSS_SYNTAX_MISSING_URL': + case self::CSS_SYNTAX_MISSING_URL: return __( 'CSS syntax error in tag \'%1$s\' - missing url.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE': + case self::CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE: return __( 'CSS syntax error in tag \'%1$s\' - disallowed media feature \'%2$s\'.', 'amp' ); - case 'TAG_REQUIRED_BY_MISSING': + case self::TAG_REQUIRED_BY_MISSING: return __( 'The tag \'%1$s\' is missing or incorrect, but required by \'%2$s\'.', 'amp' ); - case 'INCORRECT_MIN_NUM_CHILD_TAGS': + case self::INCORRECT_MIN_NUM_CHILD_TAGS: return __( 'Tag \'%1$s\' must have a minimum of %2$s child tags - saw %3$s child tags.', 'amp' ); - case 'DISALLOWED_SCRIPT_TAG': + case self::DISALLOWED_SCRIPT_TAG: return __( 'Custom JavaScript is not allowed.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME': + case self::CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME: return __( 'CSS syntax error in tag \'%1$s\' - keyframe inside keyframe is not allowed.', 'amp' ); - case 'DISALLOWED_TAG_ANCESTOR': + case self::DISALLOWED_TAG_ANCESTOR: return __( 'The tag \'%1$s\' may not appear as a descendant of tag \'%2$s\'.', 'amp' ); - case 'DUPLICATE_UNIQUE_TAG_WARNING': + case self::DUPLICATE_UNIQUE_TAG_WARNING: return __( 'The tag \'%1$s\' appears more than once in the document. This will soon be an error.', 'amp' ); - case 'NON_WHITESPACE_CDATA_ENCOUNTERED': + case self::NON_WHITESPACE_CDATA_ENCOUNTERED: return __( 'The tag \'%1$s\' contains text, which is disallowed.', 'amp' ); - case 'WARNING_EXTENSION_DEPRECATED_VERSION': + case self::WARNING_EXTENSION_DEPRECATED_VERSION: return __( 'The extension \'%1$s\' is referenced at version \'%2$s\' which is a deprecated version. Please use a more recent version of this extension. This may become an error in the future.', 'amp' ); - case 'CSS_SYNTAX_INVALID_AT_RULE': + case self::CSS_SYNTAX_INVALID_AT_RULE: return __( 'CSS syntax error in tag \'%1$s\' - saw invalid at rule \'@%2$s\'.', 'amp' ); - case 'DISALLOWED_ATTR': + case self::DISALLOWED_ATTR: return __( 'The attribute \'%1$s\' may not appear in tag \'%2$s\'.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT': + case self::CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT: return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is set to the disallowed value \'%3$s\'. Allowed values: %4$s.', 'amp' ); - case 'CSS_SYNTAX_INVALID_DECLARATION': + case self::CSS_SYNTAX_INVALID_DECLARATION: return __( 'CSS syntax error in tag \'%1$s\' - invalid declaration.', 'amp' ); - case 'ATTR_MISSING_REQUIRED_EXTENSION': + case self::ATTR_MISSING_REQUIRED_EXTENSION: return __( 'The attribute \'%1$s\' requires including the \'%2$s\' extension JavaScript.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_RELATIVE_URL': + case self::CSS_SYNTAX_DISALLOWED_RELATIVE_URL: return __( 'CSS syntax error in tag \'%1$s\' - disallowed relative url \'%2$s\'.', 'amp' ); - case 'DEPRECATED_TAG': + case self::DEPRECATED_TAG: return __( 'The tag \'%1$s\' is deprecated - use \'%2$s\' instead.', 'amp' ); - case 'BASE_TAG_MUST_PRECEED_ALL_URLS': + case self::BASE_TAG_MUST_PRECEED_ALL_URLS: return __( 'The tag \'%1$s\', which contains URLs, was found earlier in the document than the BASE element.', 'amp' ); - case 'CSS_SYNTAX_INVALID_PROPERTY_NOLIST': + case self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST: return __( 'CSS syntax error in tag \'%1$s\' - invalid property \'%2$s\'.', 'amp' ); - case 'ATTR_REQUIRED_BUT_MISSING': + case self::ATTR_REQUIRED_BUT_MISSING: return __( 'The attribute \'%1$s\' in tag \'%2$s\' is missing or incorrect, but required by attribute \'%3$s\'.', 'amp' ); - case 'INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT': + case self::INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT: return __( 'Inconsistent units for width and height in tag \'%1$s\' - width is specified in \'%2$s\' whereas height is specified in \'%3$s\'.', 'amp' ); - case 'UNKNOWN_CODE': + case self::UNKNOWN_CODE: return __( 'Unknown error.', 'amp' ); - case 'ATTR_DISALLOWED_BY_IMPLIED_LAYOUT': + case self::ATTR_DISALLOWED_BY_IMPLIED_LAYOUT: return __( 'The attribute \'%1$s\' in tag \'%2$s\' is disallowed by implied layout \'%3$s\'.', 'amp' ); - case 'DISALLOWED_PROPERTY_IN_ATTR_VALUE': + case self::DISALLOWED_PROPERTY_IN_ATTR_VALUE: return __( 'The property \'%1$s\' in attribute \'%2$s\' in tag \'%3$s\' is disallowed.', 'amp' ); - case 'DUPLICATE_REFERENCE_POINT': + case self::DUPLICATE_REFERENCE_POINT: return __( 'The reference point \'%1$s\' for \'%2$s\' must be unique but a duplicate was encountered.', 'amp' ); - case 'TEMPLATE_IN_ATTR_NAME': + case self::TEMPLATE_IN_ATTR_NAME: return __( 'Mustache template syntax in attribute name \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case 'DISALLOWED_STYLE_ATTR': + case self::DISALLOWED_STYLE_ATTR: return __( 'The inline \'style\' attribute is not allowed in AMP documents. Use \'style amp-custom\' tag instead.', 'amp' ); - case 'CSS_EXCESSIVELY_NESTED': + case self::CSS_EXCESSIVELY_NESTED: return __( 'CSS excessively nested in tag \'%1$s\'.', 'amp' ); - case 'CSS_SYNTAX_MALFORMED_MEDIA_QUERY': + case self::CSS_SYNTAX_MALFORMED_MEDIA_QUERY: return __( 'CSS syntax error in tag \'%1$s\' - malformed media query.', 'amp' ); - case 'CSS_SYNTAX_UNTERMINATED_STRING': + case self::CSS_SYNTAX_UNTERMINATED_STRING: return __( 'CSS syntax error in tag \'%1$s\' - unterminated string.', 'amp' ); - case 'INVALID_ATTR_VALUE': + case self::INVALID_ATTR_VALUE: return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to the invalid value \'%3$s\'.', 'amp' ); - case 'EXTENSION_UNUSED': + case self::EXTENSION_UNUSED: return __( 'The extension \'%1$s\' was found on this page, but is unused. Please remove this extension.', 'amp' ); - case 'DISALLOWED_DOMAIN': + case self::DISALLOWED_DOMAIN: return __( 'The domain \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\' is disallowed.', 'amp' ); - case 'INVALID_JSON_CDATA': + case self::INVALID_JSON_CDATA: return __( 'The script tag contains invalid JSON that cannot be parsed.', 'amp' ); - case 'CSS_SYNTAX_DISALLOWED_MEDIA_TYPE': + case self::CSS_SYNTAX_DISALLOWED_MEDIA_TYPE: return __( 'CSS syntax error in tag \'%1$s\' - disallowed media type \'%2$s\'.', 'amp' ); - case 'INLINE_STYLE_TOO_LONG': + case self::INLINE_STYLE_TOO_LONG: return __( 'The inline style specified in tag \'%1$s\' is too long - it contains %2$s bytes whereas the limit is %3$s bytes.', 'amp' ); - case 'DEV_MODE_ONLY': + case self::DEV_MODE_ONLY: return __( 'Tag \'html\' marked with attribute \'data-ampdevmode\'. Validator will suppress errors regarding any other tag with this attribute.', 'amp' ); default: From ef80051d41bbd9f79786697cd9df8959a68c1e34 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 23 Nov 2019 09:59:45 -0800 Subject: [PATCH 11/39] Fix removing emptyable-attributes and fix is_missing_mandatory_attribute --- .../sanitizers/class-amp-tag-and-attribute-sanitizer.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 17328bc6483..d28a365865e 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -584,7 +584,8 @@ private function process_node( DOMElement $node ) { list( $attr_node, $error_code ) = $disallowed_attribute; $validation_error['code'] = $error_code; - if ( $this->remove_invalid_attribute( $node, $attr_node, $validation_error, $merged_attr_spec_list ) ) { + $attr_spec = isset( $merged_attr_spec_list[ $attr_node->nodeName ] ) ? $merged_attr_spec_list[ $attr_node->nodeName ] : []; + if ( $this->remove_invalid_attribute( $node, $attr_node, $validation_error, $attr_spec ) ) { $removed_attributes[] = $attr_node; } } @@ -659,7 +660,7 @@ private function process_node( DOMElement $node ) { * @return bool $is_missing boolean Whether a required attribute is missing. */ public function is_missing_mandatory_attribute( $attr_spec, DOMElement $node ) { - return 0 === count( $this->get_missing_mandatory_attributes( $attr_spec, $node ) ); + return 0 !== count( $this->get_missing_mandatory_attributes( $attr_spec, $node ) ); } /** From 2d6ec3d75e831bcb6ac8e348a6411f131298768a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 23 Nov 2019 22:48:14 -0800 Subject: [PATCH 12/39] Add constants for error codes; improve CDATA error reporting --- .../validation-error-message/index.js | 4 +- .../validation-error-message/test/index.js | 6 +- .../sanitizers/class-amp-base-sanitizer.php | 6 +- .../sanitizers/class-amp-style-sanitizer.php | 6 +- .../class-amp-tag-and-attribute-sanitizer.php | 70 +++++++++++++------ .../class-amp-validation-error-taxonomy.php | 49 +++++-------- tests/php/test-amp-img-sanitizer.php | 10 +-- tests/php/test-class-amp-base-sanitizer.php | 6 +- .../php/test-tag-and-attribute-sanitizer.php | 50 ++++++------- ...test-class-amp-validated-url-post-type.php | 10 +-- ...st-class-amp-validation-error-taxonomy.php | 10 +-- .../test-class-amp-validation-manager.php | 12 ++-- 12 files changed, 127 insertions(+), 112 deletions(-) diff --git a/assets/src/block-validation/components/validation-error-message/index.js b/assets/src/block-validation/components/validation-error-message/index.js index f50f9efdddd..86800c6989d 100644 --- a/assets/src/block-validation/components/validation-error-message/index.js +++ b/assets/src/block-validation/components/validation-error-message/index.js @@ -25,7 +25,7 @@ const ValidationErrorMessage = ( { message, code, node_name: nodeName, parent_na return message; } - if ( 'invalid_element' === code && nodeName ) { + if ( 'DISALLOWED_TAG' === code && nodeName ) { // @todo Needs to be fleshed out. return ( <> { __( 'Invalid element: ', 'amp' ) } @@ -34,7 +34,7 @@ const ValidationErrorMessage = ( { message, code, node_name: nodeName, parent_na ); - } else if ( 'invalid_attribute' === code && nodeName ) { + } else if ( 'DISALLOWED_ATTR' === code && nodeName ) { // @todo Needs to be fleshed out. return ( <> { __( 'Invalid attribute: ', 'amp' ) } diff --git a/assets/src/block-validation/components/validation-error-message/test/index.js b/assets/src/block-validation/components/validation-error-message/test/index.js index 9c09c35627d..0b94167c40c 100644 --- a/assets/src/block-validation/components/validation-error-message/test/index.js +++ b/assets/src/block-validation/components/validation-error-message/test/index.js @@ -15,17 +15,17 @@ describe( 'ValidationErrorMessage', () => { } ); it( 'renders an error for an invalid element', () => { - const errorMessage = render( ); + const errorMessage = render( ); expect( errorMessage ).toMatchSnapshot(); } ); it( 'renders an error for an invalid attribute', () => { - const errorMessage = render( ); + const errorMessage = render( ); expect( errorMessage ).toMatchSnapshot(); } ); it( 'renders an error for an invalid attribute with parent node', () => { - const errorMessage = render( ); + const errorMessage = render( ); expect( errorMessage ).toMatchSnapshot(); } ); diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index ac46f50b203..6dbb2442d5c 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -551,13 +551,15 @@ public function prepare_validation_error( array $error = [], array $data = [] ) if ( $node instanceof DOMElement ) { if ( ! isset( $error['code'] ) ) { - $error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE; + $error['code'] = AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG; } if ( ! isset( $error['type'] ) ) { + // @todo Also include javascript: protocol for URL errors. $error['type'] = 'script' === $node->nodeName ? AMP_Validation_Error_Taxonomy::JS_ERROR_TYPE : AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE; } + // @todo Change from node_attributes to element_attributes to harmonize the two. if ( ! isset( $error['node_attributes'] ) ) { $error['node_attributes'] = []; foreach ( $node->attributes as $attribute ) { @@ -578,7 +580,7 @@ public function prepare_validation_error( array $error = [], array $data = [] ) } } elseif ( $node instanceof DOMAttr ) { if ( ! isset( $error['code'] ) ) { - $error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE; + $error['code'] = AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR; } if ( ! isset( $error['type'] ) ) { // If this is an attribute that begins with on, like onclick, it should be a js_error. diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 54fede23b6c..66c1644fa13 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -31,7 +31,7 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { * * @var string */ - const ILLEGAL_AT_RULE_ERROR_CODE = 'illegal_css_at_rule'; + const ILLEGAL_AT_RULE_ERROR_CODE = 'illegal_css_at_rule'; // @todo CSS_SYNTAX_INVALID_AT_RULE /** * Inline style selector's specificity multiplier, i.e. used to generate the number of ':not(#_)' placeholders. @@ -326,10 +326,10 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { public static function get_css_parser_validation_error_codes() { return [ 'css_parse_error', - 'excessive_css', + 'excessive_css', // @todo STYLESHEET_TOO_LONG? But it's not technically the right error message? self::ILLEGAL_AT_RULE_ERROR_CODE, 'illegal_css_important', - 'illegal_css_property', + 'illegal_css_property', // @todo CSS_SYNTAX_INVALID_PROPERTY_NOLIST 'unrecognized_css', 'disallowed_file_extension', 'file_path_not_found', diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index d28a365865e..885054be914 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -25,6 +25,26 @@ */ class AMP_Tag_And_Attribute_Sanitizer extends AMP_Base_Sanitizer { + const DISALLOWED_TAG = 'DISALLOWED_TAG'; + const DISALLOWED_ATTR = 'DISALLOWED_ATTR'; + const DISALLOWED_PROCESSING_INSTRUCTION = 'DISALLOWED_PROCESSING_INSTRUCTION'; + const CDATA_VIOLATES_BLACKLIST = 'CDATA_VIOLATES_BLACKLIST'; + const DUPLICATE_UNIQUE_TAG = 'DUPLICATE_UNIQUE_TAG'; + const MANDATORY_CDATA_MISSING_OR_INCORRECT = 'MANDATORY_CDATA_MISSING_OR_INCORRECT'; + const CDATA_TOO_LONG = 'CDATA_TOO_LONG'; + const INVALID_ATTR_VALUE = 'INVALID_ATTR_VALUE'; + const INVALID_ATTR_VALUE_CASEI = 'INVALID_ATTR_VALUE_CASEI'; + const INVALID_ATTR_VALUE_REGEX = 'INVALID_ATTR_VALUE_REGEX'; + const INVALID_ATTR_VALUE_REGEX_CASEI = 'INVALID_ATTR_VALUE_REGEX_CASEI'; + const INVALID_URL_PROTOCOL = 'INVALID_URL_PROTOCOL'; + const INVALID_URL = 'INVALID_URL'; + const DISALLOWED_RELATIVE_URL = 'DISALLOWED_RELATIVE_URL'; + const DISALLOWED_EMPTY = 'DISALLOWED_EMPTY'; + const DISALLOWED_DOMAIN = 'DISALLOWED_DOMAIN'; + const INVALID_BLACKLISTED_VALUE_REGEX = 'INVALID_BLACKLISTED_VALUE_REGEX'; + const DISALLOWED_PROPERTY_IN_ATTR_VALUE = 'DISALLOWED_PROPERTY_IN_ATTR_VALUE'; + const ATTR_REQUIRED_BUT_MISSING = 'ATTR_REQUIRED_BUT_MISSING'; + /** * Allowed tags. * @@ -298,7 +318,7 @@ private function sanitize_element( DOMElement $element ) { ); } } elseif ( $this_child instanceof DOMProcessingInstruction ) { - $this->remove_invalid_child( $this_child, [ 'code' => 'invalid_processing_instruction' ] ); + $this->remove_invalid_child( $this_child, [ 'code' => self::DISALLOWED_PROCESSING_INSTRUCTION ] ); } $this_child = $next_child; } @@ -476,7 +496,6 @@ private function process_node( DOMElement $node ) { // If no attribute spec lists match, then the element must be removed. if ( empty( $attr_spec_scores ) ) { - // @todo How can we tell the reason for removal if none of the tag specs matched? Ignore attribute values? $this->remove_node( $node ); return null; } @@ -519,7 +538,13 @@ private function process_node( DOMElement $node ) { if ( ! empty( $cdata ) && $node instanceof DOMElement ) { $validity = $this->validate_cdata_for_node( $node, $cdata ); if ( is_wp_error( $validity ) ) { - $this->remove_invalid_child( $node, [ 'code' => 'illegal_cdata' ] ); + $this->remove_invalid_child( + $node, + [ + 'code' => $validity->get_error_code(), + 'message' => $validity->get_error_message(), // @todo We need to ensure this gets translated. + ] + ); return null; } } @@ -547,7 +572,7 @@ private function process_node( DOMElement $node ) { if ( ! empty( $this->visited_unique_tag_specs[ $node->nodeName ][ $tag_spec_key ] ) ) { $removed = $this->remove_invalid_child( $node, - [ 'code' => 'duplicate_element' ] + [ 'code' => self::DUPLICATE_UNIQUE_TAG ] ); } $this->visited_unique_tag_specs[ $node->nodeName ][ $tag_spec_key ] = true; @@ -573,7 +598,6 @@ private function process_node( DOMElement $node ) { } $removed_attributes = []; - // @todo First needing to iterate over the $disallowed_attributes and if any are mandatory, skip removing attributes and instead remove the element. foreach ( $disallowed_attributes as $disallowed_attribute ) { /** * Returned vars. @@ -611,7 +635,7 @@ private function process_node( DOMElement $node ) { $this->remove_invalid_child( $node, [ - 'code' => 'missing_mandatory_attribute', + 'code' => self::ATTR_REQUIRED_BUT_MISSING, 'attributes' => $missing_mandatory_attributes, ] ); @@ -700,16 +724,16 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { // This would mean that AMP was disabled to not break the styling. ! ( 'style' === $element->nodeName && $element->hasAttribute( 'amp-custom' ) ) ) { - return new WP_Error( 'excessive_bytes' ); + return new WP_Error( self::CDATA_TOO_LONG ); } if ( isset( $cdata_spec['blacklisted_cdata_regex'] ) ) { if ( preg_match( '@' . $cdata_spec['blacklisted_cdata_regex']['regex'] . '@u', $element->textContent ) ) { - return new WP_Error( $cdata_spec['blacklisted_cdata_regex']['error_message'] ); + return new WP_Error( self::CDATA_VIOLATES_BLACKLIST, $cdata_spec['blacklisted_cdata_regex']['error_message'] ); } } elseif ( isset( $cdata_spec['cdata_regex'] ) ) { $delimiter = false === strpos( $cdata_spec['cdata_regex'], '@' ) ? '@' : '#'; if ( ! preg_match( $delimiter . $cdata_spec['cdata_regex'] . $delimiter . 'u', $element->textContent ) ) { - return new WP_Error( 'cdata_regex' ); + return new WP_Error( self::MANDATORY_CDATA_MISSING_OR_INCORRECT ); } } return true; @@ -908,6 +932,7 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ * Allowed values are found $this->globally_allowed_attributes and in parameter $attr_spec_list * * @see \AMP_Tag_And_Attribute_Sanitizer::validate_attr_spec_list_for_node() + * @see https://github.com/ampproject/amphtml/blob/b692bf32880910cd52273cb41935098b86fb6725/validator/engine/validator.js#L3210-L3289 * * @param DOMElement $node Node. * @param array[] $attr_spec_list Attribute spec list. @@ -931,7 +956,7 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, * remove in the first loop, then remove them in the second loop. */ if ( ! $this->is_amp_allowed_attribute( $attr_node, $attr_spec_list ) ) { - $attrs_to_remove[] = [ $attr_node, AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE ]; + $attrs_to_remove[] = [ $attr_node, self::DISALLOWED_ATTR ]; continue; } @@ -966,37 +991,38 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, // @todo The check methods should return an array of validation error data when failure. if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_value'; + $error_code = self::INVALID_ATTR_VALUE; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_CASEI ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_casei( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_case_insensitive_value'; + $error_code = self::INVALID_ATTR_VALUE_CASEI; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_REGEX ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_regex( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_value_for_pattern'; + $error_code = self::INVALID_ATTR_VALUE_REGEX; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_REGEX_CASEI ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_regex_casei( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_value_for_case_insensitive_pattern'; + $error_code = self::INVALID_ATTR_VALUE_REGEX_CASEI; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOWED_PROTOCOL ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_allowed_protocol( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_url_protocol'; // @todo A javascript: protocol could be treated differently. It should have a JS error type. + $error_code = self::INVALID_URL_PROTOCOL; // @todo A javascript: protocol could be treated differently. It should have a JS error type. } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_valid_url( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'invalid_url'; + $error_code = self::INVALID_URL; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_RELATIVE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_relative( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_relative_url'; + $error_code = self::DISALLOWED_RELATIVE_URL; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_empty( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_empty_value'; + $error_code = self::DISALLOWED_EMPTY; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::DISALLOWED_DOMAIN ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_domain( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_url_host'; + $error_code = self::DISALLOWED_DOMAIN; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::BLACKLISTED_VALUE_REGEX ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_blacklisted_value_regex( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_value_by_pattern'; + $error_code = self::INVALID_BLACKLISTED_VALUE_REGEX; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_PROPERTIES ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value_properties( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = 'illegal_value_properties'; // @todo Which property(s) in particular? + // @todo Should there be a separate validation error for each invalid property? + $error_code = self::DISALLOWED_PROPERTY_IN_ATTR_VALUE; // @todo Which property(s) in particular? } if ( isset( $error_code ) ) { @@ -1592,7 +1618,7 @@ private function is_amp_allowed_attribute( DOMAttr $attr_node, $attr_spec_list ) $is_allowed_alt_name_attr = isset( $this->rev_alternate_attr_name_lookup[ $attr_name ], $attr_spec_list[ $this->rev_alternate_attr_name_lookup[ $attr_name ] ] ); if ( $is_allowed_alt_name_attr ) { - return true; + return true; // @todo Return error. } /* diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index ac43a215f8b..bcb5d4c860c 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -120,25 +120,11 @@ class AMP_Validation_Error_Taxonomy { */ const NO_FILTER_VALUE = ''; - /** - * Validation code for an invalid element. - * - * @var string - */ - const INVALID_ELEMENT_CODE = 'invalid_element'; - - /** - * Validation code for an invalid attribute. - * - * @var string - */ - const INVALID_ATTRIBUTE_CODE = 'invalid_attribute'; - /** * The 'type' of error for invalid HTML elements, like . * - * These usually have the 'code' of 'invalid_element'. - * Except for 'invalid_element' errors for a ', 2 ), '', [ 'amp-geo' ], - [ 'duplicate_element' ], + [ AMP_Tag_And_Attribute_Sanitizer::DUPLICATE_UNIQUE_TAG ], ], 'amp-autocomplete' => [ @@ -1697,28 +1697,28 @@ static function() { str_repeat( '
', 200 ) . 'hello world!' . str_repeat( '
', 200 ), str_repeat( '
', 200 ) . 'hello world!' . str_repeat( '
', 200 ), [], - [ 'invalid_element' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'invalid_php_pi' => [ '', '', [], - [ 'invalid_processing_instruction' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROCESSING_INSTRUCTION ], ], 'invalid_xml_pi' => [ '', '', [], - [ 'invalid_processing_instruction' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROCESSING_INSTRUCTION ], ], 'malformed_attribute_syntax_curly_quotes' => [ 'Whatever', 'Whatever', [], - [ 'invalid_attribute', 'invalid_attribute' ], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE, AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL ], ], ]; } @@ -1810,25 +1810,25 @@ public function get_html_data() { '', // phpcs:ignore '', [], - [ 'invalid_attribute', 'missing_mandatory_attribute' ], + [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX ], ], 'bad_meta_ua_compatible' => [ '', '', [], - [ 'invalid_attribute', 'missing_mandatory_attribute' ], + [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE ], ], 'bad_meta_charset' => [ 'Mojibake?', 'Mojibake?', // Note the charset attribute is removed because it violates the attribute spec, but the entire element is not removed because charset is not mandatory. [], - [ 'invalid_attribute' ], // @todo Should actually be invalid_mandatory_attribute? + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], // @todo Should actually be invalid_mandatory_attribute? ], 'bad_meta_viewport' => [ '', '', [], - [ 'invalid_attribute', 'missing_mandatory_attribute' ], + [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE ], ], 'edge_meta_ua_compatible' => [ '', @@ -1855,13 +1855,13 @@ public function get_html_data() { '

Content

', '

Content

', [], - [ 'duplicate_element' ], + [ AMP_Tag_And_Attribute_Sanitizer::DUPLICATE_UNIQUE_TAG ], ], 'head_with_duplicate_viewport' => [ '

Content

', '

Content

', [], - [ 'duplicate_element' ], + [ AMP_Tag_And_Attribute_Sanitizer::DUPLICATE_UNIQUE_TAG ], ], 'meta_amp_script_src' => [ '', @@ -1915,10 +1915,10 @@ public function get_html_data() { ', [ 'amp-bind' ], [ - 'illegal_cdata', - 'invalid_attribute', - 'invalid_element', - 'invalid_element', + AMP_Tag_And_Attribute_Sanitizer::CDATA_TOO_LONG, + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, ], ]; @@ -2022,7 +2022,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'amp-image', 'parent_name' => 'body', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [ 'src' => '/none.jpg', 'width' => '100', @@ -2041,7 +2041,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'baz', 'parent_name' => 'body', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [ 'class' => 'baz-invalid' ], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], @@ -2055,7 +2055,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'amp-story-grid-layer', 'parent_name' => 'body', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [ 'class' => 'a-invalid' ], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], @@ -2069,7 +2069,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'foo', 'parent_name' => 'body', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [ 'class' => 'foo-invalid' ], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], @@ -2083,7 +2083,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'bazbar', 'parent_name' => 'body', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], @@ -2108,14 +2108,14 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'invalid_p', 'parent_name' => 'div', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [ 'id' => 'invalid' ], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], [ 'node_name' => 'bazfoo', 'parent_name' => 'div', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], @@ -2134,7 +2134,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'lili', 'parent_name' => 'ul', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], @@ -2148,14 +2148,14 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'foo', 'parent_name' => 'divs', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], [ 'node_name' => 'divs', 'parent_name' => 'body', - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], diff --git a/tests/php/validation/test-class-amp-validated-url-post-type.php b/tests/php/validation/test-class-amp-validated-url-post-type.php index 8a7478918d0..65adf32ad7f 100644 --- a/tests/php/validation/test-class-amp-validated-url-post-type.php +++ b/tests/php/validation/test-class-amp-validated-url-post-type.php @@ -598,19 +598,19 @@ public function get_custom_columns() { ]; $errors = [ [ - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_name' => 'script', 'sources' => [ $source ], ], [ - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, 'node_name' => 'onclick', 'sources' => [ $source ], ], ]; return [ - 'invalid_element' => [ + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG => [ AMP_Validation_Error_Taxonomy::SOURCES_INVALID_OUTPUT, 'AMP', $errors, @@ -1661,7 +1661,7 @@ public function test_filter_bulk_post_updated_messages() { public function get_mock_errors() { return [ [ - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_name' => 'script', 'parent_name' => 'div', 'node_attributes' => [], @@ -1673,7 +1673,7 @@ public function get_mock_errors() { ], ], [ - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, 'node_name' => 'onclick', 'parent_name' => 'div', 'element_attributes' => [ diff --git a/tests/php/validation/test-class-amp-validation-error-taxonomy.php b/tests/php/validation/test-class-amp-validation-error-taxonomy.php index 05d9515b640..bd7ca3d012b 100644 --- a/tests/php/validation/test-class-amp-validation-error-taxonomy.php +++ b/tests/php/validation/test-class-amp-validation-error-taxonomy.php @@ -504,7 +504,7 @@ public function test_summarize_validation_errors() { $element_node_name = 'nonexistent-element'; $validation_errors = [ [ - 'code' => 'invalid_attribute', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, 'node_name' => $attribute_node_name, 'sources' => [ [ @@ -514,7 +514,7 @@ public function test_summarize_validation_errors() { ], ], [ - 'code' => 'invalid_element', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_name' => $element_node_name, 'sources' => [ [ @@ -1081,7 +1081,7 @@ public function test_get_reader_friendly_error_type_text() { public function test_get_details_summary_label() { $validation_error = $this->get_mock_error(); $this->assertEquals( '<link>', AMP_Validation_Error_Taxonomy::get_details_summary_label( $validation_error ) ); - $validation_error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ATTRIBUTE_CODE; + $validation_error['code'] = AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR; $this->assertEquals( '<head>', AMP_Validation_Error_Taxonomy::get_details_summary_label( $validation_error ) ); unset( $validation_error['node_name'] ); $this->assertEquals( '<head>', AMP_Validation_Error_Taxonomy::get_details_summary_label( $validation_error ) ); @@ -1173,7 +1173,7 @@ public function test_add_single_post_sortable_columns() { */ public function test_render_single_url_error_details() { $validation_error = self::get_mock_error(); - $validation_error['code'] = AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE; + $validation_error['code'] = AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG; $term = self::factory()->term->create_and_get( [ 'taxonomy' => AMP_Validation_Error_Taxonomy::TAXONOMY_SLUG ] ); $html = AMP_Validation_Error_Taxonomy::render_single_url_error_details( $validation_error, $term ); $this->assertContains( '
', $html ); @@ -1187,7 +1187,7 @@ public function test_render_single_url_error_details() { public function test_get_translated_type_name() { // When the error doesn't have a type, this should return null. $error_without_type = [ - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, ]; $this->assertEmpty( AMP_Validation_Error_Taxonomy::get_translated_type_name( $error_without_type ) ); diff --git a/tests/php/validation/test-class-amp-validation-manager.php b/tests/php/validation/test-class-amp-validation-manager.php index a9b4194b467..8ead94c4750 100644 --- a/tests/php/validation/test-class-amp-validation-manager.php +++ b/tests/php/validation/test-class-amp-validation-manager.php @@ -235,7 +235,7 @@ public function test_is_sanitization_auto_accepted() { 'node_name' => 'href', 'parent_name' => 'a', 'type' => 'html_attribute_error', - 'code' => 'invalid_attribute', + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, ]; $excessive_css_error = [ 'node_name' => 'style', @@ -599,7 +599,7 @@ public function test_add_validation_error_track_removed() { 'amp_validation_error', static function( $error, $context ) use ( $node, $that ) { $error['filtered'] = true; - $that->assertEquals( AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE, $error['code'] ); + $that->assertEquals( AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, $error['code'] ); $that->assertSame( $node, $context['node'] ); return $error; }, @@ -610,7 +610,7 @@ static function( $error, $context ) use ( $node, $that ) { AMP_Validation_Manager::add_validation_error( [ 'node_name' => $this->node->nodeName, - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [], ], [ @@ -623,7 +623,7 @@ static function( $error, $context ) use ( $node, $that ) { [ 'node_name' => 'img', 'sources' => [], - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_attributes' => [], 'filtered' => true, ], @@ -680,7 +680,7 @@ public function test_print_edit_form_validation_status() { $validation_errors = [ [ - 'code' => AMP_Validation_Error_Taxonomy::INVALID_ELEMENT_CODE, + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, 'node_name' => $this->disallowed_tag_name, 'parent_name' => 'div', 'node_attributes' => [], @@ -1540,7 +1540,7 @@ public function test_finalize_validation() { $validation_results = [ [ - 'error' => [ 'code' => 'invalid_attribute' ], + 'error' => [ 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], 'sanitized' => false, 'slug' => '98765b4', 'term_status' => 0, From 00eb55c902653d66d050384882dec7bb89f0ddf8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 24 Nov 2019 11:22:19 -0800 Subject: [PATCH 13/39] Add fine-grained error codes for CDATA --- bin/amphtml-update.py | 5 +++ .../class-amp-tag-and-attribute-sanitizer.php | 33 +++++++++++++------ .../php/test-tag-and-attribute-sanitizer.php | 19 +++++++++++ 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/bin/amphtml-update.py b/bin/amphtml-update.py index 7757f8f70ce..9f631bc32fb 100644 --- a/bin/amphtml-update.py +++ b/bin/amphtml-update.py @@ -514,6 +514,11 @@ def GetTagSpec(tag_spec, attr_lists): cdata_dict['css_spec'] = css_spec if len( cdata_dict ) > 0: + if 'blacklisted_cdata_regex' in cdata_dict: + if 'error_message' not in cdata_dict['blacklisted_cdata_regex']: + raise Exception( 'Missing error_message for blacklisted_cdata_regex.' ); + if cdata_dict['blacklisted_cdata_regex']['error_message'] not in ( 'CSS !important', 'contents', 'html comments' ): + raise Exception( 'Unexpected error_message "%s" for blacklisted_cdata_regex.' % cdata_dict['blacklisted_cdata_regex']['error_message'] ); tag_spec_dict['cdata'] = cdata_dict return tag_spec_dict diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 885054be914..6e5d9867a85 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -32,6 +32,9 @@ class AMP_Tag_And_Attribute_Sanitizer extends AMP_Base_Sanitizer { const DUPLICATE_UNIQUE_TAG = 'DUPLICATE_UNIQUE_TAG'; const MANDATORY_CDATA_MISSING_OR_INCORRECT = 'MANDATORY_CDATA_MISSING_OR_INCORRECT'; const CDATA_TOO_LONG = 'CDATA_TOO_LONG'; + const INVALID_CDATA_CSS_IMPORTANT = 'INVALID_CDATA_CSS_IMPORTANT'; + const INVALID_CDATA_CONTENTS = 'INVALID_CDATA_CONTENTS'; + const INVALID_CDATA_HTML_COMMENTS = 'INVALID_CDATA_HTML_COMMENTS'; const INVALID_ATTR_VALUE = 'INVALID_ATTR_VALUE'; const INVALID_ATTR_VALUE_CASEI = 'INVALID_ATTR_VALUE_CASEI'; const INVALID_ATTR_VALUE_REGEX = 'INVALID_ATTR_VALUE_REGEX'; @@ -537,13 +540,10 @@ private function process_node( DOMElement $node ) { // Remove element if it has illegal CDATA. if ( ! empty( $cdata ) && $node instanceof DOMElement ) { $validity = $this->validate_cdata_for_node( $node, $cdata ); - if ( is_wp_error( $validity ) ) { + if ( true !== $validity ) { $this->remove_invalid_child( $node, - [ - 'code' => $validity->get_error_code(), - 'message' => $validity->get_error_message(), // @todo We need to ensure this gets translated. - ] + [ 'code' => $validity ] ); return null; } @@ -714,7 +714,7 @@ private function get_missing_mandatory_attributes( $attr_spec, DOMElement $node * * @param DOMElement $element Element. * @param array $cdata_spec CDATA. - * @return true|WP_Error True when valid or error when invalid. + * @return true|string True when valid or error code when invalid. */ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { if ( @@ -724,16 +724,29 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { // This would mean that AMP was disabled to not break the styling. ! ( 'style' === $element->nodeName && $element->hasAttribute( 'amp-custom' ) ) ) { - return new WP_Error( self::CDATA_TOO_LONG ); + return self::CDATA_TOO_LONG; } if ( isset( $cdata_spec['blacklisted_cdata_regex'] ) ) { if ( preg_match( '@' . $cdata_spec['blacklisted_cdata_regex']['regex'] . '@u', $element->textContent ) ) { - return new WP_Error( self::CDATA_VIOLATES_BLACKLIST, $cdata_spec['blacklisted_cdata_regex']['error_message'] ); + if ( isset( $cdata_spec['blacklisted_cdata_regex']['error_message'] ) ) { + // There are only a few error messages, so map them to error codes. + switch ( $cdata_spec['blacklisted_cdata_regex']['error_message'] ) { + case 'CSS !important': + return self::INVALID_CDATA_CSS_IMPORTANT; + case 'contents': + return self::INVALID_CDATA_CONTENTS; + case 'html comments': + return self::INVALID_CDATA_HTML_COMMENTS; + } + } + + // Note: This fallback case is not currently reachable because all error messages are accounted for in the switch statement. + return self::CDATA_VIOLATES_BLACKLIST; } } elseif ( isset( $cdata_spec['cdata_regex'] ) ) { $delimiter = false === strpos( $cdata_spec['cdata_regex'], '@' ) ? '@' : '#'; if ( ! preg_match( $delimiter . $cdata_spec['cdata_regex'] . $delimiter . 'u', $element->textContent ) ) { - return new WP_Error( self::MANDATORY_CDATA_MISSING_OR_INCORRECT ); + return self::MANDATORY_CDATA_MISSING_OR_INCORRECT; } } return true; @@ -1618,7 +1631,7 @@ private function is_amp_allowed_attribute( DOMAttr $attr_node, $attr_spec_list ) $is_allowed_alt_name_attr = isset( $this->rev_alternate_attr_name_lookup[ $attr_name ], $attr_spec_list[ $this->rev_alternate_attr_name_lookup[ $attr_name ] ] ); if ( $is_allowed_alt_name_attr ) { - return true; // @todo Return error. + return true; } /* diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 4132a426f53..79f294c5c3a 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -1720,6 +1720,13 @@ static function() { [], [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE, AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL ], ], + + 'cdata_html_comments' => [ + '', + '', + [ 'amp-geo' ], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS ], + ], ]; } @@ -1871,6 +1878,18 @@ public function get_html_data() { '', '', ], + 'cdata_css_important' => [ + '', + '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CSS_IMPORTANT ], + ], + 'cdata_contents' => [ + '', + '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS ], + ], ]; $bad_dev_mode_document = sprintf( From a6d0fc9315f917ebbc756b2d98c84d20b7fe46e2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 24 Nov 2019 11:26:41 -0800 Subject: [PATCH 14/39] Revert "Extract AMP validator error codes and messages from spec" This reverts commits 8ae5c3b5959af631e8404111315b128f1ade610a and 0b30e4a593793843a64c10f261812f2e58331e74. --- bin/amphtml-update.py | 48 +-- .../class-amp-allowed-tags-generated.php | 335 ------------------ 2 files changed, 2 insertions(+), 381 deletions(-) diff --git a/bin/amphtml-update.py b/bin/amphtml-update.py index 9f631bc32fb..0e8f509e616 100644 --- a/bin/amphtml-update.py +++ b/bin/amphtml-update.py @@ -94,7 +94,7 @@ def GeneratePHP(out_dir): """ logging.info('entering ...') - allowed_tags, attr_lists, descendant_lists, reference_points, versions, error_formats = ParseRules(out_dir) + allowed_tags, attr_lists, descendant_lists, reference_points, versions = ParseRules(out_dir) #Generate the output out = [] @@ -105,7 +105,6 @@ def GeneratePHP(out_dir): GenerateLayoutAttributesPHP(out, attr_lists) GenerateGlobalAttributesPHP(out, attr_lists) GenerateReferencePointsPHP(out, reference_points) - GenerateErrorFormatsPHP(out, error_formats) GenerateFooterPHP(out) # join out array into a single string and remove unneeded whitespace @@ -197,40 +196,6 @@ def GenerateReferencePointsPHP(out, reference_points): out.append('') logging.info('... done') -def GenerateErrorFormatsPHP(out, error_formats): - logging.info('entering ...') - - # TODO: Delete the error formats which are not used! - for code in error_formats.keys(): - out.append('\tconst %s = \'%s\';' % ( code, code )) - - out.append(''' - /** - * Get error message format. - * - * @since 1.5.0 - * @internal - * - * @param string $code Error code. - * @return string Error message format. - */ - public static function get_error_message_format( $code ) { - switch ( $code ) {''') - - for ( code, message_format ) in error_formats.items(): - out.append('\t\t\tcase self::%s:' % code) - message_format = re.sub( r'%(\d+)', r'%\1$s', message_format ) - out.append('\t\t\t\treturn __( %s, \'amp\' );' % Phpize( message_format )) - - out.append(''' - default: - return __( 'Unknown error', 'amp' ); - } - }''') - - logging.info('... done') - - def GenerateFooterPHP(out): logging.info('entering ...') @@ -368,8 +333,6 @@ def ParseRules(out_dir): descendant_lists = {} reference_points = {} versions = {} - error_formats = {} # TODO: Include some additional error codes not accounted for in AMP Validator. - validation_error_code_lookup = {} specfile='%s/validator.protoascii' % out_dir @@ -377,10 +340,6 @@ def ParseRules(out_dir): rules = validator_pb2.ValidatorRules() text_format.Merge(open(specfile).read(), rules) - validationError = validator_pb2.ValidationError() - for ( name, code ) in validationError.Code.items(): - validation_error_code_lookup[ code ] = name - # Record the version of this specfile and the corresponding validator version. if rules.HasField('spec_file_revision'): versions['spec_file_revision'] = rules.spec_file_revision @@ -445,12 +404,9 @@ def ParseRules(out_dir): continue descendant_lists[list.name].append( val.lower() ) - elif 'error_formats' == field_desc.name: - for list in field_val: - error_formats[ validation_error_code_lookup[list.code] ] = list.format logging.info('... done') - return allowed_tags, attr_lists, descendant_lists, reference_points, versions, error_formats + return allowed_tags, attr_lists, descendant_lists, reference_points, versions def GetTagSpec(tag_spec, attr_lists): diff --git a/includes/sanitizers/class-amp-allowed-tags-generated.php b/includes/sanitizers/class-amp-allowed-tags-generated.php index b06a5d93158..65d3e7d65be 100644 --- a/includes/sanitizers/class-amp-allowed-tags-generated.php +++ b/includes/sanitizers/class-amp-allowed-tags-generated.php @@ -17584,341 +17584,6 @@ class AMP_Allowed_Tags_Generated { ), ); - const CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR = 'CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR'; - const WARNING_EXTENSION_UNUSED = 'WARNING_EXTENSION_UNUSED'; - const CSS_SYNTAX_NOT_A_SELECTOR_START = 'CSS_SYNTAX_NOT_A_SELECTOR_START'; - const DOCUMENT_TOO_COMPLEX = 'DOCUMENT_TOO_COMPLEX'; - const MANDATORY_TAG_ANCESTOR_WITH_HINT = 'MANDATORY_TAG_ANCESTOR_WITH_HINT'; - const CSS_SYNTAX_BAD_URL = 'CSS_SYNTAX_BAD_URL'; - const MANDATORY_ATTR_MISSING = 'MANDATORY_ATTR_MISSING'; - const DEPRECATED_ATTR = 'DEPRECATED_ATTR'; - const TAG_REFERENCE_POINT_CONFLICT = 'TAG_REFERENCE_POINT_CONFLICT'; - const CSS_SYNTAX_STRAY_TRAILING_BACKSLASH = 'CSS_SYNTAX_STRAY_TRAILING_BACKSLASH'; - const STYLESHEET_AND_INLINE_STYLE_TOO_LONG = 'STYLESHEET_AND_INLINE_STYLE_TOO_LONG'; - const UNESCAPED_TEMPLATE_IN_ATTR_VALUE = 'UNESCAPED_TEMPLATE_IN_ATTR_VALUE'; - const GENERAL_DISALLOWED_TAG = 'GENERAL_DISALLOWED_TAG'; - const SPECIFIED_LAYOUT_INVALID = 'SPECIFIED_LAYOUT_INVALID'; - const CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR = 'CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR'; - const CDATA_VIOLATES_BLACKLIST = 'CDATA_VIOLATES_BLACKLIST'; - const CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE = 'CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE'; - const MANDATORY_TAG_ANCESTOR = 'MANDATORY_TAG_ANCESTOR'; - const WARNING_TAG_REQUIRED_BY_MISSING = 'WARNING_TAG_REQUIRED_BY_MISSING'; - const TAG_EXCLUDED_BY_TAG = 'TAG_EXCLUDED_BY_TAG'; - const INVALID_PROPERTY_VALUE_IN_ATTR_VALUE = 'INVALID_PROPERTY_VALUE_IN_ATTR_VALUE'; - const MANDATORY_REFERENCE_POINT_MISSING = 'MANDATORY_REFERENCE_POINT_MISSING'; - const MISSING_URL = 'MISSING_URL'; - const MANDATORY_ANYOF_ATTR_MISSING = 'MANDATORY_ANYOF_ATTR_MISSING'; - const DISALLOWED_TAG = 'DISALLOWED_TAG'; - const CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT = 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT'; - const DOCUMENT_SIZE_LIMIT_EXCEEDED = 'DOCUMENT_SIZE_LIMIT_EXCEEDED'; - const INVALID_URL_PROTOCOL = 'INVALID_URL_PROTOCOL'; - const MANDATORY_CDATA_MISSING_OR_INCORRECT = 'MANDATORY_CDATA_MISSING_OR_INCORRECT'; - const INCORRECT_NUM_CHILD_TAGS = 'INCORRECT_NUM_CHILD_TAGS'; - const INVALID_UTF8 = 'INVALID_UTF8'; - const ATTR_VALUE_REQUIRED_BY_LAYOUT = 'ATTR_VALUE_REQUIRED_BY_LAYOUT'; - const DUPLICATE_UNIQUE_TAG = 'DUPLICATE_UNIQUE_TAG'; - const MISSING_REQUIRED_EXTENSION = 'MISSING_REQUIRED_EXTENSION'; - const MUTUALLY_EXCLUSIVE_ATTRS = 'MUTUALLY_EXCLUSIVE_ATTRS'; - const DISALLOWED_FIRST_CHILD_TAG_NAME = 'DISALLOWED_FIRST_CHILD_TAG_NAME'; - const MANDATORY_LAST_CHILD_TAG = 'MANDATORY_LAST_CHILD_TAG'; - const ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT = 'ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT'; - const DISALLOWED_RELATIVE_URL = 'DISALLOWED_RELATIVE_URL'; - const MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE = 'MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE'; - const MANDATORY_TAG_MISSING = 'MANDATORY_TAG_MISSING'; - const CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS = 'CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS'; - const CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE = 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE'; - const CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE = 'CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE'; - const CSS_SYNTAX_DISALLOWED_DOMAIN = 'CSS_SYNTAX_DISALLOWED_DOMAIN'; - const TEMPLATE_PARTIAL_IN_ATTR_VALUE = 'TEMPLATE_PARTIAL_IN_ATTR_VALUE'; - const MANDATORY_ONEOF_ATTR_MISSING = 'MANDATORY_ONEOF_ATTR_MISSING'; - const WRONG_PARENT_TAG = 'WRONG_PARENT_TAG'; - const CSS_SYNTAX_UNTERMINATED_COMMENT = 'CSS_SYNTAX_UNTERMINATED_COMMENT'; - const DUPLICATE_ATTRIBUTE = 'DUPLICATE_ATTRIBUTE'; - const DUPLICATE_DIMENSION = 'DUPLICATE_DIMENSION'; - const CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME = 'CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME'; - const CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH = 'CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH'; - const STYLESHEET_TOO_LONG = 'STYLESHEET_TOO_LONG'; - const TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS = 'TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS'; - const INVALID_URL = 'INVALID_URL'; - const CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR = 'CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR'; - const VALUE_SET_MISMATCH = 'VALUE_SET_MISMATCH'; - const CSS_SYNTAX_INVALID_URL = 'CSS_SYNTAX_INVALID_URL'; - const MISSING_LAYOUT_ATTRIBUTES = 'MISSING_LAYOUT_ATTRIBUTES'; - const CSS_SYNTAX_MISSING_SELECTOR = 'CSS_SYNTAX_MISSING_SELECTOR'; - const CSS_SYNTAX_INVALID_PROPERTY = 'CSS_SYNTAX_INVALID_PROPERTY'; - const CSS_SYNTAX_INVALID_ATTR_SELECTOR = 'CSS_SYNTAX_INVALID_ATTR_SELECTOR'; - const CSS_SYNTAX_INVALID_URL_PROTOCOL = 'CSS_SYNTAX_INVALID_URL_PROTOCOL'; - const DISALLOWED_MANUFACTURED_BODY = 'DISALLOWED_MANUFACTURED_BODY'; - const CSS_SYNTAX_INCOMPLETE_DECLARATION = 'CSS_SYNTAX_INCOMPLETE_DECLARATION'; - const IMPLIED_LAYOUT_INVALID = 'IMPLIED_LAYOUT_INVALID'; - const CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION = 'CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION'; - const DISALLOWED_CHILD_TAG_NAME = 'DISALLOWED_CHILD_TAG_NAME'; - const CSS_SYNTAX_MISSING_URL = 'CSS_SYNTAX_MISSING_URL'; - const CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE = 'CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE'; - const TAG_REQUIRED_BY_MISSING = 'TAG_REQUIRED_BY_MISSING'; - const INCORRECT_MIN_NUM_CHILD_TAGS = 'INCORRECT_MIN_NUM_CHILD_TAGS'; - const DISALLOWED_SCRIPT_TAG = 'DISALLOWED_SCRIPT_TAG'; - const CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME = 'CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME'; - const DISALLOWED_TAG_ANCESTOR = 'DISALLOWED_TAG_ANCESTOR'; - const DUPLICATE_UNIQUE_TAG_WARNING = 'DUPLICATE_UNIQUE_TAG_WARNING'; - const NON_WHITESPACE_CDATA_ENCOUNTERED = 'NON_WHITESPACE_CDATA_ENCOUNTERED'; - const WARNING_EXTENSION_DEPRECATED_VERSION = 'WARNING_EXTENSION_DEPRECATED_VERSION'; - const CSS_SYNTAX_INVALID_AT_RULE = 'CSS_SYNTAX_INVALID_AT_RULE'; - const DISALLOWED_ATTR = 'DISALLOWED_ATTR'; - const CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT = 'CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT'; - const CSS_SYNTAX_INVALID_DECLARATION = 'CSS_SYNTAX_INVALID_DECLARATION'; - const ATTR_MISSING_REQUIRED_EXTENSION = 'ATTR_MISSING_REQUIRED_EXTENSION'; - const CSS_SYNTAX_DISALLOWED_RELATIVE_URL = 'CSS_SYNTAX_DISALLOWED_RELATIVE_URL'; - const DEPRECATED_TAG = 'DEPRECATED_TAG'; - const BASE_TAG_MUST_PRECEED_ALL_URLS = 'BASE_TAG_MUST_PRECEED_ALL_URLS'; - const CSS_SYNTAX_INVALID_PROPERTY_NOLIST = 'CSS_SYNTAX_INVALID_PROPERTY_NOLIST'; - const ATTR_REQUIRED_BUT_MISSING = 'ATTR_REQUIRED_BUT_MISSING'; - const INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT = 'INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT'; - const UNKNOWN_CODE = 'UNKNOWN_CODE'; - const ATTR_DISALLOWED_BY_IMPLIED_LAYOUT = 'ATTR_DISALLOWED_BY_IMPLIED_LAYOUT'; - const DISALLOWED_PROPERTY_IN_ATTR_VALUE = 'DISALLOWED_PROPERTY_IN_ATTR_VALUE'; - const DUPLICATE_REFERENCE_POINT = 'DUPLICATE_REFERENCE_POINT'; - const TEMPLATE_IN_ATTR_NAME = 'TEMPLATE_IN_ATTR_NAME'; - const DISALLOWED_STYLE_ATTR = 'DISALLOWED_STYLE_ATTR'; - const CSS_EXCESSIVELY_NESTED = 'CSS_EXCESSIVELY_NESTED'; - const CSS_SYNTAX_MALFORMED_MEDIA_QUERY = 'CSS_SYNTAX_MALFORMED_MEDIA_QUERY'; - const CSS_SYNTAX_UNTERMINATED_STRING = 'CSS_SYNTAX_UNTERMINATED_STRING'; - const INVALID_ATTR_VALUE = 'INVALID_ATTR_VALUE'; - const EXTENSION_UNUSED = 'EXTENSION_UNUSED'; - const DISALLOWED_DOMAIN = 'DISALLOWED_DOMAIN'; - const INVALID_JSON_CDATA = 'INVALID_JSON_CDATA'; - const CSS_SYNTAX_DISALLOWED_MEDIA_TYPE = 'CSS_SYNTAX_DISALLOWED_MEDIA_TYPE'; - const INLINE_STYLE_TOO_LONG = 'INLINE_STYLE_TOO_LONG'; - const DEV_MODE_ONLY = 'DEV_MODE_ONLY'; - - /** - * Get error message format. - * - * @since 1.5.0 - * @internal - * - * @param string $code Error code. - * @return string Error message format. - */ - public static function get_error_message_format( $code ) { - switch ( $code ) { - case self::CSS_SYNTAX_ERROR_IN_PSEUDO_SELECTOR: - return __( 'CSS syntax error in tag \'%1$s\' - invalid pseudo selector.', 'amp' ); - case self::WARNING_EXTENSION_UNUSED: - return __( 'The extension \'%1$s\' was found on this page, but is unused (no \'%2$s\' tag seen). This may become an error in the future.', 'amp' ); - case self::CSS_SYNTAX_NOT_A_SELECTOR_START: - return __( 'CSS syntax error in tag \'%1$s\' - not a selector start.', 'amp' ); - case self::DOCUMENT_TOO_COMPLEX: - return __( 'The document is too complex.', 'amp' ); - case self::MANDATORY_TAG_ANCESTOR_WITH_HINT: - return __( 'The tag \'%1$s\' may only appear as a descendant of tag \'%2$s\'. Did you mean \'%3$s\'?', 'amp' ); - case self::CSS_SYNTAX_BAD_URL: - return __( 'CSS syntax error in tag \'%1$s\' - bad url.', 'amp' ); - case self::MANDATORY_ATTR_MISSING: - return __( 'The mandatory attribute \'%1$s\' is missing in tag \'%2$s\'.', 'amp' ); - case self::DEPRECATED_ATTR: - return __( 'The attribute \'%1$s\' in tag \'%2$s\' is deprecated - use \'%3$s\' instead.', 'amp' ); - case self::TAG_REFERENCE_POINT_CONFLICT: - return __( 'The tag \'%1$s\' conflicts with reference point \'%2$s\' because both define reference points.', 'amp' ); - case self::CSS_SYNTAX_STRAY_TRAILING_BACKSLASH: - return __( 'CSS syntax error in tag \'%1$s\' - stray trailing backslash.', 'amp' ); - case self::STYLESHEET_AND_INLINE_STYLE_TOO_LONG: - return __( 'The author stylesheet specified in tag \'style amp-custom\' and the combined inline styles is too large - document contains %1$s bytes whereas the limit is %2$s bytes.', 'amp' ); - case self::UNESCAPED_TEMPLATE_IN_ATTR_VALUE: - return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to \'%3$s\', which contains unescaped Mustache template syntax.', 'amp' ); - case self::GENERAL_DISALLOWED_TAG: - return __( 'The tag \'%1$s\' is disallowed except in specific forms.', 'amp' ); - case self::SPECIFIED_LAYOUT_INVALID: - return __( 'The specified layout \'%1$s\' is not supported by tag \'%2$s\'.', 'amp' ); - case self::CSS_SYNTAX_UNPARSED_INPUT_REMAINS_IN_SELECTOR: - return __( 'CSS syntax error in tag \'%1$s\' - unparsed input remains in selector.', 'amp' ); - case self::CDATA_VIOLATES_BLACKLIST: - return __( 'The text inside tag \'%1$s\' contains \'%2$s\', which is disallowed.', 'amp' ); - case self::CSS_SYNTAX_PROPERTY_DISALLOWED_WITHIN_AT_RULE: - return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed within @%3$s. Allowed properties: %4$s.', 'amp' ); - case self::MANDATORY_TAG_ANCESTOR: - return __( 'The tag \'%1$s\' may only appear as a descendant of tag \'%2$s\'.', 'amp' ); - case self::WARNING_TAG_REQUIRED_BY_MISSING: - return __( 'The tag \'%1$s\' is missing or incorrect, but required by \'%2$s\'. This will soon be an error.', 'amp' ); - case self::TAG_EXCLUDED_BY_TAG: - return __( 'The tag \'%1$s\' is present, but is excluded by the presence of \'%2$s\'.', 'amp' ); - case self::INVALID_PROPERTY_VALUE_IN_ATTR_VALUE: - return __( 'The property \'%1$s\' in attribute \'%2$s\' in tag \'%3$s\' is set to \'%4$s\', which is invalid.', 'amp' ); - case self::MANDATORY_REFERENCE_POINT_MISSING: - return __( 'The mandatory reference point \'%1$s\' for \'%2$s\' is missing.', 'amp' ); - case self::MISSING_URL: - return __( 'Missing URL for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case self::MANDATORY_ANYOF_ATTR_MISSING: - return __( 'The tag \'%1$s\' is missing a mandatory attribute - pick at least one of %2$s.', 'amp' ); - case self::DISALLOWED_TAG: - return __( 'The tag \'%1$s\' is disallowed.', 'amp' ); - case self::CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT: - return __( 'The tag \'%1$s\', a child tag of \'%2$s\', does not satisfy one of the acceptable reference points: %3$s.', 'amp' ); - case self::DOCUMENT_SIZE_LIMIT_EXCEEDED: - return __( 'Document exceeded %1$s bytes limit. Actual size %2$s bytes.', 'amp' ); - case self::INVALID_URL_PROTOCOL: - return __( 'Invalid URL protocol \'%3$s:\' for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case self::MANDATORY_CDATA_MISSING_OR_INCORRECT: - return __( 'The mandatory text inside tag \'%1$s\' is missing or incorrect.', 'amp' ); - case self::INCORRECT_NUM_CHILD_TAGS: - return __( 'Tag \'%1$s\' must have %2$s child tags - saw %3$s child tags.', 'amp' ); - case self::INVALID_UTF8: - return __( 'The document contains invalid UTF8.', 'amp' ); - case self::ATTR_VALUE_REQUIRED_BY_LAYOUT: - return __( 'Invalid value \'%1$s\' for attribute \'%2$s\' in tag \'%3$s\' - for layout \'%4$s\', set the attribute \'%2$s\' to value \'%5$s\'.', 'amp' ); - case self::DUPLICATE_UNIQUE_TAG: - return __( 'The tag \'%1$s\' appears more than once in the document.', 'amp' ); - case self::MISSING_REQUIRED_EXTENSION: - return __( 'The tag \'%1$s\' requires including the \'%2$s\' extension JavaScript.', 'amp' ); - case self::MUTUALLY_EXCLUSIVE_ATTRS: - return __( 'Mutually exclusive attributes encountered in tag \'%1$s\' - pick one of %2$s.', 'amp' ); - case self::DISALLOWED_FIRST_CHILD_TAG_NAME: - return __( 'Tag \'%1$s\' is disallowed as first child of tag \'%2$s\'. First child tag must be one of %3$s.', 'amp' ); - case self::MANDATORY_LAST_CHILD_TAG: - return __( 'Tag \'%1$s\', if present, must be the last child of tag \'%2$s\'.', 'amp' ); - case self::ATTR_DISALLOWED_BY_SPECIFIED_LAYOUT: - return __( 'The attribute \'%1$s\' in tag \'%2$s\' is disallowed by specified layout \'%3$s\'.', 'amp' ); - case self::DISALLOWED_RELATIVE_URL: - return __( 'The relative URL \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\' is disallowed.', 'amp' ); - case self::MANDATORY_PROPERTY_MISSING_FROM_ATTR_VALUE: - return __( 'The property \'%1$s\' is missing from attribute \'%2$s\' in tag \'%3$s\'.', 'amp' ); - case self::MANDATORY_TAG_MISSING: - return __( 'The mandatory tag \'%1$s\' is missing or incorrect.', 'amp' ); - case self::CSS_SYNTAX_QUALIFIED_RULE_HAS_NO_DECLARATIONS: - return __( 'CSS syntax error in tag \'%1$s\' - qualified rule \'%2$s\' has no declarations.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE: - return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is set to the disallowed value \'%3$s\'.', 'amp' ); - case self::CSS_SYNTAX_EOF_IN_PRELUDE_OF_QUALIFIED_RULE: - return __( 'CSS syntax error in tag \'%1$s\' - end of stylesheet encountered in prelude of a qualified rule.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_DOMAIN: - return __( 'CSS syntax error in tag \'%1$s\' - invalid domain \'%2$s\'.', 'amp' ); - case self::TEMPLATE_PARTIAL_IN_ATTR_VALUE: - return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to \'%3$s\', which contains a Mustache template partial.', 'amp' ); - case self::MANDATORY_ONEOF_ATTR_MISSING: - return __( 'The tag \'%1$s\' is missing a mandatory attribute - pick one of %2$s.', 'amp' ); - case self::WRONG_PARENT_TAG: - return __( 'The parent tag of tag \'%1$s\' is \'%2$s\', but it can only be \'%3$s\'.', 'amp' ); - case self::CSS_SYNTAX_UNTERMINATED_COMMENT: - return __( 'CSS syntax error in tag \'%1$s\' - unterminated comment.', 'amp' ); - case self::DUPLICATE_ATTRIBUTE: - return __( 'The tag \'%1$s\' contains the attribute \'%2$s\' repeated multiple times.', 'amp' ); - case self::DUPLICATE_DIMENSION: - return __( 'Multiple image candidates with the same width or pixel density found in attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_QUALIFIED_RULE_MUST_BE_INSIDE_KEYFRAME: - return __( 'CSS syntax error in tag \'%1$s\' - qualified rule \'%2$s\' must be located inside of a keyframe.', 'amp' ); - case self::CSS_SYNTAX_PROPERTY_DISALLOWED_TOGETHER_WITH: - return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed together with \'%3$s\'. Allowed properties: %4$s.', 'amp' ); - case self::STYLESHEET_TOO_LONG: - return __( 'The author stylesheet specified in tag \'%1$s\' is too long - document contains %2$s bytes whereas the limit is %3$s bytes.', 'amp' ); - case self::TAG_NOT_ALLOWED_TO_HAVE_SIBLINGS: - return __( 'Tag \'%1$s\' is not allowed to have any sibling tags (\'%2$s\' should only have 1 child).', 'amp' ); - case self::INVALID_URL: - return __( 'Malformed URL \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case self::CHILD_TAG_DOES_NOT_SATISFY_REFERENCE_POINT_SINGULAR: - return __( 'The tag \'%1$s\', a child tag of \'%2$s\', does not satisfy the reference point \'%3$s\'.', 'amp' ); - case self::VALUE_SET_MISMATCH: - return __( 'Attribute \'%1$s\' in tag \'%2$s\' contains a value that does not match any other tags on the page.', 'amp' ); - case self::CSS_SYNTAX_INVALID_URL: - return __( 'CSS syntax error in tag \'%1$s\' - invalid url \'%2$s\'.', 'amp' ); - case self::MISSING_LAYOUT_ATTRIBUTES: - return __( 'Incomplete layout attributes specified for tag \'%1$s\'. For example, provide attributes \'width\' and \'height\'.', 'amp' ); - case self::CSS_SYNTAX_MISSING_SELECTOR: - return __( 'CSS syntax error in tag \'%1$s\' - missing selector.', 'amp' ); - case self::CSS_SYNTAX_INVALID_PROPERTY: - return __( 'CSS syntax error in tag \'%1$s\' - invalid property \'%2$s\'. The only allowed properties are \'%3$s\'.', 'amp' ); - case self::CSS_SYNTAX_INVALID_ATTR_SELECTOR: - return __( 'CSS syntax error in tag \'%1$s\' - invalid attribute selector.', 'amp' ); - case self::CSS_SYNTAX_INVALID_URL_PROTOCOL: - return __( 'CSS syntax error in tag \'%1$s\' - invalid url protocol \'%2$s:\'.', 'amp' ); - case self::DISALLOWED_MANUFACTURED_BODY: - return __( 'Tag or text which is only allowed inside the body section found outside of the body section.', 'amp' ); - case self::CSS_SYNTAX_INCOMPLETE_DECLARATION: - return __( 'CSS syntax error in tag \'%1$s\' - incomplete declaration.', 'amp' ); - case self::IMPLIED_LAYOUT_INVALID: - return __( 'The implied layout \'%1$s\' is not supported by tag \'%2$s\'.', 'amp' ); - case self::CSS_SYNTAX_PROPERTY_REQUIRES_QUALIFICATION: - return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is disallowed unless the enclosing rule is prefixed with the \'%3$s\' qualification.', 'amp' ); - case self::DISALLOWED_CHILD_TAG_NAME: - return __( 'Tag \'%1$s\' is disallowed as child of tag \'%2$s\'. Child tag must be one of %3$s.', 'amp' ); - case self::CSS_SYNTAX_MISSING_URL: - return __( 'CSS syntax error in tag \'%1$s\' - missing url.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_MEDIA_FEATURE: - return __( 'CSS syntax error in tag \'%1$s\' - disallowed media feature \'%2$s\'.', 'amp' ); - case self::TAG_REQUIRED_BY_MISSING: - return __( 'The tag \'%1$s\' is missing or incorrect, but required by \'%2$s\'.', 'amp' ); - case self::INCORRECT_MIN_NUM_CHILD_TAGS: - return __( 'Tag \'%1$s\' must have a minimum of %2$s child tags - saw %3$s child tags.', 'amp' ); - case self::DISALLOWED_SCRIPT_TAG: - return __( 'Custom JavaScript is not allowed.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_KEYFRAME_INSIDE_KEYFRAME: - return __( 'CSS syntax error in tag \'%1$s\' - keyframe inside keyframe is not allowed.', 'amp' ); - case self::DISALLOWED_TAG_ANCESTOR: - return __( 'The tag \'%1$s\' may not appear as a descendant of tag \'%2$s\'.', 'amp' ); - case self::DUPLICATE_UNIQUE_TAG_WARNING: - return __( 'The tag \'%1$s\' appears more than once in the document. This will soon be an error.', 'amp' ); - case self::NON_WHITESPACE_CDATA_ENCOUNTERED: - return __( 'The tag \'%1$s\' contains text, which is disallowed.', 'amp' ); - case self::WARNING_EXTENSION_DEPRECATED_VERSION: - return __( 'The extension \'%1$s\' is referenced at version \'%2$s\' which is a deprecated version. Please use a more recent version of this extension. This may become an error in the future.', 'amp' ); - case self::CSS_SYNTAX_INVALID_AT_RULE: - return __( 'CSS syntax error in tag \'%1$s\' - saw invalid at rule \'@%2$s\'.', 'amp' ); - case self::DISALLOWED_ATTR: - return __( 'The attribute \'%1$s\' may not appear in tag \'%2$s\'.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_PROPERTY_VALUE_WITH_HINT: - return __( 'CSS syntax error in tag \'%1$s\' - the property \'%2$s\' is set to the disallowed value \'%3$s\'. Allowed values: %4$s.', 'amp' ); - case self::CSS_SYNTAX_INVALID_DECLARATION: - return __( 'CSS syntax error in tag \'%1$s\' - invalid declaration.', 'amp' ); - case self::ATTR_MISSING_REQUIRED_EXTENSION: - return __( 'The attribute \'%1$s\' requires including the \'%2$s\' extension JavaScript.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_RELATIVE_URL: - return __( 'CSS syntax error in tag \'%1$s\' - disallowed relative url \'%2$s\'.', 'amp' ); - case self::DEPRECATED_TAG: - return __( 'The tag \'%1$s\' is deprecated - use \'%2$s\' instead.', 'amp' ); - case self::BASE_TAG_MUST_PRECEED_ALL_URLS: - return __( 'The tag \'%1$s\', which contains URLs, was found earlier in the document than the BASE element.', 'amp' ); - case self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST: - return __( 'CSS syntax error in tag \'%1$s\' - invalid property \'%2$s\'.', 'amp' ); - case self::ATTR_REQUIRED_BUT_MISSING: - return __( 'The attribute \'%1$s\' in tag \'%2$s\' is missing or incorrect, but required by attribute \'%3$s\'.', 'amp' ); - case self::INCONSISTENT_UNITS_FOR_WIDTH_AND_HEIGHT: - return __( 'Inconsistent units for width and height in tag \'%1$s\' - width is specified in \'%2$s\' whereas height is specified in \'%3$s\'.', 'amp' ); - case self::UNKNOWN_CODE: - return __( 'Unknown error.', 'amp' ); - case self::ATTR_DISALLOWED_BY_IMPLIED_LAYOUT: - return __( 'The attribute \'%1$s\' in tag \'%2$s\' is disallowed by implied layout \'%3$s\'.', 'amp' ); - case self::DISALLOWED_PROPERTY_IN_ATTR_VALUE: - return __( 'The property \'%1$s\' in attribute \'%2$s\' in tag \'%3$s\' is disallowed.', 'amp' ); - case self::DUPLICATE_REFERENCE_POINT: - return __( 'The reference point \'%1$s\' for \'%2$s\' must be unique but a duplicate was encountered.', 'amp' ); - case self::TEMPLATE_IN_ATTR_NAME: - return __( 'Mustache template syntax in attribute name \'%1$s\' in tag \'%2$s\'.', 'amp' ); - case self::DISALLOWED_STYLE_ATTR: - return __( 'The inline \'style\' attribute is not allowed in AMP documents. Use \'style amp-custom\' tag instead.', 'amp' ); - case self::CSS_EXCESSIVELY_NESTED: - return __( 'CSS excessively nested in tag \'%1$s\'.', 'amp' ); - case self::CSS_SYNTAX_MALFORMED_MEDIA_QUERY: - return __( 'CSS syntax error in tag \'%1$s\' - malformed media query.', 'amp' ); - case self::CSS_SYNTAX_UNTERMINATED_STRING: - return __( 'CSS syntax error in tag \'%1$s\' - unterminated string.', 'amp' ); - case self::INVALID_ATTR_VALUE: - return __( 'The attribute \'%1$s\' in tag \'%2$s\' is set to the invalid value \'%3$s\'.', 'amp' ); - case self::EXTENSION_UNUSED: - return __( 'The extension \'%1$s\' was found on this page, but is unused. Please remove this extension.', 'amp' ); - case self::DISALLOWED_DOMAIN: - return __( 'The domain \'%3$s\' for attribute \'%1$s\' in tag \'%2$s\' is disallowed.', 'amp' ); - case self::INVALID_JSON_CDATA: - return __( 'The script tag contains invalid JSON that cannot be parsed.', 'amp' ); - case self::CSS_SYNTAX_DISALLOWED_MEDIA_TYPE: - return __( 'CSS syntax error in tag \'%1$s\' - disallowed media type \'%2$s\'.', 'amp' ); - case self::INLINE_STYLE_TOO_LONG: - return __( 'The inline style specified in tag \'%1$s\' is too long - it contains %2$s bytes whereas the limit is %3$s bytes.', 'amp' ); - case self::DEV_MODE_ONLY: - return __( 'Tag \'html\' marked with attribute \'data-ampdevmode\'. Validator will suppress errors regarding any other tag with this attribute.', 'amp' ); - - default: - return __( 'Unknown error', 'amp' ); - } - } /** * Get allowed tags. From 15fb6dd4c06903cfd9238825674da8bb91b99b05 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 24 Nov 2019 18:57:48 -0800 Subject: [PATCH 15/39] Fix validation of __amp_source_origin URL value --- .../class-amp-tag-and-attribute-sanitizer.php | 8 +++ .../php/test-tag-and-attribute-sanitizer.php | 50 ++++++++++++++----- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 6e5d9867a85..16a510e34a5 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -1416,6 +1416,10 @@ private function check_attr_spec_rule_disallowed_relative( DOMElement $node, $at if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_RELATIVE ] ) && ! $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_RELATIVE ] ) { if ( $node->hasAttribute( $attr_name ) ) { foreach ( $this->extract_attribute_urls( $node->getAttributeNode( $attr_name ) ) as $url ) { + if ( '__amp_source_origin' === $url ) { + return AMP_Rule_Spec::PASS; + } + $parsed_url = wp_parse_url( $url ); /* @@ -1436,6 +1440,10 @@ private function check_attr_spec_rule_disallowed_relative( DOMElement $node, $at foreach ( $attr_spec_rule[ AMP_Rule_Spec::ALTERNATIVE_NAMES ] as $alternative_name ) { if ( $node->hasAttribute( $alternative_name ) ) { foreach ( $this->extract_attribute_urls( $node->getAttributeNode( $alternative_name ), $attr_name ) as $url ) { + if ( '__amp_source_origin' === $url ) { + return AMP_Rule_Spec::PASS; + } + $parsed_url = wp_parse_url( $url ); if ( empty( $parsed_url['scheme'] ) ) { return AMP_Rule_Spec::FAIL; diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 79f294c5c3a..8e3b25bb34e 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -93,6 +93,16 @@ public function get_body_data() { '+1 (23) 456-789', '', [], // Important: This needs to be empty because the amp-call-tracking is stripped. + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX, + 'node_name' => 'config', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'node_name' => 'amp-call-tracking', + ], + ], ], 'amp-embed' => [ @@ -1758,20 +1768,20 @@ public function test_is_missing_mandatory_attribute() { * @dataProvider get_body_data * @group allowed-tags * - * @param string $source Markup to process. - * @param string $expected The markup to expect. - * @param array $expected_scripts The AMP component script names that are obtained through sanitization. - * @param array|null $expected_error_codes Expected validation error codes. + * @param string $source Markup to process. + * @param string $expected The markup to expect. + * @param array $expected_scripts The AMP component script names that are obtained through sanitization. + * @param array|null $expected_errors Expected validation errors, either codes or validation error subsets. @todo Make always default to array. */ - public function test_body_sanitizer( $source, $expected = null, $expected_scripts = [], $expected_error_codes = null ) { - $expected = isset( $expected ) ? $expected : $source; - $dom = AMP_DOM_Utils::get_dom_from_content( $source ); - $actual_error_codes = []; - $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( + public function test_body_sanitizer( $source, $expected = null, $expected_scripts = [], $expected_errors = null ) { + $expected = isset( $expected ) ? $expected : $source; + $dom = AMP_DOM_Utils::get_dom_from_content( $source ); + $actual_errors = []; + $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom, [ - 'validation_error_callback' => static function( $error ) use ( &$actual_error_codes ) { - $actual_error_codes[] = $error['code']; + 'validation_error_callback' => static function( $error ) use ( &$actual_errors ) { + $actual_errors[] = $error; return true; }, ] @@ -1781,8 +1791,22 @@ public function test_body_sanitizer( $source, $expected = null, $expected_script $this->assertEqualMarkup( $expected, $content ); $this->assertEqualSets( $expected_scripts, array_keys( $sanitizer->get_scripts() ) ); - if ( is_array( $expected_error_codes ) ) { - $this->assertEqualSets( $expected_error_codes, $actual_error_codes ); + + if ( isset( $expected_errors ) ) { + $expected_errors = array_map( + static function ( $error ) { + if ( is_string( $error ) ) { + return [ 'code' => $error ]; + } else { + return $error; + } + }, + $expected_errors + ); + $this->assertEquals( wp_list_pluck( $expected_errors, 'code' ), wp_list_pluck( $actual_errors, 'code' ) ); + foreach ( $expected_errors as $i => $expected_error ) { + $this->assertArraySubset( $expected_error, $actual_errors[ $i ] ); + } } } From c55c58ccf6f8ccbc229e500f55797c018628a690 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 24 Nov 2019 19:33:37 -0800 Subject: [PATCH 16/39] Eliminate duplicated testing; add code checking --- .../php/test-tag-and-attribute-sanitizer.php | 152 +++++++++--------- 1 file changed, 78 insertions(+), 74 deletions(-) diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 8e3b25bb34e..ef8512be9fc 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -69,6 +69,7 @@ public function get_body_data() { 'not allowednot ok', '', [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'amp-animation' => [ @@ -87,6 +88,7 @@ public function get_body_data() { 'bad--and not great: +1 (23) 456-789more badnot great', '', [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'amp-call-tracking_blacklisted_config' => [ @@ -1728,7 +1730,7 @@ static function() { 'Whatever', 'Whatever', [], - [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE, AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL ], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL, AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE ], ], 'cdata_html_comments' => [ @@ -1762,54 +1764,6 @@ public function test_is_missing_mandatory_attribute() { $this->assertFalse( $sanitizer->is_missing_mandatory_attribute( $spec, $node ) ); } - /** - * Test sanitization of tags and attributes. - * - * @dataProvider get_body_data - * @group allowed-tags - * - * @param string $source Markup to process. - * @param string $expected The markup to expect. - * @param array $expected_scripts The AMP component script names that are obtained through sanitization. - * @param array|null $expected_errors Expected validation errors, either codes or validation error subsets. @todo Make always default to array. - */ - public function test_body_sanitizer( $source, $expected = null, $expected_scripts = [], $expected_errors = null ) { - $expected = isset( $expected ) ? $expected : $source; - $dom = AMP_DOM_Utils::get_dom_from_content( $source ); - $actual_errors = []; - $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( - $dom, - [ - 'validation_error_callback' => static function( $error ) use ( &$actual_errors ) { - $actual_errors[] = $error; - return true; - }, - ] - ); - $sanitizer->sanitize(); - $content = AMP_DOM_Utils::get_content_from_dom( $dom ); - - $this->assertEqualMarkup( $expected, $content ); - $this->assertEqualSets( $expected_scripts, array_keys( $sanitizer->get_scripts() ) ); - - if ( isset( $expected_errors ) ) { - $expected_errors = array_map( - static function ( $error ) { - if ( is_string( $error ) ) { - return [ 'code' => $error ]; - } else { - return $error; - } - }, - $expected_errors - ); - $this->assertEquals( wp_list_pluck( $expected_errors, 'code' ), wp_list_pluck( $actual_errors, 'code' ) ); - foreach ( $expected_errors as $i => $expected_error ) { - $this->assertArraySubset( $expected_error, $actual_errors[ $i ] ); - } - } - } - /** * Get data for testing sanitization in the html. * @@ -1841,13 +1795,13 @@ public function get_html_data() { '', // phpcs:ignore '', [], - [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX ], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX, AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING ], ], 'bad_meta_ua_compatible' => [ '', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE, AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING ], ], 'bad_meta_charset' => [ 'Mojibake?', @@ -1859,7 +1813,7 @@ public function get_html_data() { '', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_PROPERTY_IN_ATTR_VALUE, AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING ], ], 'edge_meta_ua_compatible' => [ '', @@ -1958,10 +1912,10 @@ public function get_html_data() { ', [ 'amp-bind' ], [ + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, AMP_Tag_And_Attribute_Sanitizer::CDATA_TOO_LONG, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, - AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, ], ]; @@ -1996,7 +1950,7 @@ public function get_html_data() { // Also include the body tests. $html_doc_format = '%s'; - foreach ( $this->get_body_data() as $body_test ) { + foreach ( $this->get_body_data() as $name => $body_test ) { $html_test = [ sprintf( $html_doc_format, array_shift( $body_test ) ), ]; @@ -2004,10 +1958,12 @@ public function get_html_data() { if ( isset( $expected ) ) { $expected = sprintf( $html_doc_format, $expected ); } - $html_test[] = $expected; - $html_test[] = array_shift( $body_test ); - $html_test[] = array_shift( $body_test ); - $data[] = $html_test; + $html_test[] = $expected; + $expected_scripts = array_shift( $body_test ); + $html_test[] = isset( $expected_scripts ) ? $expected_scripts : []; + $html_test[] = array_shift( $body_test ); + + $data[ "html_{$name}" ] = $html_test; } return $data; @@ -2018,22 +1974,23 @@ public function get_html_data() { * * @dataProvider get_html_data * @group allowed-tags + * @covers AMP_Tag_And_Attribute_Sanitizer::sanitize() * - * @param string $source Markup to process. - * @param string $expected The markup to expect. - * @param array $expected_scripts The AMP component script names that are obtained through sanitization. - * @param array|null $expected_error_codes Expected validation error codes. + * @param string $source Markup to process. + * @param string $expected The markup to expect. + * @param array $expected_scripts The AMP component script names that are obtained through sanitization. + * @param array|null $expected_errors Expected validation errors, either codes or validation error subsets. @todo Make always default to array. */ - public function test_html_sanitizer( $source, $expected = null, $expected_scripts = [], $expected_error_codes = null ) { - $expected = isset( $expected ) ? $expected : $source; - $dom = AMP_DOM_Utils::get_dom( $source ); - $actual_error_codes = []; - $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( + public function test_sanitize( $source, $expected = null, $expected_scripts = [], $expected_errors = null ) { + $expected = isset( $expected ) ? $expected : $source; + $dom = AMP_DOM_Utils::get_dom( $source ); + $actual_errors = []; + $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom, [ 'use_document_element' => true, - 'validation_error_callback' => static function( $error ) use ( &$actual_error_codes ) { - $actual_error_codes[] = $error['code']; + 'validation_error_callback' => static function( $error ) use ( &$actual_errors ) { + $actual_errors[] = $error; return true; }, ] @@ -2042,15 +1999,62 @@ public function test_html_sanitizer( $source, $expected = null, $expected_script $content = AMP_DOM_Utils::get_content_from_dom_node( $dom, $dom->documentElement ); $this->assertEqualMarkup( $expected, $content ); - if ( is_array( $expected_error_codes ) ) { - $this->assertEqualSets( $expected_error_codes, $actual_error_codes ); - } + $this->assertEqualSets( $expected_scripts, array_keys( $sanitizer->get_scripts() ) ); - if ( is_array( $expected_scripts ) ) { - $this->assertEqualSets( $expected_scripts, array_keys( $sanitizer->get_scripts() ) ); + if ( isset( $expected_errors ) ) { + $expected_errors = array_map( + static function ( $error ) { + if ( is_string( $error ) ) { + return [ 'code' => $error ]; + } else { + return $error; + } + }, + $expected_errors + ); + $this->assertEquals( wp_list_pluck( $expected_errors, 'code' ), wp_list_pluck( $actual_errors, 'code' ) ); + foreach ( $expected_errors as $i => $expected_error ) { + $this->assertArraySubset( $expected_error, $actual_errors[ $i ] ); + } } } + /** + * Ensure that sanitizing with use_document_element arg not supplied works as expected. + * + * @covers AMP_Tag_And_Attribute_Sanitizer::sanitize() + */ + public function test_sanitize_body_only() { + $source = 'Hello'; + $expected = 'Hello'; + + $dom = AMP_DOM_Utils::get_dom( $source ); + $actual_errors = []; + $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( + $dom, + [ + 'use_document_element' => false, + 'validation_error_callback' => static function( $error ) use ( &$actual_errors ) { + $actual_errors[] = $error; + return true; + }, + ] + ); + + $sanitizer->sanitize(); + $actual = AMP_DOM_Utils::get_content_from_dom( $dom ); + $this->assertEquals( $expected, $actual ); + $this->assertEqualSets( [ 'amp-sidebar' ], array_keys( $sanitizer->get_scripts() ) ); + + $this->assertCount( 1, $actual_errors ); + $this->assertArraySubset( + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, // @todo Should be DISALLOWED_SCRIPT_TAG. + ], + $actual_errors[0] + ); + } + /** * Get data for replace_node_with_children_validation_errors test. * From 7f4bf4e1adba077455df1744fff42d1191fe688a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 24 Nov 2019 20:16:48 -0800 Subject: [PATCH 17/39] Add DISALLOWED_DESCENDANT_TAG error code --- .../sanitizers/class-amp-tag-and-attribute-sanitizer.php | 3 ++- tests/php/test-tag-and-attribute-sanitizer.php | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 16a510e34a5..41bd76c7a78 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -26,6 +26,7 @@ class AMP_Tag_And_Attribute_Sanitizer extends AMP_Base_Sanitizer { const DISALLOWED_TAG = 'DISALLOWED_TAG'; + const DISALLOWED_DESCENDANT_TAG = 'DISALLOWED_DESCENDANT_TAG'; const DISALLOWED_ATTR = 'DISALLOWED_ATTR'; const DISALLOWED_PROCESSING_INSTRUCTION = 'DISALLOWED_PROCESSING_INSTRUCTION'; const CDATA_VIOLATES_BLACKLIST = 'CDATA_VIOLATES_BLACKLIST'; @@ -1790,7 +1791,7 @@ private function remove_disallowed_descendants( DOMElement $node, $allowed_desce foreach ( $child_elements as $child_element ) { if ( ! in_array( $child_element->nodeName, $allowed_descendants, true ) ) { - $this->remove_invalid_child( $child_element ); + $this->remove_invalid_child( $child_element, [ 'code' => self::DISALLOWED_DESCENDANT_TAG ] ); } else { $this->remove_disallowed_descendants( $child_element, $allowed_descendants ); } diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index ef8512be9fc..a56037bd7ed 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -362,6 +362,12 @@ static function () { $html, preg_replace( '#<\w+[^>]*>bad#', '', $html ), [ 'amp-story', 'amp-analytics', 'amp-twitter', 'amp-youtube', 'amp-video' ], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_DESCENDANT_TAG, + 'node_name' => 'button', + ], + ], ]; } ), From df9666745e96daba8ae29dce07ea592b614f9075 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 24 Nov 2019 20:36:34 -0800 Subject: [PATCH 18/39] Fix checking of empty URL before relative URL --- .../sanitizers/class-amp-tag-and-attribute-sanitizer.php | 8 ++++---- tests/php/test-tag-and-attribute-sanitizer.php | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 41bd76c7a78..d3f93dea019 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -43,7 +43,7 @@ class AMP_Tag_And_Attribute_Sanitizer extends AMP_Base_Sanitizer { const INVALID_URL_PROTOCOL = 'INVALID_URL_PROTOCOL'; const INVALID_URL = 'INVALID_URL'; const DISALLOWED_RELATIVE_URL = 'DISALLOWED_RELATIVE_URL'; - const DISALLOWED_EMPTY = 'DISALLOWED_EMPTY'; + const DISALLOWED_EMPTY_URL = 'DISALLOWED_EMPTY_URL'; const DISALLOWED_DOMAIN = 'DISALLOWED_DOMAIN'; const INVALID_BLACKLISTED_VALUE_REGEX = 'INVALID_BLACKLISTED_VALUE_REGEX'; const DISALLOWED_PROPERTY_IN_ATTR_VALUE = 'DISALLOWED_PROPERTY_IN_ATTR_VALUE'; @@ -1021,12 +1021,12 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_valid_url( $node, $attr_name, $attr_spec_rule ) ) { $error_code = self::INVALID_URL; + } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) && + AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_empty( $node, $attr_name, $attr_spec_rule ) ) { + $error_code = self::DISALLOWED_EMPTY_URL; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_RELATIVE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_relative( $node, $attr_name, $attr_spec_rule ) ) { $error_code = self::DISALLOWED_RELATIVE_URL; - } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_EMPTY ] ) && - AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_empty( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = self::DISALLOWED_EMPTY; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::DISALLOWED_DOMAIN ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_domain( $node, $attr_name, $attr_spec_rule ) ) { $error_code = self::DISALLOWED_DOMAIN; diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index a56037bd7ed..2b1ee5f0c75 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -795,6 +795,7 @@ static function() { '', '', [ 'amp-user-notification' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_EMPTY_URL ], ], 'allowed_empty_attr' => [ From 7b32198bee7d4a5b8bfd4bc3746b52348a528b54 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 24 Nov 2019 21:28:46 -0800 Subject: [PATCH 19/39] Add assertions for specific error codes --- .../php/test-tag-and-attribute-sanitizer.php | 336 +++++++++++++++--- 1 file changed, 284 insertions(+), 52 deletions(-) diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 2b1ee5f0c75..90a5206b922 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -123,6 +123,12 @@ public function get_body_data() { '', '', [], // Empty because invalid. + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'attributes' => [ 'data-href' ], + ], + ], ], 'amp-facebook-like' => [ @@ -135,6 +141,12 @@ public function get_body_data() { '', '', [], // Empty because invalid. + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'attributes' => [ 'data-href' ], + ], + ], ], 'amp-fit-text' => [ @@ -153,6 +165,12 @@ public function get_body_data() { '', '', [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'attributes' => [ 'data-gistid' ], + ], + ], ], 'amp-iframe' => [ @@ -165,6 +183,7 @@ public function get_body_data() { '', '', [ 'amp-iframe' ], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL ], ], 'amp-ima-video' => [ @@ -183,6 +202,8 @@ public function get_body_data() { 'amp-ima-video_missing_required_attribute' => [ '', '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING ], ], 'amp-imgur' => [ @@ -252,15 +273,31 @@ public function get_body_data() { ], 'invalid_element_stripped' => [ - '

Foo text

', + '

Foo text

', '

Foo text

', [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'nonexistent', + ], + ], ], 'nested_invalid_elements_stripped' => [ '

Example Summary

Example expanded text

', '

Example Summary

Example expanded text

', [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'bad-summary', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'bad-details', + ], + ], ], // AMP-NEXT-PAGE > [separator]. @@ -376,6 +413,7 @@ static function () { '
BAD REFERENCE POINTS
', '
BAD REFERENCE POINTS
', [], + array_fill( 0, 8, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ), ], 'amp-position-observer' => [ @@ -506,6 +544,8 @@ static function () { 'json_linked_data_with_bad_cdata' => [ '', '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS ], ], 'facebook' => [ @@ -543,11 +583,15 @@ static function () { 'merge_two_attr_specs' => [ '
Whatever
', '
Whatever
', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'attribute_value_blacklisted_by_regex_removed' => [ 'Click me.', 'Click me.', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX ], ], 'host_relative_url_allowed' => [ @@ -581,11 +625,15 @@ static function () { 'node_with_non_parseable_url_removed' => [ 'Invalid Link', 'Invalid Link', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL ], ], 'node_with_non_parseable_url_leftovers_cleaned_up' => [ 'Invalid Link', 'Invalid Link', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL ], ], 'attribute_value_valid' => [ @@ -599,6 +647,16 @@ static function () { '', '', [], // No scripts because removed. + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE, + 'node_name' => 'type', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'node_name' => 'template', + ], + ], ], 'attribute_requirements_overriden_by_placeholders_within_template' => [ @@ -616,6 +674,11 @@ static function () { 'attribute_requirements_not_overriden_by_placeholders_outside_of_template' => [ '', '', + [], + [ + AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX, + AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + ], ], 'attribute_requirements_overriden_in_indirect_template_parents' => [ @@ -628,6 +691,10 @@ static function () { '', '', [ 'amp-mustache' ], + [ + AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX, + AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + ], ], 'attribute_amp_accordion_value' => call_user_func( @@ -669,11 +736,15 @@ static function() { 'attribute_value_with_blacklisted_regex_removed' => [ 'Click me.', 'Click me.', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX ], ], 'attribute_value_with_blacklisted_multi-part_regex_removed' => [ 'Click me.', 'Click me.', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX ], ], 'attribute_value_with_required_regex' => [ @@ -683,6 +754,8 @@ static function() { 'attribute_value_with_disallowed_required_regex_removed' => [ 'Click me.', 'Click me.', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE ], ], 'attribute_value_with_required_value_casei_lower' => [ @@ -700,6 +773,8 @@ static function() { 'attribute_value_with_bad_value_casei_removed' => [ 'Click.me.', 'Click.me.', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_CASEI ], ], 'attribute_value_with_value_regex_casei_lower' => [ @@ -719,39 +794,50 @@ static function() { '', '', [ 'amp-dailymotion' ], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE ], ], 'attribute_bad_attr_with_no_value_removed' => [ '
something here
', '
something here
', [ 'amp-ad' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'attribute_bad_attr_with_value_removed' => [ 'something here', 'something here', [ 'amp-ad' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'remove_node_with_invalid_mandatory_attribute' => [ // script only allows application/json, nothing else. '', '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'remove_node_without_mandatory_attribute' => [ '', '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'remove_script_with_async_attribute' => [ '', // phpcs:ignore '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'remove_invalid_json_script' => [ '', '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'allow_node_with_valid_mandatory_attribute' => [ @@ -763,26 +849,36 @@ static function() { 'nodes_with_non_whitelisted_tags_replaced_by_children' => [ 'this is some text inside the invalid node', 'this is some text inside the invalid node', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'empty_parent_nodes_of_non_whitelisted_tags_removed' => [ '
', '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'non_empty_parent_nodes_of_non_whitelisted_tags_removed' => [ '
', '
', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'replace_non_whitelisted_node_with_children' => [ '

This is some text with a disallowed tag in the middle of it.

', '

This is some text with a disallowed tag in the middle of it.

', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'remove_attribute_on_node_with_missing_mandatory_parent' => [ '
This is a test.
', '
This is a test.
', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'leave_attribute_on_node_with_present_mandatory_parent' => [ @@ -806,6 +902,16 @@ static function() { 'This node is not allowed here.This node is not allowed here.', '', [ 'amp-sidebar' ], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'amp-app-banner', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'amp-app-banner', + ], + ], ], 'amp_story_with_amp_sidebar' => [ @@ -883,6 +989,8 @@ static function() { 'remove_node_without_mandatory_ancestor' => [ '
All I have is this div, when all you want is a noscript tag.
', '
All I have is this div, when all you want is a noscript tag.
', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'amp-img_with_good_protocols' => [ @@ -896,16 +1004,22 @@ static function() { 'allowed_tag_only' => [ '

Text

', '

Text

', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'disallowed_attributes' => [ 'Link', 'Link', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX ], ], 'onclick_attribute' => [ 'Link', 'Link', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'on_attribute' => [ @@ -915,11 +1029,33 @@ static function() { 'multiple_disallowed_attributes' => [ 'Link', 'Link', + [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX, + 'node_name' => 'style', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, + 'node_name' => 'onclick', + ], + ], ], 'attribute_recursive' => [ '', '', + [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, + 'node_name' => 'onclick', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX, + 'node_name' => 'style', + ], + ], ], 'no_strip_amp_tags' => [ @@ -933,6 +1069,8 @@ static function() { 'a_with_invalid_name' => [ 'Shadow Root!', 'Shadow Root!', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX ], ], 'a_with_attachment_rel_plus_another_valid_value' => [ @@ -950,11 +1088,15 @@ static function() { 'a_with_target_uppercase_blank' => [ 'Link', 'Link', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE ], ], 'a_with_target_new' => [ 'Link', 'Link', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE ], ], 'a_with_target_self' => [ @@ -964,6 +1106,8 @@ static function() { 'a_with_target_invalid' => [ 'Link', 'Link', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE ], ], 'a_with_href_invalid' => [ @@ -1005,36 +1149,50 @@ static function() { 'a_empty_with_children_with_restricted_attributes' => [ 'Red&Orange', 'Red&Orange', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX, AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX ], ], 'spans_with_xml_namespaced_attributes' => [ '

holamundo

', '

holamundo

', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'h1_with_size' => [ '

Headline

', '

Headline

', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'font_tag' => [ 'Headline', 'Headline', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'span_with_custom_attr' => [ 'value', 'value', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'a_with_custom_protocol' => [ 'value', 'value', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL ], ], 'a_with_wrong_host' => [ 'value', 'value', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL ], ], 'a_with_encoded_host' => [ 'value', @@ -1043,16 +1201,22 @@ static function() { 'a_with_wrong_schemeless_host' => [ 'value', 'value', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL ], ], 'a_with_mail_host' => [ 'value', 'value', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL ], ], // font is removed so we should check that other elements are checked as well. 'font_with_other_bad_elements' => [ 'HeadlineSpan', 'HeadlineSpan', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX ], ], 'amp_bind_attr' => [ @@ -1077,6 +1241,12 @@ static function() { 'test

', 'test

', [ 'amp-bind' ], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, + 'node_name' => 'data-amp-bind-unrecognized', + ], + ], ], 'amp-state' => [ @@ -1089,6 +1259,9 @@ static function() { 'bad', '', [], + [ + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, // @todo This should actually be DISALLOWED_FIRST_CHILD_TAG. + ], ], 'amp-state-src' => [ @@ -1147,6 +1320,7 @@ static function() { '
Foo!
', '
Foo!
', [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ], ], 'amp_live_list_sort' => [ @@ -1200,24 +1374,35 @@ static function() { '', '', [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX_CASEI ], ], 'amp-img-layout-unknown' => [ '', '', [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX_CASEI ], ], 'non-layout-span-element-attrs' => [ 'Test', 'Test', [], + array_fill( 0, 7, AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR ), ], 'non-layout-col-element-attrs' => [ '
123
', '
123
', [], + [ + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, + AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX, + AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX, + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, + AMP_Tag_And_Attribute_Sanitizer::INVALID_BLACKLISTED_VALUE_REGEX, + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_ATTR, + ], ], 'amp-geo' => [ @@ -1230,6 +1415,7 @@ static function() { '
bad
', '', [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'amp-addthis-valid' => [ @@ -1349,6 +1535,7 @@ static function() { '', '', [ 'amp-addthis' ], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_URL_PROTOCOL ], ], 'amp-3d-gltf' => [ @@ -1439,12 +1626,14 @@ static function() { '', '', [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'amp-image-slider-more-bad-children' => [ 'Not allowedforbidden
This apple is green
not allowed
This apple is red
not ok
', '', [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'amp-fx-collection' => [ @@ -1749,28 +1938,6 @@ static function() { ]; } - /** - * Tests is_missing_mandatory_attribute - * - * @see AMP_Tag_And_Attribute_Sanitizer::is_missing_mandatory_attribute() - */ - public function test_is_missing_mandatory_attribute() { - $spec = [ - 'data-gistid' => [ - 'mandatory' => true, - ], - 'noloading' => [], - ]; - $dom = new DomDocument(); - $node = new DOMElement( 'amp-gist' ); - $dom->appendChild( $node ); - $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom ); - $this->assertTrue( $sanitizer->is_missing_mandatory_attribute( $spec, $node ) ); - - $node->setAttribute( 'data-gistid', 'foo-value' ); - $this->assertFalse( $sanitizer->is_missing_mandatory_attribute( $spec, $node ) ); - } - /** * Get data for testing sanitization in the html. * @@ -1785,18 +1952,54 @@ public function get_html_data() { 'script_tag_externals' => [ '', // phpcs:ignore '', + [], + array_fill( + 0, + 4, + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'script', + ] + ), ], 'script_tag_inline' => [ '', '', + [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'script', + ], + ], ], 'style_external' => [ '', // phpcs:ignore '', + [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INVALID_ATTR_VALUE_REGEX, + 'node_name' => 'href', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'node_name' => 'link', + ], + ], ], 'style_inline' => [ '', '', + [], + array_fill( + 0, + 2, + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'node_name' => 'style', + ] + ), ], 'bad_external_font' => [ '', // phpcs:ignore @@ -1842,6 +2045,8 @@ public function get_html_data() { 'head_with_invalid_nodes' => [ 'bad! other', 'bad!

other

', + [], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], ], 'head_with_duplicate_charset' => [ '

Content

', @@ -1862,6 +2067,17 @@ public function get_html_data() { 'link_without_valid_mandatory_href' => [ '', '', + [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INVALID_URL, + 'node_name' => 'href', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'node_name' => 'link', + ], + ], ], 'cdata_css_important' => [ '', @@ -1958,19 +2174,15 @@ public function get_html_data() { // Also include the body tests. $html_doc_format = '%s'; foreach ( $this->get_body_data() as $name => $body_test ) { - $html_test = [ - sprintf( $html_doc_format, array_shift( $body_test ) ), - ]; - $expected = array_shift( $body_test ); - if ( isset( $expected ) ) { - $expected = sprintf( $html_doc_format, $expected ); + if ( isset( $data[ $name ] ) ) { + throw new Exception( "Test data error: duplicate test name: $name" ); } - $html_test[] = $expected; - $expected_scripts = array_shift( $body_test ); - $html_test[] = isset( $expected_scripts ) ? $expected_scripts : []; - $html_test[] = array_shift( $body_test ); - - $data[ "html_{$name}" ] = $html_test; + $html_test = $body_test; + $html_test[0] = sprintf( $html_doc_format, $html_test[0] ); + if ( isset( $html_test[1] ) ) { + $html_test[1] = sprintf( $html_doc_format, $html_test[1] ); + } + $data[ $name ] = $html_test; } return $data; @@ -1986,9 +2198,9 @@ public function get_html_data() { * @param string $source Markup to process. * @param string $expected The markup to expect. * @param array $expected_scripts The AMP component script names that are obtained through sanitization. - * @param array|null $expected_errors Expected validation errors, either codes or validation error subsets. @todo Make always default to array. + * @param array|null $expected_errors Expected validation errors, either codes or validation error subsets. */ - public function test_sanitize( $source, $expected = null, $expected_scripts = [], $expected_errors = null ) { + public function test_sanitize( $source, $expected = null, $expected_scripts = [], $expected_errors = [] ) { $expected = isset( $expected ) ? $expected : $source; $dom = AMP_DOM_Utils::get_dom( $source ); $actual_errors = []; @@ -2008,21 +2220,19 @@ public function test_sanitize( $source, $expected = null, $expected_scripts = [] $this->assertEqualSets( $expected_scripts, array_keys( $sanitizer->get_scripts() ) ); - if ( isset( $expected_errors ) ) { - $expected_errors = array_map( - static function ( $error ) { - if ( is_string( $error ) ) { - return [ 'code' => $error ]; - } else { - return $error; - } - }, - $expected_errors - ); - $this->assertEquals( wp_list_pluck( $expected_errors, 'code' ), wp_list_pluck( $actual_errors, 'code' ) ); - foreach ( $expected_errors as $i => $expected_error ) { - $this->assertArraySubset( $expected_error, $actual_errors[ $i ] ); - } + $expected_errors = array_map( + static function ( $error ) { + if ( is_string( $error ) ) { + return [ 'code' => $error ]; + } else { + return $error; + } + }, + $expected_errors + ); + $this->assertEquals( wp_list_pluck( $expected_errors, 'code' ), wp_list_pluck( $actual_errors, 'code' ) ); + foreach ( $expected_errors as $i => $expected_error ) { + $this->assertArraySubset( $expected_error, $actual_errors[ $i ] ); } } @@ -2062,6 +2272,28 @@ public function test_sanitize_body_only() { ); } + /** + * Tests is_missing_mandatory_attribute + * + * @see AMP_Tag_And_Attribute_Sanitizer::is_missing_mandatory_attribute() + */ + public function test_is_missing_mandatory_attribute() { + $spec = [ + 'data-gistid' => [ + 'mandatory' => true, + ], + 'noloading' => [], + ]; + $dom = new DomDocument(); + $node = new DOMElement( 'amp-gist' ); + $dom->appendChild( $node ); + $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom ); + $this->assertTrue( $sanitizer->is_missing_mandatory_attribute( $spec, $node ) ); + + $node->setAttribute( 'data-gistid', 'foo-value' ); + $this->assertFalse( $sanitizer->is_missing_mandatory_attribute( $spec, $node ) ); + } + /** * Get data for replace_node_with_children_validation_errors test. * From 980876199a3cee02d0af5cac2aa5d037b5df0cf7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 10:07:06 -0800 Subject: [PATCH 20/39] Move erroneous sanitiation inside of validate_tag_spec_for_node method --- .../class-amp-tag-and-attribute-sanitizer.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index d3f93dea019..5c74e9a2aad 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -629,6 +629,13 @@ private function process_node( DOMElement $node ) { } } + if ( ! empty( $tag_spec[ AMP_Rule_Spec::DESCENDANT_TAG_LIST ] ) ) { + $allowed_tags = AMP_Allowed_Tags_Generated::get_descendant_tag_list( $tag_spec[ AMP_Rule_Spec::DESCENDANT_TAG_LIST ] ); + if ( ! empty( $allowed_tags ) ) { + $this->remove_disallowed_descendants( $node, $allowed_tags ); + } + } + // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. // After attributes have been sanitized (and potentially removed), if mandatory attribute(s) are missing, remove the element. $missing_mandatory_attributes = $this->get_missing_mandatory_attributes( $merged_attr_spec_list, $node ); @@ -761,6 +768,8 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { * that can be an immediate parent or an ancestor of this node, then make * sure those restrictions are met. * + * This method has no side effects. It should not sanitize the DOM. It is purely to see if the spec matches. + * * @since 0.5 * * @param DOMElement $node The node to validate. @@ -790,13 +799,6 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { return false; } - if ( ! empty( $tag_spec[ AMP_Rule_Spec::DESCENDANT_TAG_LIST ] ) ) { - $allowed_tags = AMP_Allowed_Tags_Generated::get_descendant_tag_list( $tag_spec[ AMP_Rule_Spec::DESCENDANT_TAG_LIST ] ); - if ( ! empty( $allowed_tags ) ) { - $this->remove_disallowed_descendants( $node, $allowed_tags ); - } - } - return ! ( ! empty( $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ) && ! $this->check_valid_children( $node, $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ) ); } From 374e0d366097ae0d7432ae15bed87d7e5bc88281 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 10:40:25 -0800 Subject: [PATCH 21/39] Add fine-grained error codes for elements that have bad ancestors or descendants --- .../class-amp-tag-and-attribute-sanitizer.php | 61 ++++++++++++++----- .../php/test-tag-and-attribute-sanitizer.php | 22 +++---- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 5c74e9a2aad..f63c53fbab8 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -26,6 +26,13 @@ class AMP_Tag_And_Attribute_Sanitizer extends AMP_Base_Sanitizer { const DISALLOWED_TAG = 'DISALLOWED_TAG'; + const DISALLOWED_CHILD_TAG = 'DISALLOWED_CHILD_TAG'; + const DISALLOWED_FIRST_CHILD_TAG = 'DISALLOWED_FIRST_CHILD_TAG'; + const INCORRECT_NUM_CHILD_TAGS = 'INCORRECT_NUM_CHILD_TAGS'; + const INCORRECT_MIN_NUM_CHILD_TAGS = 'INCORRECT_MIN_NUM_CHILD_TAGS'; + const WRONG_PARENT_TAG = 'WRONG_PARENT_TAG'; + const DISALLOWED_TAG_ANCESTOR = 'DISALLOWED_TAG_ANCESTOR'; + const MANDATORY_TAG_ANCESTOR = 'MANDATORY_TAG_ANCESTOR'; const DISALLOWED_DESCENDANT_TAG = 'DISALLOWED_DESCENDANT_TAG'; const DISALLOWED_ATTR = 'DISALLOWED_ATTR'; const DISALLOWED_PROCESSING_INSTRUCTION = 'DISALLOWED_PROCESSING_INSTRUCTION'; @@ -448,18 +455,32 @@ private function process_node( DOMElement $node ) { */ $rule_spec_list_to_validate = []; $rule_spec_list = []; + $invalid_rule_spec_list = []; if ( isset( $this->allowed_tags[ $node->nodeName ] ) ) { $rule_spec_list = $this->allowed_tags[ $node->nodeName ]; } foreach ( $rule_spec_list as $id => $rule_spec ) { - if ( $this->validate_tag_spec_for_node( $node, $rule_spec[ AMP_Rule_Spec::TAG_SPEC ] ) ) { + $validity = $this->validate_tag_spec_for_node( $node, $rule_spec[ AMP_Rule_Spec::TAG_SPEC ] ); + if ( true === $validity ) { $rule_spec_list_to_validate[ $id ] = $this->get_rule_spec_list_to_validate( $node, $rule_spec ); + } else { + $invalid_rule_spec_list[] = [ + 'error' => $validity, + 'tag_spec' => $rule_spec[ AMP_Rule_Spec::TAG_SPEC ], + ]; } } // If no valid rule_specs exist, then remove this node and return. if ( empty( $rule_spec_list_to_validate ) ) { - $this->remove_node( $node ); + if ( 1 === count( $invalid_rule_spec_list ) ) { + // If there was only one tag spec candidate that failed, use its error code for removing the node, + // since it's we know it is the specific reason for why the node had to be removed. + // This is the normal case. + $this->remove_invalid_child( $node, [ 'code' => $invalid_rule_spec_list[0]['error'] ] ); // @todo Need to pass tag_spec. + } else { + $this->remove_node( $node ); + } return null; } @@ -774,32 +795,36 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { * * @param DOMElement $node The node to validate. * @param array $tag_spec The specification. - * @return boolean $valid Whether the node's placement is valid. + * @return true|string True if node is valid for spec, or error code if otherwise. */ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) && ! $this->has_parent( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) ) { - return false; + return self::WRONG_PARENT_TAG; // @todo Pass back the expected parent tag name. } - // Extension scripts must be in the head. + // Extension scripts must be in the head. Note this currently never fails because all AMP scripts are moved to the head before sanitization. if ( isset( $tag_spec['extension_spec'] ) && ! $this->has_parent( $node, 'head' ) ) { - return false; + return self::WRONG_PARENT_TAG; } if ( ! empty( $tag_spec[ AMP_Rule_Spec::DISALLOWED_ANCESTOR ] ) ) { foreach ( $tag_spec[ AMP_Rule_Spec::DISALLOWED_ANCESTOR ] as $disallowed_ancestor_node_name ) { if ( $this->has_ancestor( $node, $disallowed_ancestor_node_name ) ) { - return false; + return self::DISALLOWED_TAG_ANCESTOR; // @todo Need to pass back the ancestor that is a problem. } } } if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) && ! $this->has_ancestor( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) ) { - return false; + return self::MANDATORY_TAG_ANCESTOR; // @todo Need to pass back the missing mandatory ancestor. + } + + if ( empty( $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ) ) { + return true; } - return ! ( ! empty( $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ) && ! $this->check_valid_children( $node, $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ) ); + return $this->check_valid_children( $node, $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ); } /** @@ -1812,7 +1837,7 @@ private function remove_disallowed_descendants( DOMElement $node, $allowed_desce * @type int $mandatory_num_child_tags Mandatory number of child tags. * @type int $mandatory_min_num_child_tags Mandatory minimum number of child tags. * } - * @return bool Whether the element satisfies the requirements, or else it should be removed. + * @return true|string True if the element satisfies the requirements, or error code if it should be removed. @todo Return validation error array instead? */ private function check_valid_children( DOMElement $node, $child_tags ) { $child_elements = []; @@ -1825,26 +1850,34 @@ private function check_valid_children( DOMElement $node, $child_tags ) { // If the first element is not of the required type, invalidate the entire element. if ( isset( $child_tags['first_child_tag_name_oneof'] ) && ! empty( $child_elements[0] ) && ! in_array( $child_elements[0]->nodeName, $child_tags['first_child_tag_name_oneof'], true ) ) { - return false; + return self::DISALLOWED_FIRST_CHILD_TAG; // @todo Would be better if the name of the child were included as context. } // Verify that all of the child are among the set of allowed elements. if ( isset( $child_tags['child_tag_name_oneof'] ) ) { foreach ( $child_elements as $child_element ) { if ( ! in_array( $child_element->nodeName, $child_tags['child_tag_name_oneof'], true ) ) { - return false; + return self::DISALLOWED_CHILD_TAG; // @todo Would be better if the name of the child were included as context. } } } // If there aren't the exact number of elements, then mark this $node as being invalid. if ( isset( $child_tags['mandatory_num_child_tags'] ) ) { - return count( $child_elements ) === $child_tags['mandatory_num_child_tags']; + if ( count( $child_elements ) === $child_tags['mandatory_num_child_tags'] ) { + return true; + } else { + return self::INCORRECT_NUM_CHILD_TAGS; + } } // If there aren't enough elements, then mark this $node as being invalid. if ( isset( $child_tags['mandatory_min_num_child_tags'] ) ) { - return count( $child_elements ) >= $child_tags['mandatory_min_num_child_tags']; + if ( count( $child_elements ) >= $child_tags['mandatory_min_num_child_tags'] ) { + return true; + } else { + return self::INCORRECT_MIN_NUM_CHILD_TAGS; + } } return true; diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 90a5206b922..93ec6c41ebc 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -69,7 +69,7 @@ public function get_body_data() { 'not allowednot ok', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_FIRST_CHILD_TAG ], ], 'amp-animation' => [ @@ -88,7 +88,7 @@ public function get_body_data() { 'bad--and not great: +1 (23) 456-789more badnot great', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_CHILD_TAG ], ], 'amp-call-tracking_blacklisted_config' => [ @@ -904,11 +904,11 @@ static function() { [ 'amp-sidebar' ], [ [ - 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'code' => AMP_Tag_And_Attribute_Sanitizer::WRONG_PARENT_TAG, 'node_name' => 'amp-app-banner', ], [ - 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'code' => AMP_Tag_And_Attribute_Sanitizer::WRONG_PARENT_TAG, 'node_name' => 'amp-app-banner', ], ], @@ -990,7 +990,7 @@ static function() { '
All I have is this div, when all you want is a noscript tag.
', '
All I have is this div, when all you want is a noscript tag.
', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR ], ], 'amp-img_with_good_protocols' => [ @@ -1005,7 +1005,7 @@ static function() { '

Text

', '

Text

', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR ], ], 'disallowed_attributes' => [ @@ -1260,7 +1260,7 @@ static function() { '', [], [ - AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, // @todo This should actually be DISALLOWED_FIRST_CHILD_TAG. + AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_FIRST_CHILD_TAG, ], ], @@ -1415,7 +1415,7 @@ static function() { '
bad
', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_FIRST_CHILD_TAG ], ], 'amp-addthis-valid' => [ @@ -1626,14 +1626,14 @@ static function() { '', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::INCORRECT_MIN_NUM_CHILD_TAGS ], ], 'amp-image-slider-more-bad-children' => [ 'Not allowedforbidden
This apple is green
not allowed
This apple is red
not ok
', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_CHILD_TAG ], ], 'amp-fx-collection' => [ @@ -2341,7 +2341,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { [ 'node_name' => 'amp-story-grid-layer', 'parent_name' => 'body', - 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG, + 'code' => AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR, 'node_attributes' => [ 'class' => 'a-invalid' ], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, ], From 934bbd1b295adf03fabc9b6774ada45e900d2448 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 12:14:02 -0800 Subject: [PATCH 22/39] Add constants for normalized error codes used in style sanitizer --- .../class-amp-core-theme-sanitizer.php | 2 +- .../sanitizers/class-amp-style-sanitizer.php | 157 ++++++++++-------- .../class-amp-validation-error-taxonomy.php | 28 ++-- .../class-amp-validation-manager.php | 2 +- tests/php/test-amp-style-sanitizer.php | 34 ++-- ...st-class-amp-validation-error-taxonomy.php | 11 +- .../test-class-amp-validation-manager.php | 2 +- 7 files changed, 121 insertions(+), 115 deletions(-) diff --git a/includes/sanitizers/class-amp-core-theme-sanitizer.php b/includes/sanitizers/class-amp-core-theme-sanitizer.php index 166a275ca43..3642f8122f1 100644 --- a/includes/sanitizers/class-amp-core-theme-sanitizer.php +++ b/includes/sanitizers/class-amp-core-theme-sanitizer.php @@ -222,7 +222,7 @@ public static function get_supported_themes() { public static function get_acceptable_errors( $template ) { if ( isset( self::$theme_features[ $template ] ) ) { return [ - 'illegal_css_at_rule' => [ + AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE => [ [ 'at_rule' => 'viewport', ], diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 66c1644fa13..80a2e13345f 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -26,12 +26,23 @@ */ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { - /** - * Error code for illegal at-rule. - * - * @var string - */ - const ILLEGAL_AT_RULE_ERROR_CODE = 'illegal_css_at_rule'; // @todo CSS_SYNTAX_INVALID_AT_RULE + const CSS_SYNTAX_INVALID_AT_RULE = 'CSS_SYNTAX_INVALID_AT_RULE'; + const CSS_SYNTAX_INVALID_DECLARATION = 'CSS_SYNTAX_INVALID_DECLARATION'; + const CSS_SYNTAX_INVALID_PROPERTY = 'CSS_SYNTAX_INVALID_PROPERTY'; + const CSS_SYNTAX_INVALID_PROPERTY_NOLIST = 'CSS_SYNTAX_INVALID_PROPERTY_NOLIST'; + const CSS_SYNTAX_INVALID_IMPORTANT = 'CSS_SYNTAX_INVALID_IMPORTANT'; + const CSS_SYNTAX_PARSE_ERROR = 'CSS_SYNTAX_PARSE_ERROR'; + const STYLESHEET_TOO_LONG = 'STYLESHEET_TOO_LONG'; + const STYLESHEET_INVALID_FILE_URL = 'STYLESHEET_INVALID_FILE_URL'; + const STYLESHEET_INVALID_FILE_PATH = 'STYLESHEET_INVALID_FILE_PATH'; + + // These are internal to the sanitizer and not exposed as validation error codes. + const STYLESHEET_DISALLOWED_FILE_EXT = 'STYLESHEET_DISALLOWED_FILE_EXT'; + const STYLESHEET_EXTERNAL_FILE_URL = 'STYLESHEET_EXTERNAL_FILE_URL'; + const STYLESHEET_FILE_PATH_NOT_FOUND = 'STYLESHEET_FILE_PATH_NOT_FOUND'; + const STYLESHEET_FILE_PATH_NOT_ALLOWED = 'STYLESHEET_FILE_PATH_NOT_ALLOWED'; + const STYLESHEET_URL_SYNTAX_ERROR = 'STYLESHEET_URL_SYNTAX_ERROR'; + const STYLESHEET_INVALID_RELATIVE_PATH = 'STYLESHEET_INVALID_RELATIVE_PATH'; /** * Inline style selector's specificity multiplier, i.e. used to generate the number of ':not(#_)' placeholders. @@ -325,14 +336,15 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { */ public static function get_css_parser_validation_error_codes() { return [ - 'css_parse_error', - 'excessive_css', // @todo STYLESHEET_TOO_LONG? But it's not technically the right error message? - self::ILLEGAL_AT_RULE_ERROR_CODE, - 'illegal_css_important', - 'illegal_css_property', // @todo CSS_SYNTAX_INVALID_PROPERTY_NOLIST - 'unrecognized_css', - 'disallowed_file_extension', - 'file_path_not_found', + self::CSS_SYNTAX_INVALID_AT_RULE, + self::CSS_SYNTAX_INVALID_DECLARATION, + self::CSS_SYNTAX_INVALID_IMPORTANT, + self::CSS_SYNTAX_INVALID_PROPERTY, + self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST, + self::CSS_SYNTAX_PARSE_ERROR, + self::STYLESHEET_INVALID_FILE_PATH, + self::STYLESHEET_INVALID_FILE_URL, + self::STYLESHEET_TOO_LONG, ]; } @@ -884,7 +896,7 @@ public function sanitize() { * * As with hooks, lower priorities mean they should be included first. * The higher the priority value, the more likely it will be that the - * stylesheet will be among those excluded due to 'excessive_css' when + * stylesheet will be among those excluded due to STYLESHEET_TOO_LONG when * concatenated CSS reaches 50KB. * * @todo This will eventually need to be abstracted to not be CMS-specific, allowing for the prioritization scheme to be defined by configuration. @@ -1008,8 +1020,7 @@ private function unrelativize_path( $path ) { } while ( 0 !== $count ); if ( preg_match( '#(^|/)\.+/#', $path ) ) { - /* translators: %s is the path with the remaining relative segments. */ - return new WP_Error( 'remaining_relativity', sprintf( __( 'There are remaining relative path segments: %s', 'amp' ), $path ) ); + return new WP_Error( self::STYLESHEET_INVALID_RELATIVE_PATH ); } return $path; @@ -1058,7 +1069,7 @@ private function reconstruct_url( $parsed_url ) { */ public function get_validated_url_file_path( $url, $allowed_extensions = [] ) { if ( ! is_string( $url ) ) { - return new WP_Error( 'url_not_string' ); + return new WP_Error( self::STYLESHEET_URL_SYNTAX_ERROR ); } $needs_base_url = ( @@ -1072,12 +1083,10 @@ public function get_validated_url_file_path( $url, $allowed_extensions = [] ) { $parsed_url = wp_parse_url( $url ); if ( empty( $parsed_url['host'] ) ) { - /* translators: %s is the original URL */ - return new WP_Error( 'no_url_host', sprintf( __( 'URL is missing host: %s', 'amp' ), $url ) ); + return new WP_Error( self::STYLESHEET_URL_SYNTAX_ERROR ); } if ( empty( $parsed_url['path'] ) ) { - /* translators: %s is the original URL */ - return new WP_Error( 'no_url_path', sprintf( __( 'URL is missing path: %s', 'amp' ), $url ) ); + return new WP_Error( self::STYLESHEET_URL_SYNTAX_ERROR ); } $path = $this->unrelativize_path( $parsed_url['path'] ); @@ -1109,13 +1118,13 @@ public function get_validated_url_file_path( $url, $allowed_extensions = [] ) { $pattern = sprintf( '/\.(%s)$/i', implode( '|', $allowed_extensions ) ); if ( ! preg_match( $pattern, $url ) ) { /* translators: %s: the file URL. */ - return new WP_Error( 'disallowed_file_extension', sprintf( __( 'File does not have an allowed file extension for filesystem access (%s).', 'amp' ), $url ) ); + return new WP_Error( self::STYLESHEET_DISALLOWED_FILE_EXT ); } } if ( ! in_array( $parsed_url['host'], $allowed_hosts, true ) ) { /* translators: %s: the file URL */ - return new WP_Error( 'external_file_url', sprintf( __( 'URL is located on an external domain: %s.', 'amp' ), $parsed_url['host'] ) ); + return new WP_Error( self::STYLESHEET_EXTERNAL_FILE_URL ); } $base_path = null; @@ -1137,12 +1146,10 @@ public function get_validated_url_file_path( $url, $allowed_extensions = [] ) { } if ( ! $file_path || false !== strpos( $file_path, '../' ) || false !== strpos( $file_path, '..\\' ) ) { - /* translators: %s: the file URL. */ - return new WP_Error( 'file_path_not_allowed', sprintf( __( 'Disallowed URL filesystem path for %s.', 'amp' ), $url ) ); + return new WP_Error( self::STYLESHEET_FILE_PATH_NOT_ALLOWED ); } if ( ! file_exists( $base_path . $file_path ) ) { - /* translators: %s: the file URL. */ - return new WP_Error( 'file_path_not_found', sprintf( __( 'Unable to locate filesystem path for %s.', 'amp' ), $url ) ); + return new WP_Error( self::STYLESHEET_FILE_PATH_NOT_FOUND ); } return $base_path . $file_path; @@ -1261,6 +1268,17 @@ private function process_link_element( DOMElement $element ) { $css_file_path = $this->get_validated_url_file_path( $href, [ 'css', 'less', 'scss', 'sass' ] ); if ( ! is_wp_error( $css_file_path ) ) { $stylesheet = file_get_contents( $css_file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. + if ( false === $stylesheet ) { + $this->remove_invalid_child( + $element, + [ + // @todo Also include details about the error. + 'code' => self::STYLESHEET_INVALID_FILE_PATH, + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + ] + ); + return; + } } else { // Fall back to doing an HTTP request for the stylesheet is not accessible directly from the filesystem. $contents = $this->fetch_external_stylesheet( $normalized_url ); @@ -1270,26 +1288,16 @@ private function process_link_element( DOMElement $element ) { $this->remove_invalid_child( $element, [ - 'code' => $contents->get_error_code(), - 'message' => $contents->get_error_message(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + // @todo Also include details about the error. + 'code' => self::STYLESHEET_INVALID_FILE_URL, + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'url' => $normalized_url, ] ); return; } } - if ( false === $stylesheet ) { - $this->remove_invalid_child( - $element, - [ - 'code' => 'stylesheet_file_missing', - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - ] - ); - return; - } - // Honor the link's media attribute. $media = $element->getAttribute( 'media' ); if ( $media && 'all' !== $media ) { @@ -1505,14 +1513,14 @@ private function parse_import_stylesheet( Import $item, CSSList $css_list, $opti $css_file_path = $this->get_validated_url_file_path( $import_stylesheet_url, [ 'css', 'less', 'scss', 'sass' ] ); - if ( is_wp_error( $css_file_path ) && ( 'disallowed_file_extension' === $css_file_path->get_error_code() || 'external_file_url' === $css_file_path->get_error_code() ) ) { + if ( is_wp_error( $css_file_path ) && ( self::STYLESHEET_DISALLOWED_FILE_EXT === $css_file_path->get_error_code() || self::STYLESHEET_EXTERNAL_FILE_URL === $css_file_path->get_error_code() ) ) { $contents = $this->fetch_external_stylesheet( $import_stylesheet_url ); if ( is_wp_error( $contents ) ) { $error = [ - 'code' => $contents->get_error_code(), - 'message' => $contents->get_error_message(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - 'url' => $import_stylesheet_url, + // @todo Also include details about the error. + 'code' => self::STYLESHEET_INVALID_FILE_URL, + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'url' => $import_stylesheet_url, ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -1525,10 +1533,10 @@ private function parse_import_stylesheet( Import $item, CSSList $css_list, $opti $stylesheet = $contents; } elseif ( is_wp_error( $css_file_path ) ) { $error = [ - 'code' => $css_file_path->get_error_code(), - 'message' => $css_file_path->get_error_message(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - 'url' => $import_stylesheet_url, + // @todo Also include details about the error. + 'code' => self::STYLESHEET_INVALID_FILE_PATH, + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'url' => $import_stylesheet_url, ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -1618,7 +1626,7 @@ static function ( $value ) { ); } catch ( Exception $exception ) { $error = [ - 'code' => 'css_parse_error', + 'code' => self::CSS_SYNTAX_PARSE_ERROR, 'message' => $exception->getMessage(), 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; @@ -1917,7 +1925,7 @@ private function process_css_list( CSSList $css_list, $options ) { } elseif ( $css_item instanceof AtRuleBlockList ) { if ( ! in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) { $error = [ - 'code' => self::ILLEGAL_AT_RULE_ERROR_CODE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; @@ -1938,7 +1946,7 @@ private function process_css_list( CSSList $css_list, $options ) { } elseif ( $css_item instanceof AtRuleSet ) { if ( ! in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) { $error = [ - 'code' => self::ILLEGAL_AT_RULE_ERROR_CODE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; @@ -1955,7 +1963,7 @@ private function process_css_list( CSSList $css_list, $options ) { } elseif ( $css_item instanceof KeyFrame ) { if ( ! in_array( 'keyframes', $options['allowed_at_rules'], true ) ) { $error = [ - 'code' => self::ILLEGAL_AT_RULE_ERROR_CODE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; @@ -1980,7 +1988,7 @@ private function process_css_list( CSSList $css_list, $options ) { $sanitized = true; } else { $error = [ - 'code' => self::ILLEGAL_AT_RULE_ERROR_CODE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, 'at_rule' => $css_item->atRuleName(), 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; @@ -1989,7 +1997,7 @@ private function process_css_list( CSSList $css_list, $options ) { } } else { $error = [ - 'code' => 'unrecognized_css', + 'code' => self::CSS_SYNTAX_INVALID_DECLARATION, 'item' => get_class( $css_item ), 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; @@ -2077,10 +2085,11 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() ); if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) { $error = [ - 'code' => 'illegal_css_property', - 'property_name' => $property->getRule(), - 'property_value' => $property->getValue(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_PROPERTY, + 'property_name' => $property->getRule(), + 'property_value' => $property->getValue(), + 'allowed_properties' => $options['property_whitelist'], + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2094,10 +2103,11 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l $properties = $ruleset->getRules( $illegal_property_name ); foreach ( $properties as $property ) { $error = [ - 'code' => 'illegal_css_property', - 'property_name' => $property->getRule(), - 'property_value' => (string) $property->getValue(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST, + 'property_name' => $property->getRule(), + 'property_value' => (string) $property->getValue(), + 'allowed_properties' => $options['property_whitelist'], + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2306,7 +2316,7 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { foreach ( $css_list->getContents() as $rules ) { if ( ! ( $rules instanceof DeclarationBlock ) ) { $error = [ - 'code' => 'unrecognized_css', + 'code' => self::CSS_SYNTAX_INVALID_DECLARATION, 'item' => get_class( $rules ), 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; @@ -2328,10 +2338,11 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() ); if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) { $error = [ - 'code' => 'illegal_css_property', - 'property_name' => $property->getRule(), - 'property_value' => (string) $property->getValue(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_PROPERTY, + 'property_name' => $property->getRule(), + 'property_value' => (string) $property->getValue(), + 'allowed_properties' => $options['property_whitelist'], + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2376,7 +2387,7 @@ private function transform_important_qualifiers( RuleSet $ruleset, CSSList $css_ $ruleset->removeRule( $property->getRule() ); } else { $error = [ - 'code' => 'illegal_css_important', + 'code' => self::CSS_SYNTAX_INVALID_IMPORTANT, 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'property_name' => $property->getRule(), 'property_value' => $property->getValue(), @@ -2719,7 +2730,7 @@ private function finalize_styles() { if ( ! $body ) { $this->should_sanitize_validation_error( [ - 'code' => 'missing_body_element', + 'code' => 'missing_body_element', // @todo Add the body. 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ] ); @@ -3135,7 +3146,7 @@ function( $a, $b ) { // Report validation error if size is now too big. if ( $current_concatenated_size + $this->pending_stylesheets[ $i ]['size'] > $max_bytes ) { $validation_error = [ - 'code' => 'excessive_css', + 'code' => self::STYLESHEET_TOO_LONG, 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, ]; if ( isset( $this->pending_stylesheets[ $i ]['sources'] ) ) { diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index bcb5d4c860c..fa5de9464c1 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -2893,39 +2893,41 @@ public static function get_error_title_from_code( $validation_error ) { esc_html( $validation_error['node_name'] ), '?' ); - case 'file_path_not_allowed': - return esc_html__( 'Stylesheet file path not allowed', 'amp' ); - case 'excessive_css': + case AMP_Style_Sanitizer::STYLESHEET_TOO_LONG: return esc_html__( 'Excessive CSS', 'amp' ); - case 'illegal_css_at_rule': + case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE: return sprintf( '%s: @%s', esc_html__( 'Illegal CSS at-rule', 'amp' ), esc_html( $validation_error['at_rule'] ) ); - case 'disallowed_file_extension': - return esc_html__( 'Disallowed CSS file extension', 'amp' ); - case 'duplicate_element': + case AMP_Tag_And_Attribute_Sanitizer::DUPLICATE_UNIQUE_TAG: return sprintf( '%s: <%s>', esc_html__( 'Duplicate element', 'amp' ), esc_html( $validation_error['node_name'] ) ); - case 'unrecognized_css': + case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_DECLARATION: return esc_html__( 'Unrecognized CSS', 'amp' ); - case 'css_parse_error': + case AMP_Style_Sanitizer::CSS_SYNTAX_PARSE_ERROR: return esc_html__( 'CSS parse error', 'amp' ); - case 'stylesheet_file_missing': + case AMP_Style_Sanitizer::STYLESHEET_INVALID_FILE_URL: + case AMP_Style_Sanitizer::STYLESHEET_INVALID_FILE_PATH: return esc_html__( 'Missing stylesheet file', 'amp' ); - case 'illegal_css_property': + case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY: + case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY_NOLIST: $title = esc_html__( 'Illegal CSS property', 'amp' ); if ( isset( $validation_error['property_name'] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['property_name'] ) ); } return $title; - case 'illegal_cdata': + case AMP_Tag_And_Attribute_Sanitizer::CDATA_TOO_LONG: + case AMP_Tag_And_Attribute_Sanitizer::MANDATORY_CDATA_MISSING_OR_INCORRECT: + case AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS: + case AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CSS_IMPORTANT: + case AMP_Tag_And_Attribute_Sanitizer::CDATA_VIOLATES_BLACKLIST: return esc_html__( 'Illegal text content', 'amp' ); - case 'illegal_css_important': + case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_IMPORTANT: $title = esc_html__( 'Illegal CSS !important property', 'amp' ); if ( isset( $validation_error['property_name'] ) ) { $title .= sprintf( ': %s', esc_html( $validation_error['property_name'] ) ); diff --git a/includes/validation/class-amp-validation-manager.php b/includes/validation/class-amp-validation-manager.php index 25f9aa93eb5..e6883a6321b 100644 --- a/includes/validation/class-amp-validation-manager.php +++ b/includes/validation/class-amp-validation-manager.php @@ -310,7 +310,7 @@ public static function is_sanitization_auto_accepted( $error = null ) { if ( $error && amp_is_canonical() ) { // Excessive CSS on AMP-first sites must not be removed by default since removing CSS can severely break a site. - $accepted = 'excessive_css' !== $error['code']; + $accepted = AMP_Style_Sanitizer::STYLESHEET_TOO_LONG !== $error['code']; } else { $accepted = true; } diff --git a/tests/php/test-amp-style-sanitizer.php b/tests/php/test-amp-style-sanitizer.php index f95bc9c8ac7..0a938dc03ad 100644 --- a/tests/php/test-amp-style-sanitizer.php +++ b/tests/php/test-amp-style-sanitizer.php @@ -143,7 +143,7 @@ public function get_body_style_attribute_data() { 'button{font-weight:bold}', '@media screen{button{font-weight:bold}}', ], - [ 'illegal_css_property', 'illegal_css_property', 'illegal_css_property', 'illegal_css_property' ], + array_fill( 0, 4, AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY_NOLIST ), ], 'illegal_at_rule_in_style_attribute' => [ @@ -161,7 +161,7 @@ public function get_body_style_attribute_data() { [ '@page{margin:1cm}body{color:black}', ], - [ 'illegal_css_at_rule', 'illegal_css_at_rule', 'illegal_css_at_rule' ], + [ AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE, AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE, AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE ], ], 'allowed_at_rules_retained' => [ @@ -1278,7 +1278,7 @@ public function test_large_custom_css_and_rule_removal() { ); $this->assertEquals( - [ 'excessive_css' ], + [ AMP_Style_Sanitizer::STYLESHEET_TOO_LONG ], $error_codes ); } @@ -1390,7 +1390,7 @@ public function test_css_manifest() { ], true ); - $this->assertEquals( [ 'excessive_css' ], $error_codes ); + $this->assertEquals( [ AMP_Style_Sanitizer::STYLESHEET_TOO_LONG ], $error_codes ); $this->assertInstanceOf( 'DOMComment', $style->previousSibling, 'Expected manifest comment to be present because excessive.' ); $comment = $style->previousSibling; $this->assertContains( 'The style[amp-custom] element is populated with', $comment->nodeValue ); @@ -1458,7 +1458,7 @@ public function get_http_stylesheets() { home_url( '/this.is.not.css' ), 'image/jpeg', 'JPEG...', - [ 'no_css_content_type' ], + [ AMP_Style_Sanitizer::STYLESHEET_INVALID_FILE_URL ], ], ]; } @@ -1559,7 +1559,7 @@ public function get_keyframe_data() { 'style_amp_keyframes_max_overflow' => [ '', '', - [ 'excessive_css' ], + [ AMP_Style_Sanitizer::STYLESHEET_TOO_LONG ], ], 'style_amp_keyframes_last_child' => [ @@ -1571,13 +1571,13 @@ public function get_keyframe_data() { 'blacklisted_and_whitelisted_keyframe_properties' => [ '', '', - [ 'illegal_css_property', 'illegal_css_property', 'illegal_css_property' ], + array_fill( 0, 3, AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY ), ], 'style_amp_keyframes_with_disallowed_rules' => [ '', '', - [ 'unrecognized_css', 'illegal_css_important', 'illegal_css_at_rule' ], + [ AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_DECLARATION, AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_IMPORTANT, AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE ], ], ]; } @@ -1632,12 +1632,12 @@ public function get_stylesheet_urls() { 'url_without_path' => [ 'https://example.com', null, - 'no_url_path', + AMP_Style_Sanitizer::STYLESHEET_URL_SYNTAX_ERROR, ], 'url_not_string' => [ false, null, - 'url_not_string', + AMP_Style_Sanitizer::STYLESHEET_URL_SYNTAX_ERROR, ], 'theme_stylesheet_without_host' => [ '/wp-content/themes/twentyseventeen/style.css', @@ -1654,7 +1654,7 @@ public function get_stylesheet_urls() { 'theme_stylesheet_with_trailing_dot' => [ $theme->get_stylesheet_directory_uri() . '/foo./bar.css', null, - 'file_path_not_found', + AMP_Style_Sanitizer::STYLESHEET_FILE_PATH_NOT_FOUND, ], 'dashicons_without_host' => [ '/wp-includes/css/dashicons.css', @@ -1687,32 +1687,32 @@ public function get_stylesheet_urls() { 'amp_disallowed_file_extension' => [ content_url( 'themes/twentyseventeen/index.php' ), null, - 'disallowed_file_extension', + AMP_Style_Sanitizer::STYLESHEET_DISALLOWED_FILE_EXT, ], 'amp_file_path_not_found' => [ content_url( 'themes/twentyseventeen/404.css' ), null, - 'file_path_not_found', + AMP_Style_Sanitizer::STYLESHEET_FILE_PATH_NOT_FOUND, ], 'amp_file_path_illegal_linux' => [ content_url( '../../../../../../../../../../../../../../../bad.css' ), null, - 'remaining_relativity', + AMP_Style_Sanitizer::STYLESHEET_INVALID_RELATIVE_PATH, ], 'amp_file_path_illegal_windows' => [ content_url( '..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\bad.css' ), null, - 'file_path_not_allowed', + AMP_Style_Sanitizer::STYLESHEET_FILE_PATH_NOT_ALLOWED, ], 'amp_file_path_illegal_location' => [ site_url( 'outside/root.css' ), null, - 'file_path_not_allowed', + AMP_Style_Sanitizer::STYLESHEET_FILE_PATH_NOT_ALLOWED, ], 'amp_external_file' => [ '//s.w.org/wp-includes/css/dashicons.css', false, - 'external_file_url', + AMP_Style_Sanitizer::STYLESHEET_EXTERNAL_FILE_URL, ], ]; } diff --git a/tests/php/validation/test-class-amp-validation-error-taxonomy.php b/tests/php/validation/test-class-amp-validation-error-taxonomy.php index bd7ca3d012b..e2b3396ab4a 100644 --- a/tests/php/validation/test-class-amp-validation-error-taxonomy.php +++ b/tests/php/validation/test-class-amp-validation-error-taxonomy.php @@ -21,13 +21,6 @@ class Test_AMP_Validation_Error_Taxonomy extends WP_UnitTestCase { */ const TESTED_CLASS = 'AMP_Validation_Error_Taxonomy'; - /** - * A mock acceptable error code. - * - * @var string - */ - const MOCK_ACCEPTABLE_ERROR = 'illegal_css_at_rule'; - /** * Resets the state after each test method. */ @@ -385,7 +378,7 @@ public function test_accept_validation_errors() { $this->assertNull( apply_filters( 'amp_validation_error_sanitized', null, $error ) ); remove_all_filters( 'amp_validation_error_sanitized' ); - AMP_Validation_Error_Taxonomy::accept_validation_errors( [ self::MOCK_ACCEPTABLE_ERROR => true ] ); + AMP_Validation_Error_Taxonomy::accept_validation_errors( [ AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE => true ] ); $this->assertTrue( apply_filters( 'amp_validation_error_sanitized', null, $error ) ); remove_all_filters( 'amp_validation_error_sanitized' ); @@ -1347,7 +1340,7 @@ static function() { public function get_mock_error() { return [ 'at_rule' => '-ms-viewport', - 'code' => self::MOCK_ACCEPTABLE_ERROR, + 'code' => AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_AT_RULE, 'node_attributes' => [ 'href' => 'https://example.com', 'id' => 'twentysixteen-style-css', diff --git a/tests/php/validation/test-class-amp-validation-manager.php b/tests/php/validation/test-class-amp-validation-manager.php index 8ead94c4750..c7b1f057818 100644 --- a/tests/php/validation/test-class-amp-validation-manager.php +++ b/tests/php/validation/test-class-amp-validation-manager.php @@ -240,7 +240,7 @@ public function test_is_sanitization_auto_accepted() { $excessive_css_error = [ 'node_name' => 'style', 'type' => 'css', - 'code' => 'excessive_css', + 'code' => AMP_Style_Sanitizer::STYLESHEET_TOO_LONG, ]; remove_theme_support( AMP_Theme_Support::SLUG ); From 5da07dc724e6a497c1bf6ff8c1fe22ca6a9ab9bf Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 12:16:52 -0800 Subject: [PATCH 23/39] Ensure body present instead of raising error --- includes/sanitizers/class-amp-style-sanitizer.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 80a2e13345f..4bfc50970f1 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -2728,12 +2728,8 @@ private function finalize_styles() { if ( $stylesheet_groups['keyframes']['included_count'] > 0 ) { $body = $this->dom->getElementsByTagName( 'body' )->item( 0 ); if ( ! $body ) { - $this->should_sanitize_validation_error( - [ - 'code' => 'missing_body_element', // @todo Add the body. - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - ] - ); + $body = $this->dom->createElement( 'body' ); + $this->dom->documentElement->appendChild( $body ); } else { $css = $stylesheet_groups['keyframes']['import_front_matter']; From b0dfd17fda174bf61762866e77aef70cfa75c5dd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 14:42:30 -0800 Subject: [PATCH 24/39] Improve error codes used in media converters; add error context data --- .../sanitizers/class-amp-audio-sanitizer.php | 9 +- .../sanitizers/class-amp-iframe-sanitizer.php | 9 +- .../sanitizers/class-amp-img-sanitizer.php | 9 +- .../class-amp-tag-and-attribute-sanitizer.php | 103 +++++++++++++----- .../sanitizers/class-amp-video-sanitizer.php | 9 +- tests/php/test-amp-img-sanitizer.php | 6 +- .../php/test-tag-and-attribute-sanitizer.php | 12 +- 7 files changed, 117 insertions(+), 40 deletions(-) diff --git a/includes/sanitizers/class-amp-audio-sanitizer.php b/includes/sanitizers/class-amp-audio-sanitizer.php index 48d1a9da1c8..6a15c17e86e 100644 --- a/includes/sanitizers/class-amp-audio-sanitizer.php +++ b/includes/sanitizers/class-amp-audio-sanitizer.php @@ -147,7 +147,14 @@ public function sanitize() { * See: https://github.com/ampproject/amphtml/issues/2261 */ if ( empty( $sources ) ) { - $this->remove_invalid_child( $node ); + $this->remove_invalid_child( + $node, + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'attributes' => [ 'src' ], + 'spec_name' => 'amp-audio', + ] + ); } else { $node->parentNode->replaceChild( $new_node, $node ); diff --git a/includes/sanitizers/class-amp-iframe-sanitizer.php b/includes/sanitizers/class-amp-iframe-sanitizer.php index 786989f0386..a2a3805631a 100644 --- a/includes/sanitizers/class-amp-iframe-sanitizer.php +++ b/includes/sanitizers/class-amp-iframe-sanitizer.php @@ -110,7 +110,14 @@ public function sanitize() { * @see: https://github.com/ampproject/amphtml/issues/2261 */ if ( empty( $normalized_attributes['src'] ) ) { - $this->remove_invalid_child( $node ); + $this->remove_invalid_child( + $node, + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'attributes' => [ 'src' ], + 'spec_name' => 'amp-iframe', + ] + ); continue; } diff --git a/includes/sanitizers/class-amp-img-sanitizer.php b/includes/sanitizers/class-amp-img-sanitizer.php index af29cbfdf7c..10a585be9ed 100644 --- a/includes/sanitizers/class-amp-img-sanitizer.php +++ b/includes/sanitizers/class-amp-img-sanitizer.php @@ -107,7 +107,14 @@ public function sanitize() { } if ( ! $node->hasAttribute( 'src' ) || '' === trim( $node->getAttribute( 'src' ) ) ) { - $this->remove_invalid_child( $node ); + $this->remove_invalid_child( + $node, + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'attributes' => [ 'src' ], + 'spec_name' => 'amp-img', + ] + ); continue; } diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index f63c53fbab8..7a1f6f6d7a2 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -466,6 +466,7 @@ private function process_node( DOMElement $node ) { } else { $invalid_rule_spec_list[] = [ 'error' => $validity, + // @todo The tag_spec should be included always, whenever a validation error is raised. Or rather, just the spec_name. The spec_url can be looked up. 'tag_spec' => $rule_spec[ AMP_Rule_Spec::TAG_SPEC ], ]; } @@ -477,7 +478,15 @@ private function process_node( DOMElement $node ) { // If there was only one tag spec candidate that failed, use its error code for removing the node, // since it's we know it is the specific reason for why the node had to be removed. // This is the normal case. - $this->remove_invalid_child( $node, [ 'code' => $invalid_rule_spec_list[0]['error'] ] ); // @todo Need to pass tag_spec. + $this->remove_invalid_child( + $node, + array_merge( + $invalid_rule_spec_list[0]['error'], + [ + 'tag_spec' => $invalid_rule_spec_list[0]['tag_spec'], + ] + ) + ); } else { $this->remove_node( $node ); } @@ -563,10 +572,8 @@ private function process_node( DOMElement $node ) { if ( ! empty( $cdata ) && $node instanceof DOMElement ) { $validity = $this->validate_cdata_for_node( $node, $cdata ); if ( true !== $validity ) { - $this->remove_invalid_child( - $node, - [ 'code' => $validity ] - ); + // @todo Need to pass tag_spec. + $this->remove_invalid_child( $node, $validity ); return null; } } @@ -594,6 +601,7 @@ private function process_node( DOMElement $node ) { if ( ! empty( $this->visited_unique_tag_specs[ $node->nodeName ][ $tag_spec_key ] ) ) { $removed = $this->remove_invalid_child( $node, + // @todo Need to pass tag_spec. [ 'code' => self::DUPLICATE_UNIQUE_TAG ] ); } @@ -657,13 +665,13 @@ private function process_node( DOMElement $node ) { } } - // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. // After attributes have been sanitized (and potentially removed), if mandatory attribute(s) are missing, remove the element. $missing_mandatory_attributes = $this->get_missing_mandatory_attributes( $merged_attr_spec_list, $node ); if ( ! empty( $missing_mandatory_attributes ) ) { $this->remove_invalid_child( $node, [ + // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. 'code' => self::ATTR_REQUIRED_BUT_MISSING, 'attributes' => $missing_mandatory_attributes, ] @@ -743,7 +751,7 @@ private function get_missing_mandatory_attributes( $attr_spec, DOMElement $node * * @param DOMElement $element Element. * @param array $cdata_spec CDATA. - * @return true|string True when valid or error code when invalid. + * @return true|array True when valid or error data when invalid. */ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { if ( @@ -753,7 +761,10 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { // This would mean that AMP was disabled to not break the styling. ! ( 'style' === $element->nodeName && $element->hasAttribute( 'amp-custom' ) ) ) { - return self::CDATA_TOO_LONG; + return [ + 'code' => self::CDATA_TOO_LONG, + 'max_bytes' => $cdata_spec['max_bytes'], + ]; } if ( isset( $cdata_spec['blacklisted_cdata_regex'] ) ) { if ( preg_match( '@' . $cdata_spec['blacklisted_cdata_regex']['regex'] . '@u', $element->textContent ) ) { @@ -761,21 +772,21 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { // There are only a few error messages, so map them to error codes. switch ( $cdata_spec['blacklisted_cdata_regex']['error_message'] ) { case 'CSS !important': - return self::INVALID_CDATA_CSS_IMPORTANT; + return [ 'code' => self::INVALID_CDATA_CSS_IMPORTANT ]; case 'contents': - return self::INVALID_CDATA_CONTENTS; + return [ 'code' => self::INVALID_CDATA_CONTENTS ]; case 'html comments': - return self::INVALID_CDATA_HTML_COMMENTS; + return [ 'code' => self::INVALID_CDATA_HTML_COMMENTS ]; } } // Note: This fallback case is not currently reachable because all error messages are accounted for in the switch statement. - return self::CDATA_VIOLATES_BLACKLIST; + return [ 'code' => self::CDATA_VIOLATES_BLACKLIST ]; } } elseif ( isset( $cdata_spec['cdata_regex'] ) ) { $delimiter = false === strpos( $cdata_spec['cdata_regex'], '@' ) ? '@' : '#'; if ( ! preg_match( $delimiter . $cdata_spec['cdata_regex'] . $delimiter . 'u', $element->textContent ) ) { - return self::MANDATORY_CDATA_MISSING_OR_INCORRECT; + return [ 'code' => self::MANDATORY_CDATA_MISSING_OR_INCORRECT ]; } } return true; @@ -795,29 +806,40 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { * * @param DOMElement $node The node to validate. * @param array $tag_spec The specification. - * @return true|string True if node is valid for spec, or error code if otherwise. + * @return true|array True if node is valid for spec, or error data array if otherwise. */ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) && ! $this->has_parent( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) ) { - return self::WRONG_PARENT_TAG; // @todo Pass back the expected parent tag name. + return [ + 'code' => self::WRONG_PARENT_TAG, + 'mandatory_parent' => $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ], + ]; } // Extension scripts must be in the head. Note this currently never fails because all AMP scripts are moved to the head before sanitization. if ( isset( $tag_spec['extension_spec'] ) && ! $this->has_parent( $node, 'head' ) ) { - return self::WRONG_PARENT_TAG; + return [ + 'code' => self::WRONG_PARENT_TAG, + ]; } if ( ! empty( $tag_spec[ AMP_Rule_Spec::DISALLOWED_ANCESTOR ] ) ) { foreach ( $tag_spec[ AMP_Rule_Spec::DISALLOWED_ANCESTOR ] as $disallowed_ancestor_node_name ) { if ( $this->has_ancestor( $node, $disallowed_ancestor_node_name ) ) { - return self::DISALLOWED_TAG_ANCESTOR; // @todo Need to pass back the ancestor that is a problem. + return [ + 'code' => self::DISALLOWED_TAG_ANCESTOR, + 'disallowed_ancestor' => $disallowed_ancestor_node_name, + ]; } } } if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) && ! $this->has_ancestor( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) ) { - return self::MANDATORY_TAG_ANCESTOR; // @todo Need to pass back the missing mandatory ancestor. + return [ + 'code' => self::MANDATORY_TAG_ANCESTOR, + 'mandatory_ancestor' => $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ], + ]; } if ( empty( $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ) ) { @@ -1801,7 +1823,7 @@ private function parse_tag_and_attributes_from_spec_name( $spec_name ) { * Loop through node's descendants and remove the ones that are not whitelisted. * * @param DOMElement $node Node. - * @param array $allowed_descendants List of allowed descendant tags. + * @param string[] $allowed_descendants List of allowed descendant tags. */ private function remove_disallowed_descendants( DOMElement $node, $allowed_descendants ) { if ( ! $node->hasChildNodes() ) { @@ -1818,7 +1840,14 @@ private function remove_disallowed_descendants( DOMElement $node, $allowed_desce foreach ( $child_elements as $child_element ) { if ( ! in_array( $child_element->nodeName, $allowed_descendants, true ) ) { - $this->remove_invalid_child( $child_element, [ 'code' => self::DISALLOWED_DESCENDANT_TAG ] ); + $this->remove_invalid_child( + $child_element, + [ + // @todo Need to pass tag_spec. + 'code' => self::DISALLOWED_DESCENDANT_TAG, + 'allowed_descendants' => $allowed_descendants, + ] + ); } else { $this->remove_disallowed_descendants( $child_element, $allowed_descendants ); } @@ -1837,7 +1866,7 @@ private function remove_disallowed_descendants( DOMElement $node, $allowed_desce * @type int $mandatory_num_child_tags Mandatory number of child tags. * @type int $mandatory_min_num_child_tags Mandatory minimum number of child tags. * } - * @return true|string True if the element satisfies the requirements, or error code if it should be removed. @todo Return validation error array instead? + * @return true|array True if the element satisfies the requirements, or error data array if it should be removed. */ private function check_valid_children( DOMElement $node, $child_tags ) { $child_elements = []; @@ -1850,33 +1879,51 @@ private function check_valid_children( DOMElement $node, $child_tags ) { // If the first element is not of the required type, invalidate the entire element. if ( isset( $child_tags['first_child_tag_name_oneof'] ) && ! empty( $child_elements[0] ) && ! in_array( $child_elements[0]->nodeName, $child_tags['first_child_tag_name_oneof'], true ) ) { - return self::DISALLOWED_FIRST_CHILD_TAG; // @todo Would be better if the name of the child were included as context. + return [ + 'code' => self::DISALLOWED_FIRST_CHILD_TAG, + 'first_child_tag' => $child_elements[0]->nodeName, + 'first_child_tag_name_oneof' => $child_tags['first_child_tag_name_oneof'], + ]; } // Verify that all of the child are among the set of allowed elements. if ( isset( $child_tags['child_tag_name_oneof'] ) ) { foreach ( $child_elements as $child_element ) { if ( ! in_array( $child_element->nodeName, $child_tags['child_tag_name_oneof'], true ) ) { - return self::DISALLOWED_CHILD_TAG; // @todo Would be better if the name of the child were included as context. + return [ + 'code' => self::DISALLOWED_CHILD_TAG, + 'child_tag' => $child_element->nodeName, + 'child_tag_name_oneof' => $child_tags['child_tag_name_oneof'], + ]; } } } // If there aren't the exact number of elements, then mark this $node as being invalid. if ( isset( $child_tags['mandatory_num_child_tags'] ) ) { - if ( count( $child_elements ) === $child_tags['mandatory_num_child_tags'] ) { + $child_element_count = count( $child_elements ); + if ( $child_element_count === $child_tags['mandatory_num_child_tags'] ) { return true; } else { - return self::INCORRECT_NUM_CHILD_TAGS; + return [ + 'code' => self::INCORRECT_NUM_CHILD_TAGS, + 'children_count' => $child_element_count, + 'mandatory_num_child_tags' => $child_tags['mandatory_num_child_tags'], + ]; } } // If there aren't enough elements, then mark this $node as being invalid. if ( isset( $child_tags['mandatory_min_num_child_tags'] ) ) { - if ( count( $child_elements ) >= $child_tags['mandatory_min_num_child_tags'] ) { + $child_element_count = count( $child_elements ); + if ( $child_element_count >= $child_tags['mandatory_min_num_child_tags'] ) { return true; } else { - return self::INCORRECT_MIN_NUM_CHILD_TAGS; + return [ + 'code' => self::INCORRECT_MIN_NUM_CHILD_TAGS, + 'children_count' => $child_element_count, + 'mandatory_min_num_child_tags' => $child_tags['mandatory_min_num_child_tags'], + ]; } } @@ -1993,7 +2040,7 @@ private function remove_node( DOMElement $node ) { $this->remove_invalid_child( $node ); } - // @todo Does this parent removal even make sense anymore? + // @todo Does this parent removal even make sense anymore? Perhaps limit to

only. while ( $parent && ! $parent->hasChildNodes() && ! $parent->hasAttributes() && $this->root_element !== $parent ) { $node = $parent; $parent = $parent->parentNode; diff --git a/includes/sanitizers/class-amp-video-sanitizer.php b/includes/sanitizers/class-amp-video-sanitizer.php index 2719486ee5b..7fadfc8b201 100644 --- a/includes/sanitizers/class-amp-video-sanitizer.php +++ b/includes/sanitizers/class-amp-video-sanitizer.php @@ -161,7 +161,14 @@ public function sanitize() { * See: https://github.com/ampproject/amphtml/issues/2261 */ if ( empty( $sources ) ) { - $this->remove_invalid_child( $node ); + $this->remove_invalid_child( + $node, + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, + 'attributes' => [ 'src' ], + 'spec_name' => 'amp-video', + ] + ); } else { $node->parentNode->replaceChild( $new_node, $node ); diff --git a/tests/php/test-amp-img-sanitizer.php b/tests/php/test-amp-img-sanitizer.php index 9003c7fdb38..f124b17804d 100644 --- a/tests/php/test-amp-img-sanitizer.php +++ b/tests/php/test-amp-img-sanitizer.php @@ -73,14 +73,14 @@ public function get_data() { '

', '

', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING ], ], 'image_with_empty_src' => [ '

', '

', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING ], ], 'image_with_layout' => [ @@ -107,7 +107,7 @@ public function get_data() { '

', '

', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG ], + [ AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING ], ], 'image_with_empty_width_and_height' => [ diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 93ec6c41ebc..4293811edd0 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -2339,11 +2339,12 @@ public function get_data_for_replace_node_with_children_validation_errors() { '', [ [ - 'node_name' => 'amp-story-grid-layer', - 'parent_name' => 'body', - 'code' => AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR, - 'node_attributes' => [ 'class' => 'a-invalid' ], - 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, + 'node_name' => 'amp-story-grid-layer', + 'parent_name' => 'body', + 'code' => AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR, + 'node_attributes' => [ 'class' => 'a-invalid' ], + 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, + 'mandatory_ancestor' => 'amp-story-page', ], ], ], @@ -2468,6 +2469,7 @@ public function test_replace_node_with_children_validation_errors( $source_conte 'validation_error_callback' => function( $error, $context ) use ( &$expected_errors ) { $expected = array_shift( $expected_errors ); $tag = $expected['node_name']; + unset( $error['tag_spec'] ); // No need to check for this, although eventually we should assert it is present. $this->assertEquals( $expected, $error ); $this->assertInstanceOf( 'DOMElement', $context['node'] ); $this->assertEquals( $tag, $context['node']->tagName ); From 2c42a07941675d54087143ac1cc938ff2bb7b233 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 15:07:23 -0800 Subject: [PATCH 25/39] Include spec_name in validation error --- .../class-amp-tag-and-attribute-sanitizer.php | 83 ++++++++++++------- .../php/test-tag-and-attribute-sanitizer.php | 5 +- 2 files changed, 58 insertions(+), 30 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 7a1f6f6d7a2..9e96a82bc17 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -455,7 +455,7 @@ private function process_node( DOMElement $node ) { */ $rule_spec_list_to_validate = []; $rule_spec_list = []; - $invalid_rule_spec_list = []; + $validation_errors = []; if ( isset( $this->allowed_tags[ $node->nodeName ] ) ) { $rule_spec_list = $this->allowed_tags[ $node->nodeName ]; } @@ -464,28 +464,22 @@ private function process_node( DOMElement $node ) { if ( true === $validity ) { $rule_spec_list_to_validate[ $id ] = $this->get_rule_spec_list_to_validate( $node, $rule_spec ); } else { - $invalid_rule_spec_list[] = [ - 'error' => $validity, - // @todo The tag_spec should be included always, whenever a validation error is raised. Or rather, just the spec_name. The spec_url can be looked up. - 'tag_spec' => $rule_spec[ AMP_Rule_Spec::TAG_SPEC ], - ]; + $validation_errors[] = array_merge( + $validity, + [ 'spec_name' => $this->get_spec_name( $node, $rule_spec[ AMP_Rule_Spec::TAG_SPEC ] ) ] + ); } } // If no valid rule_specs exist, then remove this node and return. if ( empty( $rule_spec_list_to_validate ) ) { - if ( 1 === count( $invalid_rule_spec_list ) ) { + if ( 1 === count( $validation_errors ) ) { // If there was only one tag spec candidate that failed, use its error code for removing the node, // since it's we know it is the specific reason for why the node had to be removed. // This is the normal case. $this->remove_invalid_child( $node, - array_merge( - $invalid_rule_spec_list[0]['error'], - [ - 'tag_spec' => $invalid_rule_spec_list[0]['tag_spec'], - ] - ) + $validation_errors[0] ); } else { $this->remove_node( $node ); @@ -572,8 +566,13 @@ private function process_node( DOMElement $node ) { if ( ! empty( $cdata ) && $node instanceof DOMElement ) { $validity = $this->validate_cdata_for_node( $node, $cdata ); if ( true !== $validity ) { - // @todo Need to pass tag_spec. - $this->remove_invalid_child( $node, $validity ); + $this->remove_invalid_child( + $node, + array_merge( + $validity, + [ 'spec_name' => $this->get_spec_name( $node, $tag_spec ) ] + ) + ); return null; } } @@ -601,8 +600,10 @@ private function process_node( DOMElement $node ) { if ( ! empty( $this->visited_unique_tag_specs[ $node->nodeName ][ $tag_spec_key ] ) ) { $removed = $this->remove_invalid_child( $node, - // @todo Need to pass tag_spec. - [ 'code' => self::DUPLICATE_UNIQUE_TAG ] + [ + 'code' => self::DUPLICATE_UNIQUE_TAG, + 'spec_name' => $this->get_spec_name( $node, $tag_spec ), + ] ); } $this->visited_unique_tag_specs[ $node->nodeName ][ $tag_spec_key ] = true; @@ -661,7 +662,7 @@ private function process_node( DOMElement $node ) { if ( ! empty( $tag_spec[ AMP_Rule_Spec::DESCENDANT_TAG_LIST ] ) ) { $allowed_tags = AMP_Allowed_Tags_Generated::get_descendant_tag_list( $tag_spec[ AMP_Rule_Spec::DESCENDANT_TAG_LIST ] ); if ( ! empty( $allowed_tags ) ) { - $this->remove_disallowed_descendants( $node, $allowed_tags ); + $this->remove_disallowed_descendants( $node, $allowed_tags, $this->get_spec_name( $node, $tag_spec ) ); } } @@ -671,9 +672,9 @@ private function process_node( DOMElement $node ) { $this->remove_invalid_child( $node, [ - // @todo Need to pass the $tag_spec['spec_name'] into the validation errors for each. 'code' => self::ATTR_REQUIRED_BUT_MISSING, 'attributes' => $missing_mandatory_attributes, + 'spec_name' => $this->get_spec_name( $node, $tag_spec ), ] ); return null; @@ -763,7 +764,7 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { ) { return [ 'code' => self::CDATA_TOO_LONG, - 'max_bytes' => $cdata_spec['max_bytes'], + 'max_bytes' => $cdata_spec['max_bytes'], // @todo This is not needed with the spec_name in hand. ]; } if ( isset( $cdata_spec['blacklisted_cdata_regex'] ) ) { @@ -813,7 +814,7 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) && ! $this->has_parent( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) ) { return [ 'code' => self::WRONG_PARENT_TAG, - 'mandatory_parent' => $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ], + 'mandatory_parent' => $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ], // @todo This is not needed with the spec_name in hand. ]; } @@ -838,7 +839,7 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) && ! $this->has_ancestor( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) ) { return [ 'code' => self::MANDATORY_TAG_ANCESTOR, - 'mandatory_ancestor' => $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ], + 'mandatory_ancestor' => $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ], // @todo This is not needed with the spec_name in hand. ]; } @@ -989,6 +990,29 @@ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_ return $score; } + /** + * Get spec name for a given tag spec. + * + * @since 1.5 + * + * @param DOMElement $element Element. + * @param array $tag_spec Tag spec. + * @return string Spec name. + */ + private function get_spec_name( DOMElement $element, $tag_spec ) { + if ( isset( $tag_spec['spec_name'] ) ) { + return $tag_spec['spec_name']; + } elseif ( isset( $tag_spec['extension_spec']['name'] ) ) { + return sprintf( + 'script[%s=%s]', + 'amp-mustache' === $tag_spec['extension_spec']['name'] ? 'custom-template' : 'custom-element', + strtolower( $tag_spec['extension_spec']['name'] ) + ); + } else { + return $element->nodeName; + } + } + /** * Remove invalid AMP attributes values from $node that have been implicitly disallowed. * @@ -1824,8 +1848,9 @@ private function parse_tag_and_attributes_from_spec_name( $spec_name ) { * * @param DOMElement $node Node. * @param string[] $allowed_descendants List of allowed descendant tags. + * @param string $spec_name Spec name. */ - private function remove_disallowed_descendants( DOMElement $node, $allowed_descendants ) { + private function remove_disallowed_descendants( DOMElement $node, $allowed_descendants, $spec_name ) { if ( ! $node->hasChildNodes() ) { return; } @@ -1843,13 +1868,13 @@ private function remove_disallowed_descendants( DOMElement $node, $allowed_desce $this->remove_invalid_child( $child_element, [ - // @todo Need to pass tag_spec. 'code' => self::DISALLOWED_DESCENDANT_TAG, 'allowed_descendants' => $allowed_descendants, + 'spec_name' => $spec_name, ] ); } else { - $this->remove_disallowed_descendants( $child_element, $allowed_descendants ); + $this->remove_disallowed_descendants( $child_element, $allowed_descendants, $spec_name ); } } } @@ -1882,7 +1907,7 @@ private function check_valid_children( DOMElement $node, $child_tags ) { return [ 'code' => self::DISALLOWED_FIRST_CHILD_TAG, 'first_child_tag' => $child_elements[0]->nodeName, - 'first_child_tag_name_oneof' => $child_tags['first_child_tag_name_oneof'], + 'first_child_tag_name_oneof' => $child_tags['first_child_tag_name_oneof'], // @todo This is not needed with the spec_name in hand. ]; } @@ -1893,7 +1918,7 @@ private function check_valid_children( DOMElement $node, $child_tags ) { return [ 'code' => self::DISALLOWED_CHILD_TAG, 'child_tag' => $child_element->nodeName, - 'child_tag_name_oneof' => $child_tags['child_tag_name_oneof'], + 'child_tag_name_oneof' => $child_tags['child_tag_name_oneof'], // @todo This is not needed with the spec_name in hand. ]; } } @@ -1908,7 +1933,7 @@ private function check_valid_children( DOMElement $node, $child_tags ) { return [ 'code' => self::INCORRECT_NUM_CHILD_TAGS, 'children_count' => $child_element_count, - 'mandatory_num_child_tags' => $child_tags['mandatory_num_child_tags'], + 'mandatory_num_child_tags' => $child_tags['mandatory_num_child_tags'], // @todo This is not needed with the spec_name in hand. ]; } } @@ -1922,7 +1947,7 @@ private function check_valid_children( DOMElement $node, $child_tags ) { return [ 'code' => self::INCORRECT_MIN_NUM_CHILD_TAGS, 'children_count' => $child_element_count, - 'mandatory_min_num_child_tags' => $child_tags['mandatory_min_num_child_tags'], + 'mandatory_min_num_child_tags' => $child_tags['mandatory_min_num_child_tags'], // @todo This is not needed with the spec_name in hand. ]; } } diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 4293811edd0..eab8bbb9d23 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -403,6 +403,7 @@ static function () { [ 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_DESCENDANT_TAG, 'node_name' => 'button', + 'spec_name' => 'amp-story-grid-layer', ], ], ]; @@ -906,10 +907,12 @@ static function() { [ 'code' => AMP_Tag_And_Attribute_Sanitizer::WRONG_PARENT_TAG, 'node_name' => 'amp-app-banner', + 'spec_name' => 'amp-app-banner', ], [ 'code' => AMP_Tag_And_Attribute_Sanitizer::WRONG_PARENT_TAG, 'node_name' => 'amp-app-banner', + 'spec_name' => 'amp-app-banner', ], ], ], @@ -2345,6 +2348,7 @@ public function get_data_for_replace_node_with_children_validation_errors() { 'node_attributes' => [ 'class' => 'a-invalid' ], 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, 'mandatory_ancestor' => 'amp-story-page', + 'spec_name' => 'amp-story-grid-layer', ], ], ], @@ -2469,7 +2473,6 @@ public function test_replace_node_with_children_validation_errors( $source_conte 'validation_error_callback' => function( $error, $context ) use ( &$expected_errors ) { $expected = array_shift( $expected_errors ); $tag = $expected['node_name']; - unset( $error['tag_spec'] ); // No need to check for this, although eventually we should assert it is present. $this->assertEquals( $expected, $error ); $this->assertInstanceOf( 'DOMElement', $context['node'] ); $this->assertEquals( $tag, $context['node']->tagName ); From 10b0c2943b464d83402f6767196b078d05d879c8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 15:42:00 -0800 Subject: [PATCH 26/39] Include spec_name in validation errors raised by style sanitizer --- .../sanitizers/class-amp-style-sanitizer.php | 115 +++++++++++------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 4bfc50970f1..2cdb0b20aa9 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -26,6 +26,12 @@ */ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { + const STYLE_AMP_CUSTOM_SPEC_NAME = 'style amp-custom'; + const STYLE_AMP_KEYFRAMES_SPEC_NAME = 'style[amp-keyframes]'; + + const STYLE_AMP_CUSTOM_GROUP_INDEX = 0; + const STYLE_AMP_KEYFRAMES_GROUP_INDEX = 1; + const CSS_SYNTAX_INVALID_AT_RULE = 'CSS_SYNTAX_INVALID_AT_RULE'; const CSS_SYNTAX_INVALID_DECLARATION = 'CSS_SYNTAX_INVALID_DECLARATION'; const CSS_SYNTAX_INVALID_PROPERTY = 'CSS_SYNTAX_INVALID_PROPERTY'; @@ -400,9 +406,9 @@ public function __construct( DOMDocument $dom, array $args = [] ) { if ( ! isset( $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) ) { continue; } - if ( 'style[amp-keyframes]' === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) { + if ( self::STYLE_AMP_KEYFRAMES_SPEC_NAME === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) { $this->style_keyframes_cdata_spec = $spec_rule[ AMP_Rule_Spec::CDATA ]; - } elseif ( 'style amp-custom' === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) { + } elseif ( self::STYLE_AMP_CUSTOM_SPEC_NAME === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) { $this->style_custom_cdata_spec = $spec_rule[ AMP_Rule_Spec::CDATA ]; } } @@ -447,7 +453,7 @@ public function get_stylesheets() { array_filter( $this->pending_stylesheets, static function( $pending_stylesheet ) { - return $pending_stylesheet['included'] && 'custom' === $pending_stylesheet['group']; + return $pending_stylesheet['included'] && self::STYLE_AMP_CUSTOM_GROUP_INDEX === $pending_stylesheet['group']; } ), 'stylesheet' @@ -1199,12 +1205,13 @@ private function process_style_element( DOMElement $element ) { 'allowed_at_rules' => $cdata_spec['css_spec']['allowed_at_rules'], 'property_whitelist' => $cdata_spec['css_spec']['declaration'], 'validate_keyframes' => $cdata_spec['css_spec']['validate_keyframes'], + 'spec_name' => $is_keyframes ? self::STYLE_AMP_KEYFRAMES_SPEC_NAME : self::STYLE_AMP_CUSTOM_SPEC_NAME, ] ); $this->pending_stylesheets[] = array_merge( [ - 'group' => $is_keyframes ? 'keyframes' : 'custom', + 'group' => $is_keyframes ? self::STYLE_AMP_KEYFRAMES_GROUP_INDEX : self::STYLE_AMP_CUSTOM_GROUP_INDEX, 'node' => $element, 'sources' => $this->current_sources, 'priority' => $this->get_stylesheet_priority( $element ), @@ -1313,12 +1320,13 @@ private function process_link_element( DOMElement $element ) { 'property_whitelist' => $this->style_custom_cdata_spec['css_spec']['declaration'], 'stylesheet_url' => $href, 'stylesheet_path' => $css_file_path, + 'spec_name' => self::STYLE_AMP_CUSTOM_SPEC_NAME, ] ); $this->pending_stylesheets[] = array_merge( [ - 'group' => 'custom', + 'group' => self::STYLE_AMP_CUSTOM_GROUP_INDEX, 'node' => $element, 'sources' => $this->current_sources, // Needed because node is removed below. 'priority' => $this->get_stylesheet_priority( $element ), @@ -1389,6 +1397,7 @@ private function fetch_external_stylesheet( $url ) { * @type string $stylesheet_path Original filesystem path for stylesheet when originating via link or @import. * @type array $allowed_at_rules Allowed @-rules. * @type bool $validate_keyframes Whether keyframes should be validated. + * @type string $spec_name Spec name. * } * @return array { * Processed stylesheet. @@ -1593,6 +1602,7 @@ private function parse_import_stylesheet( Import $item, CSSList $css_list, $opti * @type Document $css_document CSS Document. * @type array $validation_results Validation results, array containing arrays with error and sanitized keys. * @type string $stylesheet_url Stylesheet URL, if available. + * @type string $spec_name Spec name. * } */ private function parse_stylesheet( $stylesheet_string, $options ) { @@ -1626,9 +1636,10 @@ static function ( $value ) { ); } catch ( Exception $exception ) { $error = [ - 'code' => self::CSS_SYNTAX_PARSE_ERROR, - 'message' => $exception->getMessage(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_PARSE_ERROR, + 'message' => $exception->getMessage(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; /* @@ -1677,6 +1688,7 @@ private function prepare_stylesheet( $stylesheet_string, $options = [] ) { 'validate_keyframes' => false, 'stylesheet_url' => null, 'stylesheet_path' => null, + 'spec_name' => null, ], $options ); @@ -1925,9 +1937,10 @@ private function process_css_list( CSSList $css_list, $options ) { } elseif ( $css_item instanceof AtRuleBlockList ) { if ( ! in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, - 'at_rule' => $css_item->atRuleName(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, + 'at_rule' => $css_item->atRuleName(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); $results[] = compact( 'error', 'sanitized' ); @@ -1946,9 +1959,10 @@ private function process_css_list( CSSList $css_list, $options ) { } elseif ( $css_item instanceof AtRuleSet ) { if ( ! in_array( $css_item->atRuleName(), $options['allowed_at_rules'], true ) ) { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, - 'at_rule' => $css_item->atRuleName(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, + 'at_rule' => $css_item->atRuleName(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); $results[] = compact( 'error', 'sanitized' ); @@ -1963,9 +1977,10 @@ private function process_css_list( CSSList $css_list, $options ) { } elseif ( $css_item instanceof KeyFrame ) { if ( ! in_array( 'keyframes', $options['allowed_at_rules'], true ) ) { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, - 'at_rule' => $css_item->atRuleName(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, + 'at_rule' => $css_item->atRuleName(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); $results[] = compact( 'error', 'sanitized' ); @@ -1988,18 +2003,20 @@ private function process_css_list( CSSList $css_list, $options ) { $sanitized = true; } else { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, - 'at_rule' => $css_item->atRuleName(), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_AT_RULE, + 'at_rule' => $css_item->atRuleName(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); $results[] = compact( 'error', 'sanitized' ); } } else { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_DECLARATION, - 'item' => get_class( $css_item ), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_DECLARATION, + 'item' => get_class( $css_item ), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); $results[] = compact( 'error', 'sanitized' ); @@ -2090,6 +2107,7 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l 'property_value' => $property->getValue(), 'allowed_properties' => $options['property_whitelist'], 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2108,6 +2126,7 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l 'property_value' => (string) $property->getValue(), 'allowed_properties' => $options['property_whitelist'], 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2124,7 +2143,7 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l $results = array_merge( $results, - $this->transform_important_qualifiers( $ruleset, $css_list ) + $this->transform_important_qualifiers( $ruleset, $css_list, $options ) ); // Remove the ruleset if it is now empty. @@ -2316,9 +2335,10 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { foreach ( $css_list->getContents() as $rules ) { if ( ! ( $rules instanceof DeclarationBlock ) ) { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_DECLARATION, - 'item' => get_class( $rules ), - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::CSS_SYNTAX_INVALID_DECLARATION, + 'item' => get_class( $rules ), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2330,7 +2350,7 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { $results = array_merge( $results, - $this->transform_important_qualifiers( $rules, $css_list ) + $this->transform_important_qualifiers( $rules, $css_list, $options ) ); $properties = $rules->getRules(); @@ -2343,6 +2363,7 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { 'property_value' => (string) $property->getValue(), 'allowed_properties' => $options['property_whitelist'], 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2365,9 +2386,10 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { * * @param RuleSet|DeclarationBlock $ruleset Rule set. * @param CSSList $css_list CSS List. + * @param array $options Options. * @return array Validation results. */ - private function transform_important_qualifiers( RuleSet $ruleset, CSSList $css_list ) { + private function transform_important_qualifiers( RuleSet $ruleset, CSSList $css_list, $options ) { $results = []; // An !important only makes sense for rulesets that have selectors. @@ -2391,6 +2413,7 @@ private function transform_important_qualifiers( RuleSet $ruleset, CSSList $css_ 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'property_name' => $property->getRule(), 'property_value' => $property->getValue(), + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2482,6 +2505,7 @@ private function collect_inline_styles( $element ) { [ 'allowed_at_rules' => [], 'property_whitelist' => $this->style_custom_cdata_spec['css_spec']['declaration'], + 'spec_name' => self::STYLE_AMP_CUSTOM_SPEC_NAME, ] ); @@ -2489,7 +2513,7 @@ private function collect_inline_styles( $element ) { if ( $processed['stylesheet'] ) { $this->pending_stylesheets[] = [ - 'group' => 'custom', + 'group' => self::STYLE_AMP_CUSTOM_GROUP_INDEX, 'stylesheet' => $processed['stylesheet'], 'node' => $element, 'sources' => $this->current_sources, @@ -2517,14 +2541,14 @@ private function collect_inline_styles( $element ) { */ private function finalize_styles() { $stylesheet_groups = [ - 'custom' => [ + self::STYLE_AMP_CUSTOM_GROUP_INDEX => [ 'source_map_comment' => "\n\n/*# sourceURL=amp-custom.css */", 'cdata_spec' => $this->style_custom_cdata_spec, 'pending_stylesheets' => [], 'included_count' => 0, 'import_front_matter' => '', // Extra @import statements that are prepended when fetch fails and validation error is rejected. ], - 'keyframes' => [ + self::STYLE_AMP_KEYFRAMES_GROUP_INDEX => [ 'source_map_comment' => "\n\n/*# sourceURL=amp-keyframes.css */", 'cdata_spec' => $this->style_keyframes_cdata_spec, 'pending_stylesheets' => [], @@ -2560,7 +2584,7 @@ private function finalize_styles() { } // Add style[amp-custom] to document. - if ( $stylesheet_groups['custom']['included_count'] > 0 ) { + if ( $stylesheet_groups[ self::STYLE_AMP_CUSTOM_GROUP_INDEX ]['included_count'] > 0 ) { // Ensure style[amp-custom] is present in the document. if ( ! $this->amp_custom_style_element ) { @@ -2573,10 +2597,10 @@ private function finalize_styles() { * On AMP-first themes when there are new/rejected validation errors present, a parsed stylesheet may include * @import rules. These must be moved to the beginning to be honored. */ - $css = $stylesheet_groups['custom']['import_front_matter']; + $css = $stylesheet_groups[ self::STYLE_AMP_CUSTOM_GROUP_INDEX ]['import_front_matter']; $css .= implode( '', $this->get_stylesheets() ); - $css .= $stylesheet_groups['custom']['source_map_comment']; + $css .= $stylesheet_groups[ self::STYLE_AMP_CUSTOM_GROUP_INDEX ]['source_map_comment']; /* * Let the style[amp-custom] be populated with the concatenated CSS. @@ -2596,7 +2620,7 @@ private function finalize_styles() { $included_sources = []; $excluded_sources = []; foreach ( $this->pending_stylesheets as $j => $pending_stylesheet ) { - if ( 'custom' !== $pending_stylesheet['group'] || ! ( $pending_stylesheet['node'] instanceof DOMElement ) || ! empty( $pending_stylesheet['duplicate'] ) ) { + if ( self::STYLE_AMP_CUSTOM_GROUP_INDEX !== $pending_stylesheet['group'] || ! ( $pending_stylesheet['node'] instanceof DOMElement ) || ! empty( $pending_stylesheet['duplicate'] ) ) { continue; } $message = sprintf( '% 6d B', $pending_stylesheet['size'] ); @@ -2725,13 +2749,13 @@ private function finalize_styles() { } // Add style[amp-keyframes] to document. - if ( $stylesheet_groups['keyframes']['included_count'] > 0 ) { + if ( $stylesheet_groups[ self::STYLE_AMP_KEYFRAMES_GROUP_INDEX ]['included_count'] > 0 ) { $body = $this->dom->getElementsByTagName( 'body' )->item( 0 ); if ( ! $body ) { $body = $this->dom->createElement( 'body' ); $this->dom->documentElement->appendChild( $body ); } else { - $css = $stylesheet_groups['keyframes']['import_front_matter']; + $css = $stylesheet_groups[ self::STYLE_AMP_KEYFRAMES_GROUP_INDEX ]['import_front_matter']; $css .= implode( '', @@ -2739,13 +2763,13 @@ private function finalize_styles() { array_filter( $this->pending_stylesheets, static function( $pending_stylesheet ) { - return $pending_stylesheet['included'] && 'keyframes' === $pending_stylesheet['group']; + return $pending_stylesheet['included'] && self::STYLE_AMP_KEYFRAMES_GROUP_INDEX === $pending_stylesheet['group']; } ), 'stylesheet' ) ); - $css .= $stylesheet_groups['keyframes']['source_map_comment']; + $css .= $stylesheet_groups[ self::STYLE_AMP_KEYFRAMES_GROUP_INDEX ]['source_map_comment']; $style_element = $this->dom->createElement( 'style' ); $style_element->setAttribute( 'amp-keyframes', '' ); @@ -2776,7 +2800,7 @@ private function remove_admin_bar_if_css_excluded() { $included = true; foreach ( $this->pending_stylesheets as &$pending_stylesheet ) { $is_admin_bar_css = ( - 'custom' === $pending_stylesheet['group'] + self::STYLE_AMP_CUSTOM_GROUP_INDEX === $pending_stylesheet['group'] && $pending_stylesheet['node'] instanceof DOMElement && @@ -2980,8 +3004,8 @@ static function ( $class_name ) { * * @since 1.2 * - * @param string $group Group name (either 'custom' or 'keyframes'). - * @param array $group_config Group config. + * @param int $group Group name (either self::STYLE_AMP_CUSTOM_GROUP_INDEX or self::STYLE_AMP_KEYFRAMES_GROUP_INDEX ). + * @param array $group_config Group config. * @return int Number of included stylesheets in group. */ private function finalize_stylesheet_group( $group, $group_config ) { @@ -3142,8 +3166,9 @@ function( $a, $b ) { // Report validation error if size is now too big. if ( $current_concatenated_size + $this->pending_stylesheets[ $i ]['size'] > $max_bytes ) { $validation_error = [ - 'code' => self::STYLESHEET_TOO_LONG, - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'code' => self::STYLESHEET_TOO_LONG, + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => self::STYLE_AMP_KEYFRAMES_GROUP_INDEX === $group ? self::STYLE_AMP_KEYFRAMES_SPEC_NAME : self::STYLE_AMP_CUSTOM_SPEC_NAME, ]; if ( isset( $this->pending_stylesheets[ $i ]['sources'] ) ) { $validation_error['sources'] = $this->pending_stylesheets[ $i ]['sources']; From 6975bd0ae826c7dfb6d4508dabb8114aa722d428 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 25 Nov 2019 17:48:37 -0800 Subject: [PATCH 27/39] Verify unique tag spec names when generating spec --- bin/amphtml-update.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/bin/amphtml-update.py b/bin/amphtml-update.py index 0e8f509e616..469d744696e 100644 --- a/bin/amphtml-update.py +++ b/bin/amphtml-update.py @@ -29,6 +29,8 @@ from collections import defaultdict import imp +seen_spec_names = set() + def Die(msg): print >> sys.stderr, msg sys.exit(1) @@ -96,6 +98,14 @@ def GeneratePHP(out_dir): allowed_tags, attr_lists, descendant_lists, reference_points, versions = ParseRules(out_dir) + expected_spec_names = ( + 'style amp-custom', + 'style[amp-keyframes]', + ) + for expected_spec_name in expected_spec_names: + if expected_spec_name not in seen_spec_names: + raise Exception( 'Missing spec: %s' % expected_spec_name ) + #Generate the output out = [] GenerateHeaderPHP(out) @@ -477,6 +487,21 @@ def GetTagSpec(tag_spec, attr_lists): raise Exception( 'Unexpected error_message "%s" for blacklisted_cdata_regex.' % cdata_dict['blacklisted_cdata_regex']['error_message'] ); tag_spec_dict['cdata'] = cdata_dict + if 'spec_name' not in tag_spec_dict['tag_spec']: + if 'extension_spec' in tag_spec_dict['tag_spec']: + # CUSTOM_ELEMENT=1 (default), CUSTOM_TEMPLATE=2 + extension_type = tag_spec_dict['tag_spec']['extension_spec'].get('extension_type', 1) + spec_name = 'script [%s=%s]' % ( 'custom-element' if 1 == extension_type else 'custom-template', tag_spec_dict['tag_spec']['extension_spec']['name'].lower() ) + else: + spec_name = tag_spec.tag_name.lower() + else: + spec_name = tag_spec_dict['tag_spec']['spec_name'] + + if '$reference_point' != spec_name: + if spec_name in seen_spec_names: + raise Exception( 'Already seen spec_name: %s' % spec_name ) + seen_spec_names.add( spec_name ) + return tag_spec_dict From e2db927c5af1b0a1ecae833582b10250beed8ed5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 26 Nov 2019 06:36:39 -0800 Subject: [PATCH 28/39] Remove redundant info from validation error now that spec_name provided --- .../sanitizers/class-amp-style-sanitizer.php | 33 +++++++++---------- .../class-amp-tag-and-attribute-sanitizer.php | 11 ++----- .../php/test-tag-and-attribute-sanitizer.php | 13 ++++---- 3 files changed, 24 insertions(+), 33 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 2cdb0b20aa9..97b578031ed 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -2102,12 +2102,11 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() ); if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_PROPERTY, - 'property_name' => $property->getRule(), - 'property_value' => $property->getValue(), - 'allowed_properties' => $options['property_whitelist'], - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - 'spec_name' => $options['spec_name'], + 'code' => self::CSS_SYNTAX_INVALID_PROPERTY, + 'property_name' => $property->getRule(), + 'property_value' => $property->getValue(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2121,12 +2120,11 @@ private function process_css_declaration_block( RuleSet $ruleset, CSSList $css_l $properties = $ruleset->getRules( $illegal_property_name ); foreach ( $properties as $property ) { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST, - 'property_name' => $property->getRule(), - 'property_value' => (string) $property->getValue(), - 'allowed_properties' => $options['property_whitelist'], - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - 'spec_name' => $options['spec_name'], + 'code' => self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST, + 'property_name' => $property->getRule(), + 'property_value' => (string) $property->getValue(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { @@ -2358,12 +2356,11 @@ private function process_css_keyframes( KeyFrame $css_list, $options ) { $vendorless_property_name = preg_replace( '/^-\w+-/', '', $property->getRule() ); if ( ! in_array( $vendorless_property_name, $options['property_whitelist'], true ) ) { $error = [ - 'code' => self::CSS_SYNTAX_INVALID_PROPERTY, - 'property_name' => $property->getRule(), - 'property_value' => (string) $property->getValue(), - 'allowed_properties' => $options['property_whitelist'], - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - 'spec_name' => $options['spec_name'], + 'code' => self::CSS_SYNTAX_INVALID_PROPERTY, + 'property_name' => $property->getRule(), + 'property_value' => (string) $property->getValue(), + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'spec_name' => $options['spec_name'], ]; $sanitized = $this->should_sanitize_validation_error( $error ); if ( $sanitized ) { diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 9e96a82bc17..b52e94c073e 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -763,8 +763,7 @@ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { ! ( 'style' === $element->nodeName && $element->hasAttribute( 'amp-custom' ) ) ) { return [ - 'code' => self::CDATA_TOO_LONG, - 'max_bytes' => $cdata_spec['max_bytes'], // @todo This is not needed with the spec_name in hand. + 'code' => self::CDATA_TOO_LONG, ]; } if ( isset( $cdata_spec['blacklisted_cdata_regex'] ) ) { @@ -813,8 +812,7 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) && ! $this->has_parent( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ] ) ) { return [ - 'code' => self::WRONG_PARENT_TAG, - 'mandatory_parent' => $tag_spec[ AMP_Rule_Spec::MANDATORY_PARENT ], // @todo This is not needed with the spec_name in hand. + 'code' => self::WRONG_PARENT_TAG, ]; } @@ -838,8 +836,7 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) && ! $this->has_ancestor( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ] ) ) { return [ - 'code' => self::MANDATORY_TAG_ANCESTOR, - 'mandatory_ancestor' => $tag_spec[ AMP_Rule_Spec::MANDATORY_ANCESTOR ], // @todo This is not needed with the spec_name in hand. + 'code' => self::MANDATORY_TAG_ANCESTOR, ]; } @@ -1074,8 +1071,6 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, * However, if there was only one spec for a given tag, then then validate_attr_spec_list_for_node() would * not have been called. and thus these checks need to be performed here as well. */ - // @todo Actual AMP validator error codes should be used whenever possible. And constants should be used. - // @todo The check methods should return an array of validation error data when failure. if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value( $node, $attr_name, $attr_spec_rule ) ) { $error_code = self::INVALID_ATTR_VALUE; diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index eab8bbb9d23..6feae567d10 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -2342,13 +2342,12 @@ public function get_data_for_replace_node_with_children_validation_errors() { '', [ [ - 'node_name' => 'amp-story-grid-layer', - 'parent_name' => 'body', - 'code' => AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR, - 'node_attributes' => [ 'class' => 'a-invalid' ], - 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, - 'mandatory_ancestor' => 'amp-story-page', - 'spec_name' => 'amp-story-grid-layer', + 'node_name' => 'amp-story-grid-layer', + 'parent_name' => 'body', + 'code' => AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR, + 'node_attributes' => [ 'class' => 'a-invalid' ], + 'type' => AMP_Validation_Error_Taxonomy::HTML_ELEMENT_ERROR_TYPE, + 'spec_name' => 'amp-story-grid-layer', ], ], ], From 5e56cce6dc83478d335b69a90fe7cb42b0f23906 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 26 Nov 2019 08:27:41 -0800 Subject: [PATCH 29/39] Fix up PHP comments Co-Authored-By: Alain Schlesser --- includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index b52e94c073e..5025a11165d 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -475,7 +475,7 @@ private function process_node( DOMElement $node ) { if ( empty( $rule_spec_list_to_validate ) ) { if ( 1 === count( $validation_errors ) ) { // If there was only one tag spec candidate that failed, use its error code for removing the node, - // since it's we know it is the specific reason for why the node had to be removed. + // since we know it is the specific reason for why the node had to be removed. // This is the normal case. $this->remove_invalid_child( $node, @@ -886,7 +886,7 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { * @param DOMElement $node Node. * @param array[] $attr_spec_list Attribute Spec list. * - * @return int Score for how well the attribute spec list patched. + * @return int Score for how well the attribute spec list matched. */ private function validate_attr_spec_list_for_node( DOMElement $node, $attr_spec_list ) { /* From 9067d6c01e89edae7982f40384b44036c2bf2ea5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 28 Nov 2019 00:21:25 -0800 Subject: [PATCH 30/39] Remove obsolete DISALLOWED_DOMAIN checks --- includes/sanitizers/class-amp-rule-spec.php | 1 - .../class-amp-tag-and-attribute-sanitizer.php | 35 ------------------- 2 files changed, 36 deletions(-) diff --git a/includes/sanitizers/class-amp-rule-spec.php b/includes/sanitizers/class-amp-rule-spec.php index 55472a50610..34de69cc792 100644 --- a/includes/sanitizers/class-amp-rule-spec.php +++ b/includes/sanitizers/class-amp-rule-spec.php @@ -45,7 +45,6 @@ abstract class AMP_Rule_Spec { const ALLOWED_PROTOCOL = 'protocol'; const ALTERNATIVE_NAMES = 'alternative_names'; const BLACKLISTED_VALUE_REGEX = 'blacklisted_value_regex'; - const DISALLOWED_DOMAIN = 'disallowed_domain'; const MANDATORY = 'mandatory'; const VALUE = 'value'; const VALUE_CASEI = 'value_casei'; diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 5025a11165d..0ed0cdc593b 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -51,7 +51,6 @@ class AMP_Tag_And_Attribute_Sanitizer extends AMP_Base_Sanitizer { const INVALID_URL = 'INVALID_URL'; const DISALLOWED_RELATIVE_URL = 'DISALLOWED_RELATIVE_URL'; const DISALLOWED_EMPTY_URL = 'DISALLOWED_EMPTY_URL'; - const DISALLOWED_DOMAIN = 'DISALLOWED_DOMAIN'; const INVALID_BLACKLISTED_VALUE_REGEX = 'INVALID_BLACKLISTED_VALUE_REGEX'; const DISALLOWED_PROPERTY_IN_ATTR_VALUE = 'DISALLOWED_PROPERTY_IN_ATTR_VALUE'; const ATTR_REQUIRED_BUT_MISSING = 'ATTR_REQUIRED_BUT_MISSING'; @@ -1095,9 +1094,6 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE_URL ][ AMP_Rule_Spec::ALLOW_RELATIVE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_relative( $node, $attr_name, $attr_spec_rule ) ) { $error_code = self::DISALLOWED_RELATIVE_URL; - } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::DISALLOWED_DOMAIN ] ) && - AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_disallowed_domain( $node, $attr_name, $attr_spec_rule ) ) { - $error_code = self::DISALLOWED_DOMAIN; } elseif ( isset( $attr_spec_rule[ AMP_Rule_Spec::BLACKLISTED_VALUE_REGEX ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_blacklisted_value_regex( $node, $attr_name, $attr_spec_rule ) ) { $error_code = self::INVALID_BLACKLISTED_VALUE_REGEX; @@ -1550,37 +1546,6 @@ private function check_attr_spec_rule_disallowed_empty( DOMElement $node, $attr_ return AMP_Rule_Spec::NOT_APPLICABLE; } - /** - * Check if attribute has disallowed domain value rule determine if value matches. - * - * @param DOMElement $node Node. - * @param string $attr_name Attribute name. - * @param array[]|string[] $attr_spec_rule Attribute spec rule. - * - * @return string: - * - AMP_Rule_Spec::PASS - $attr_name has a value that matches the rule. - * - AMP_Rule_Spec::FAIL - $attr_name has a value that does *not* match rule. - * - AMP_Rule_Spec::NOT_APPLICABLE - $attr_name does not exist or there - * is no rule for this attribute. - */ - private function check_attr_spec_rule_disallowed_domain( DOMElement $node, $attr_name, $attr_spec_rule ) { - if ( isset( $attr_spec_rule[ AMP_Rule_Spec::DISALLOWED_DOMAIN ] ) && $node->hasAttribute( $attr_name ) ) { - $attr_value = $node->getAttribute( $attr_name ); - $url_domain = wp_parse_url( $attr_value, PHP_URL_HOST ); - if ( ! empty( $url_domain ) ) { - foreach ( $attr_spec_rule[ AMP_Rule_Spec::DISALLOWED_DOMAIN ] as $disallowed_domain ) { - if ( strtolower( $url_domain ) === strtolower( $disallowed_domain ) ) { - - // Found a disallowed domain, fail validation. - return AMP_Rule_Spec::FAIL; - } - } - return AMP_Rule_Spec::PASS; - } - } - return AMP_Rule_Spec::NOT_APPLICABLE; - } - /** * Check if attribute has blacklisted value via regex match determine if value matches. * From 5d9c205685d9c1c413701569aad9406b89f907dd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 28 Nov 2019 00:24:47 -0800 Subject: [PATCH 31/39] Add tests for INVALID_CDATA_CONTENTS and DISALLOWED_RELATIVE_URL --- tests/php/test-tag-and-attribute-sanitizer.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 6feae567d10..c88eb73f41c 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -1938,6 +1938,13 @@ static function() { [ 'amp-geo' ], [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS ], ], + + 'amp-social-share-relative-url' => [ + '', + '', + [ 'amp-social-share' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_RELATIVE_URL ], + ], ]; } @@ -2088,12 +2095,18 @@ public function get_html_data() { [], [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CSS_IMPORTANT ], ], - 'cdata_contents' => [ + 'cdata_contents_bad_comment' => [ '', '', [], [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS ], ], + 'script_cdata_contents_bad' => [ + '', + '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CONTENTS ], + ], ]; $bad_dev_mode_document = sprintf( From 7470a24133c3b7e4e4f1e07984bbf2f3e6c50c82 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 28 Nov 2019 00:33:52 -0800 Subject: [PATCH 32/39] Add test for MANDATORY_CDATA_MISSING_OR_INCORRECT --- tests/php/test-tag-and-attribute-sanitizer.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index c88eb73f41c..1d8e403139f 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -2107,6 +2107,12 @@ public function get_html_data() { [], [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CONTENTS ], ], + 'cdata_regex_failure' => [ + '', + '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::MANDATORY_CDATA_MISSING_OR_INCORRECT ], + ], ]; $bad_dev_mode_document = sprintf( From 038caa7e43cf48a070182fa8c7500a61b6cbe87c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 29 Nov 2019 13:50:26 -0800 Subject: [PATCH 33/39] Add tests for MANDATORY_TAG_ANCESTOR, DISALLOWED_TAG_ANCESTOR, and (new) DISALLOWED_TAG_MULTIPLE_CHOICES --- .../class-amp-tag-and-attribute-sanitizer.php | 42 ++++++++++-- .../php/test-tag-and-attribute-sanitizer.php | 67 ++++++++++++++++++- 2 files changed, 102 insertions(+), 7 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 0ed0cdc593b..574c5856620 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -26,6 +26,7 @@ class AMP_Tag_And_Attribute_Sanitizer extends AMP_Base_Sanitizer { const DISALLOWED_TAG = 'DISALLOWED_TAG'; + const DISALLOWED_TAG_MULTIPLE_CHOICES = 'DISALLOWED_TAG_MULTIPLE_CHOICES'; const DISALLOWED_CHILD_TAG = 'DISALLOWED_CHILD_TAG'; const DISALLOWED_FIRST_CHILD_TAG = 'DISALLOWED_FIRST_CHILD_TAG'; const INCORRECT_NUM_CHILD_TAGS = 'INCORRECT_NUM_CHILD_TAGS'; @@ -453,11 +454,8 @@ private function process_node( DOMElement $node ) { * based on tag name of the node. */ $rule_spec_list_to_validate = []; - $rule_spec_list = []; $validation_errors = []; - if ( isset( $this->allowed_tags[ $node->nodeName ] ) ) { - $rule_spec_list = $this->allowed_tags[ $node->nodeName ]; - } + $rule_spec_list = $this->allowed_tags[ $node->nodeName ]; foreach ( $rule_spec_list as $id => $rule_spec ) { $validity = $this->validate_tag_spec_for_node( $node, $rule_spec[ AMP_Rule_Spec::TAG_SPEC ] ); if ( true === $validity ) { @@ -481,7 +479,41 @@ private function process_node( DOMElement $node ) { $validation_errors[0] ); } else { - $this->remove_node( $node ); + $spec_names = wp_list_pluck( $validation_errors, 'spec_name' ); + + $unique_validation_error_count = count( + array_unique( + array_map( + static function ( $validation_error ) { + unset( $validation_error['spec_name'] ); + return json_encode( $validation_error ); // phpcs:ignore + }, + $validation_errors + ) + ) + ); + + if ( 1 === $unique_validation_error_count ) { + // If all of the validation errors are the same except for the spec_name, use the common error code. + $validation_error = $validation_errors[0]; + unset( $validation_error['spec_name'] ); + $this->remove_invalid_child( + $node, + array_merge( + $validation_error, + compact( 'spec_names' ) + ) + ); + } else { + // Otherwise, we have a rare rare condition where multiple tag specs fail for different reasons. + $this->remove_invalid_child( + $node, + [ + 'code' => self::DISALLOWED_TAG_MULTIPLE_CHOICES, + 'errors' => $validation_errors, + ] + ); + } } return null; } diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 1d8e403139f..85a01871e1a 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -41,6 +41,69 @@ public function get_body_data() { [ 'amp-ad' ], ], + 'amp-app-banner-bad-ads' => [ + ' + + + + + + ', + ' + + + + + + ', + [ 'amp-app-banner' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG_ANCESTOR ], + ], + + 'amp-script-nested' => [ + '', + '', + [ 'amp-script' ], + [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG_ANCESTOR ], + ], + + 'bad-svg-stop' => [ + '', + '', + [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::MANDATORY_TAG_ANCESTOR, + 'spec_names' => [ + 'lineargradient > stop', + 'radialgradient > stop', + ], + ], + ], + ], + + 'bad-noscript' => [ + '', + '', + [], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG_MULTIPLE_CHOICES, + 'errors' => [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::WRONG_PARENT_TAG, + 'spec_name' => 'noscript enclosure for boilerplate', + ], + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_TAG_ANCESTOR, + 'disallowed_ancestor' => 'noscript', + 'spec_name' => 'noscript', + ], + ], + ] + ] + ], + 'adsense' => [ '
', null, // No change. @@ -2102,12 +2165,12 @@ public function get_html_data() { [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_HTML_COMMENTS ], ], 'script_cdata_contents_bad' => [ - '', + '', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript '', [], [ AMP_Tag_And_Attribute_Sanitizer::INVALID_CDATA_CONTENTS ], ], - 'cdata_regex_failure' => [ + 'cdata_regex_failure' => [ '', '', [], From 998c45c38e4b87cd8745576bfe486b31f983dafb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 29 Nov 2019 14:06:53 -0800 Subject: [PATCH 34/39] Add test for INCORRECT_NUM_CHILD_TAGS --- tests/php/test-tag-and-attribute-sanitizer.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 85a01871e1a..7e1dd280a33 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -100,8 +100,8 @@ public function get_body_data() { 'spec_name' => 'noscript', ], ], - ] - ] + ], + ], ], 'adsense' => [ @@ -1695,6 +1695,13 @@ static function() { [ AMP_Tag_And_Attribute_Sanitizer::INCORRECT_MIN_NUM_CHILD_TAGS ], ], + 'amp-animation-bad-number-children' => [ + '', + '', + [], + [ AMP_Tag_And_Attribute_Sanitizer::INCORRECT_NUM_CHILD_TAGS ], + ], + 'amp-image-slider-more-bad-children' => [ 'Not allowedforbidden
This apple is green
not allowed
This apple is red
not ok
', '', From 22b1869f0df325dd35498b02f9f08f2dcbe22515 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 29 Nov 2019 14:28:20 -0800 Subject: [PATCH 35/39] Remove redundant validation error data; test for non-redundant data --- .../class-amp-tag-and-attribute-sanitizer.php | 27 ++++++++++--------- .../php/test-tag-and-attribute-sanitizer.php | 26 +++++++++++++++--- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 574c5856620..6f0c4229e58 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -875,7 +875,12 @@ private function validate_tag_spec_for_node( DOMElement $node, $tag_spec ) { return true; } - return $this->check_valid_children( $node, $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ); + $validity = $this->check_valid_children( $node, $tag_spec[ AMP_Rule_Spec::CHILD_TAGS ] ); + if ( true !== $validity ) { + $validity['tag_spec'] = $this->get_spec_name( $node, $tag_spec ); + return $validity; + } + return true; } /** @@ -1897,9 +1902,8 @@ private function check_valid_children( DOMElement $node, $child_tags ) { // If the first element is not of the required type, invalidate the entire element. if ( isset( $child_tags['first_child_tag_name_oneof'] ) && ! empty( $child_elements[0] ) && ! in_array( $child_elements[0]->nodeName, $child_tags['first_child_tag_name_oneof'], true ) ) { return [ - 'code' => self::DISALLOWED_FIRST_CHILD_TAG, - 'first_child_tag' => $child_elements[0]->nodeName, - 'first_child_tag_name_oneof' => $child_tags['first_child_tag_name_oneof'], // @todo This is not needed with the spec_name in hand. + 'code' => self::DISALLOWED_FIRST_CHILD_TAG, + 'first_child_tag' => $child_elements[0]->nodeName, ]; } @@ -1908,9 +1912,8 @@ private function check_valid_children( DOMElement $node, $child_tags ) { foreach ( $child_elements as $child_element ) { if ( ! in_array( $child_element->nodeName, $child_tags['child_tag_name_oneof'], true ) ) { return [ - 'code' => self::DISALLOWED_CHILD_TAG, - 'child_tag' => $child_element->nodeName, - 'child_tag_name_oneof' => $child_tags['child_tag_name_oneof'], // @todo This is not needed with the spec_name in hand. + 'code' => self::DISALLOWED_CHILD_TAG, + 'child_tag' => $child_element->nodeName, ]; } } @@ -1923,9 +1926,8 @@ private function check_valid_children( DOMElement $node, $child_tags ) { return true; } else { return [ - 'code' => self::INCORRECT_NUM_CHILD_TAGS, - 'children_count' => $child_element_count, - 'mandatory_num_child_tags' => $child_tags['mandatory_num_child_tags'], // @todo This is not needed with the spec_name in hand. + 'code' => self::INCORRECT_NUM_CHILD_TAGS, + 'children_count' => $child_element_count, ]; } } @@ -1937,9 +1939,8 @@ private function check_valid_children( DOMElement $node, $child_tags ) { return true; } else { return [ - 'code' => self::INCORRECT_MIN_NUM_CHILD_TAGS, - 'children_count' => $child_element_count, - 'mandatory_min_num_child_tags' => $child_tags['mandatory_min_num_child_tags'], // @todo This is not needed with the spec_name in hand. + 'code' => self::INCORRECT_MIN_NUM_CHILD_TAGS, + 'children_count' => $child_element_count, ]; } } diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index 7e1dd280a33..b084219d577 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -1326,7 +1326,10 @@ static function() { '', [], [ - AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_FIRST_CHILD_TAG, + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_FIRST_CHILD_TAG, + 'first_child_tag' => 'i', + ], ], ], @@ -1692,21 +1695,36 @@ static function() { '', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::INCORRECT_MIN_NUM_CHILD_TAGS ], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INCORRECT_MIN_NUM_CHILD_TAGS, + 'children_count' => 1, + ], + ], ], 'amp-animation-bad-number-children' => [ '', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::INCORRECT_NUM_CHILD_TAGS ], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::INCORRECT_NUM_CHILD_TAGS, + 'children_count' => 2, + ], + ], ], 'amp-image-slider-more-bad-children' => [ 'Not allowedforbidden
This apple is green
not allowed
This apple is red
not ok
', '', [], - [ AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_CHILD_TAG ], + [ + [ + 'code' => AMP_Tag_And_Attribute_Sanitizer::DISALLOWED_CHILD_TAG, + 'child_tag' => 'span', + ], + ], ], 'amp-fx-collection' => [ From 322035bafc6645ebb50a4aad4ac0e277425b433f Mon Sep 17 00:00:00 2001 From: Alain Schlesser Date: Tue, 3 Dec 2019 16:37:35 +0100 Subject: [PATCH 36/39] Bring sanity to the code --- includes/sanitizers/class-amp-base-sanitizer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 5db01006b83..b93da28fd99 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -82,7 +82,7 @@ abstract class AMP_Base_Sanitizer { * * @var array */ - private $should_not_removed_nodes = []; + private $nodes_to_keep = []; /** * AMP_Base_Sanitizer constructor. @@ -439,7 +439,7 @@ public function remove_invalid_child( $node, $validation_error = [] ) { } // Prevent double-reporting nodes that are rejected for sanitization. - if ( isset( $this->should_not_removed_nodes[ $node->nodeName ] ) && in_array( $node, $this->should_not_removed_nodes[ $node->nodeName ], true ) ) { + if ( isset( $this->nodes_to_keep[ $node->nodeName ] ) && in_array( $node, $this->nodes_to_keep[ $node->nodeName ], true ) ) { return false; } @@ -447,7 +447,7 @@ public function remove_invalid_child( $node, $validation_error = [] ) { if ( $should_remove ) { $node->parentNode->removeChild( $node ); } else { - $this->should_not_removed_nodes[ $node->nodeName ][] = $node; + $this->nodes_to_keep[ $node->nodeName ][] = $node; } return $should_remove; } From b96e4e7cbd81521b40319a9974961d6184a32334 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Dec 2019 19:40:10 +0700 Subject: [PATCH 37/39] Fix typos in comments and code style Co-Authored-By: Alain Schlesser --- .../class-amp-tag-and-attribute-sanitizer.php | 10 +++++----- ...amp-tag-and-attribute-sanitizer-private-methods.php | 2 +- tests/php/test-tag-and-attribute-sanitizer.php | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index 6f0c4229e58..cb29836df72 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -505,7 +505,7 @@ static function ( $validation_error ) { ) ); } else { - // Otherwise, we have a rare rare condition where multiple tag specs fail for different reasons. + // Otherwise, we have a rare condition where multiple tag specs fail for different reasons. $this->remove_invalid_child( $node, [ @@ -1102,10 +1102,10 @@ private function sanitize_disallowed_attribute_values_in_node( DOMElement $node, * - check_attr_spec_rule_value_casei * * They have already been checked because the tag spec should only be considered a candidate for a given - * node if if passes those checks, that is, if the shape of the node matches the spec close enough. + * node if it passes those checks, that is, if the shape of the node matches the spec close enough. * - * However, if there was only one spec for a given tag, then then validate_attr_spec_list_for_node() would - * not have been called. and thus these checks need to be performed here as well. + * However, if there was only one spec for a given tag, then the validate_attr_spec_list_for_node() would + * not have been called, and thus these checks need to be performed here as well. */ if ( isset( $attr_spec_rule[ AMP_Rule_Spec::VALUE ] ) && AMP_Rule_Spec::FAIL === $this->check_attr_spec_rule_value( $node, $attr_name, $attr_spec_rule ) ) { @@ -1584,7 +1584,7 @@ private function check_attr_spec_rule_disallowed_empty( DOMElement $node, $attr_ } /** - * Check if attribute has blacklisted value via regex match determine if value matches. + * Check if attribute has blacklisted value via regex match and determine if value matches. * * @since 0.5 * diff --git a/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php b/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php index 6bcb42d1e7e..79e90d603d9 100644 --- a/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php +++ b/tests/php/test-amp-tag-and-attribute-sanitizer-private-methods.php @@ -1033,7 +1033,7 @@ public function get_validate_attr_spec_list_for_node_data() { ], ], ], - 2, // Allow empty is not not used until sanitization. + 2, // Allow empty is not used until sanitization. ], 'attributes_blacklisted_regex' => [ [ diff --git a/tests/php/test-tag-and-attribute-sanitizer.php b/tests/php/test-tag-and-attribute-sanitizer.php index b084219d577..163fcd3db27 100644 --- a/tests/php/test-tag-and-attribute-sanitizer.php +++ b/tests/php/test-tag-and-attribute-sanitizer.php @@ -2394,7 +2394,7 @@ public function test_is_missing_mandatory_attribute() { ], 'noloading' => [], ]; - $dom = new DomDocument(); + $dom = new DOMDocument(); $node = new DOMElement( 'amp-gist' ); $dom->appendChild( $node ); $sanitizer = new AMP_Tag_And_Attribute_Sanitizer( $dom ); From 827659a285e5334a232aa3cb5f0c0691143f7bc1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Dec 2019 23:02:31 +0700 Subject: [PATCH 38/39] Harmonize logic for getting stylesheet by URL Remove STYLESHEET_INVALID_FILE_PATH code and always fall-back to HTTP request when file not on file system --- .../sanitizers/class-amp-style-sanitizer.php | 97 +++++++------------ .../class-amp-validation-error-taxonomy.php | 1 - tests/php/test-amp-style-sanitizer.php | 4 +- 3 files changed, 39 insertions(+), 63 deletions(-) diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 1609278b0dc..5cb9ccead3a 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -40,7 +40,6 @@ class AMP_Style_Sanitizer extends AMP_Base_Sanitizer { const CSS_SYNTAX_PARSE_ERROR = 'CSS_SYNTAX_PARSE_ERROR'; const STYLESHEET_TOO_LONG = 'STYLESHEET_TOO_LONG'; const STYLESHEET_INVALID_FILE_URL = 'STYLESHEET_INVALID_FILE_URL'; - const STYLESHEET_INVALID_FILE_PATH = 'STYLESHEET_INVALID_FILE_PATH'; // These are internal to the sanitizer and not exposed as validation error codes. const STYLESHEET_DISALLOWED_FILE_EXT = 'STYLESHEET_DISALLOWED_FILE_EXT'; @@ -350,7 +349,6 @@ public static function get_css_parser_validation_error_codes() { self::CSS_SYNTAX_INVALID_PROPERTY, self::CSS_SYNTAX_INVALID_PROPERTY_NOLIST, self::CSS_SYNTAX_PARSE_ERROR, - self::STYLESHEET_INVALID_FILE_PATH, self::STYLESHEET_INVALID_FILE_URL, self::STYLESHEET_TOO_LONG, ]; @@ -1290,37 +1288,18 @@ private function process_link_element( DOMElement $element ) { return; } - $css_file_path = $this->get_validated_url_file_path( $href, [ 'css', 'less', 'scss', 'sass' ] ); - if ( ! is_wp_error( $css_file_path ) ) { - $stylesheet = file_get_contents( $css_file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. - if ( false === $stylesheet ) { - $this->remove_invalid_child( - $element, - [ - // @todo Also include details about the error. - 'code' => self::STYLESHEET_INVALID_FILE_PATH, - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - ] - ); - return; - } - } else { - // Fall back to doing an HTTP request for the stylesheet is not accessible directly from the filesystem. - $contents = $this->fetch_external_stylesheet( $normalized_url ); - if ( ! is_wp_error( $contents ) ) { - $stylesheet = $contents; - } else { - $this->remove_invalid_child( - $element, - [ - // @todo Also include details about the error. - 'code' => self::STYLESHEET_INVALID_FILE_URL, - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - 'url' => $normalized_url, - ] - ); - return; - } + $stylesheet = $this->get_stylesheet_from_url( $href ); + if ( $stylesheet instanceof WP_Error ) { + $this->remove_invalid_child( + $element, + [ + // @todo Also include details about the error. + 'code' => self::STYLESHEET_INVALID_FILE_URL, + 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, + 'url' => $normalized_url, + ] + ); + return; } // Honor the link's media attribute. @@ -1337,7 +1316,6 @@ private function process_link_element( DOMElement $element ) { 'allowed_at_rules' => $this->style_custom_cdata_spec['css_spec']['allowed_at_rules'], 'property_whitelist' => $this->style_custom_cdata_spec['css_spec']['declaration'], 'stylesheet_url' => $href, - 'stylesheet_path' => $css_file_path, 'spec_name' => self::STYLE_AMP_CUSTOM_SPEC_NAME, ] ); @@ -1358,6 +1336,28 @@ private function process_link_element( DOMElement $element ) { $this->set_current_node( null ); } + /** + * Get stylesheet from URL. + * + * @since 1.5.0 + * + * @param string $stylesheet_url Stylesheet URL. + * @return string|WP_Error Stylesheet string on success, or WP_Error on failure. + */ + private function get_stylesheet_from_url( $stylesheet_url ) { + $stylesheet = false; + $css_file_path = $this->get_validated_url_file_path( $stylesheet_url, [ 'css', 'less', 'scss', 'sass' ] ); + if ( ! is_wp_error( $css_file_path ) ) { + $stylesheet = file_get_contents( $css_file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. + } + if ( is_string( $stylesheet ) ) { + return $stylesheet; + } + + // Fall back to doing an HTTP request for the stylesheet is not accessible directly from the filesystem. + return $this->fetch_external_stylesheet( $stylesheet_url ); + } + /** * Fetch external stylesheet. * @@ -1412,7 +1412,6 @@ private function fetch_external_stylesheet( $url ) { * @type string[] $property_whitelist Exclusively-allowed properties. * @type string[] $property_blacklist Disallowed properties. * @type string $stylesheet_url Original URL for stylesheet when originating via link or @import. - * @type string $stylesheet_path Original filesystem path for stylesheet when originating via link or @import. * @type array $allowed_at_rules Allowed @-rules. * @type bool $validate_keyframes Whether keyframes should be validated. * @type string $spec_name Spec name. @@ -1538,30 +1537,11 @@ private function parse_import_stylesheet( Import $item, CSSList $css_list, $opti return []; } - $css_file_path = $this->get_validated_url_file_path( $import_stylesheet_url, [ 'css', 'less', 'scss', 'sass' ] ); - - if ( is_wp_error( $css_file_path ) && ( self::STYLESHEET_DISALLOWED_FILE_EXT === $css_file_path->get_error_code() || self::STYLESHEET_EXTERNAL_FILE_URL === $css_file_path->get_error_code() ) ) { - $contents = $this->fetch_external_stylesheet( $import_stylesheet_url ); - if ( is_wp_error( $contents ) ) { - $error = [ - // @todo Also include details about the error. - 'code' => self::STYLESHEET_INVALID_FILE_URL, - 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, - 'url' => $import_stylesheet_url, - ]; - $sanitized = $this->should_sanitize_validation_error( $error ); - if ( $sanitized ) { - $css_list->remove( $item ); - } - $results[] = compact( 'error', 'sanitized' ); - return $results; - } - - $stylesheet = $contents; - } elseif ( is_wp_error( $css_file_path ) ) { + $stylesheet = $this->get_stylesheet_from_url( $import_stylesheet_url ); + if ( $stylesheet instanceof WP_Error ) { $error = [ // @todo Also include details about the error. - 'code' => self::STYLESHEET_INVALID_FILE_PATH, + 'code' => self::STYLESHEET_INVALID_FILE_URL, 'type' => AMP_Validation_Error_Taxonomy::CSS_ERROR_TYPE, 'url' => $import_stylesheet_url, ]; @@ -1571,8 +1551,6 @@ private function parse_import_stylesheet( Import $item, CSSList $css_list, $opti } $results[] = compact( 'error', 'sanitized' ); return $results; - } else { - $stylesheet = file_get_contents( $css_file_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. } if ( $media_query ) { @@ -1705,7 +1683,6 @@ private function prepare_stylesheet( $stylesheet_string, $options = [] ) { 'property_whitelist' => [], 'validate_keyframes' => false, 'stylesheet_url' => null, - 'stylesheet_path' => null, 'spec_name' => null, ], $options diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index fa5de9464c1..c1ad2150511 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -2912,7 +2912,6 @@ public static function get_error_title_from_code( $validation_error ) { case AMP_Style_Sanitizer::CSS_SYNTAX_PARSE_ERROR: return esc_html__( 'CSS parse error', 'amp' ); case AMP_Style_Sanitizer::STYLESHEET_INVALID_FILE_URL: - case AMP_Style_Sanitizer::STYLESHEET_INVALID_FILE_PATH: return esc_html__( 'Missing stylesheet file', 'amp' ); case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY: case AMP_Style_Sanitizer::CSS_SYNTAX_INVALID_PROPERTY_NOLIST: diff --git a/tests/php/test-amp-style-sanitizer.php b/tests/php/test-amp-style-sanitizer.php index 08f8881318c..b8441e8ee62 100644 --- a/tests/php/test-amp-style-sanitizer.php +++ b/tests/php/test-amp-style-sanitizer.php @@ -1959,7 +1959,7 @@ static function ( WP_UnitTestCase $test, $stylesheet ) { 'https://bogus.example.com/remote-also-does-not-exist.css', ], '
', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet - 3, // Three HTTP requests (to bogus.example.com). The local-does-not-exist.css checks filesystem directly. + 4, // All four result in HTTP requests, even the local one because it doesn't exist on the filesystem. static function ( $requested_url ) { if ( false !== strpos( $requested_url, 'does-not-exist' ) ) { return new WP_Error( 'does_not_exist' ); @@ -1999,7 +1999,7 @@ static function ( WP_UnitTestCase $test, $stylesheet ) { 'https://bogus.example.com/remote-also-does-not-exist.css', ], '
', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet - 3, // Three HTTP requests (to bogus.example.com). The local-does-not-exist.css checks filesystem directly. + 4, // All four result in HTTP requests, even the local one because it doesn't exist on the filesystem. static function ( $requested_url ) { if ( false !== strpos( $requested_url, 'does-not-exist' ) ) { return new WP_Error( 'does_not_exist' ); From f7d00f97d4ec4205d6b2aa60412ec9eab43ee381 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Dec 2019 23:15:15 +0700 Subject: [PATCH 39/39] Use SORT_REGULAR flag for array_unique() instead of serializing arrays as strings Co-Authored-By: Alain Schlesser --- .../sanitizers/class-amp-tag-and-attribute-sanitizer.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index cb29836df72..788606abc7f 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -486,10 +486,11 @@ private function process_node( DOMElement $node ) { array_map( static function ( $validation_error ) { unset( $validation_error['spec_name'] ); - return json_encode( $validation_error ); // phpcs:ignore + return $validation_error; }, $validation_errors - ) + ), + SORT_REGULAR ) );