DEFAULT_ARGS = [ 'amp_allowed_tags' => AMP_Allowed_Tags_Generated::get_allowed_tags(), 'amp_globally_allowed_attributes' => AMP_Allowed_Tags_Generated::get_allowed_attributes(), 'amp_layout_allowed_attributes' => AMP_Allowed_Tags_Generated::get_layout_attributes(), 'prefer_bento' => false, ]; parent::__construct( $dom, $args ); // Prepare allowlists. $this->allowed_tags = $this->args['amp_allowed_tags']; foreach ( AMP_Rule_Spec::$additional_allowed_tags as $tag_name => $tag_rule_spec ) { $this->allowed_tags[ $tag_name ][] = $tag_rule_spec; } // @todo Do the same for body when !use_document_element? if ( ! empty( $this->args['use_document_element'] ) ) { foreach ( $this->allowed_tags['html'] as &$rule_spec ) { unset( $rule_spec[ AMP_Rule_Spec::TAG_SPEC ][ AMP_Rule_Spec::MANDATORY_PARENT ] ); } unset( $rule_spec ); } foreach ( $this->allowed_tags as &$tag_specs ) { foreach ( $tag_specs as &$tag_spec ) { if ( isset( $tag_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ) ) { $tag_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] = $this->process_alternate_names( $tag_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ); } } unset( $tag_spec ); } unset( $tag_specs ); $this->globally_allowed_attributes = $this->process_alternate_names( $this->args['amp_globally_allowed_attributes'] ); $this->layout_allowed_attributes = $this->process_alternate_names( $this->args['amp_layout_allowed_attributes'] ); } /** * Return array of values that would be valid as an HTML `script` element. * * Array keys are AMP element names and array values are their respective * Javascript URLs from https://cdn.ampproject.org * * @since 0.7 * @see amp_register_default_scripts() * * @return array() Returns component name as array key and true as value (or JavaScript URL string), * respectively. When true then the default component script URL will be used. * Will return an empty array if sanitization has yet to be run * or if it did not find any HTML elements to convert to AMP equivalents. */ public function get_scripts() { return array_fill_keys( array_unique( $this->script_components ), true ); } /** * Process alternative names in attribute spec list. * * @since 0.7 * * @param array $attr_spec_list Attribute spec list. * @return array Modified attribute spec list. */ private function process_alternate_names( $attr_spec_list ) { foreach ( $attr_spec_list as $attr_name => &$attr_spec ) { // Save all alternative names in lookup to improve performance. if ( isset( $attr_spec[ AMP_Rule_Spec::ALTERNATIVE_NAMES ] ) ) { foreach ( $attr_spec[ AMP_Rule_Spec::ALTERNATIVE_NAMES ] as $alternative_name ) { $this->rev_alternate_attr_name_lookup[ $alternative_name ] = $attr_name; } } } return $attr_spec_list; } /** * Sanitize the elements from the HTML contained in this instance's Dom\Document. * * @since 0.5 */ public function sanitize() { $result = $this->sanitize_element( $this->root_element ); if ( is_array( $result ) ) { $this->script_components = $result; } } /** * Sanitize element. * * Walk the DOM tree with depth first search (DFS) with post order traversal (LRN). * * @param DOMElement $element Element. * @return string[]|null Required component scripts from sanitizing an element tree, or null if the element was removed. */ private function sanitize_element( DOMElement $element ) { if ( ! isset( $this->open_elements[ $element->nodeName ] ) ) { $this->open_elements[ $element->nodeName ] = 0; } $this->open_elements[ $element->nodeName ]++; $script_components = []; // First recurse into children to sanitize descendants. // The check for $element->parentNode at each iteration is to make sure an invalid child didn't bubble up removed // ancestor nodes in AMP_Tag_And_Attribute_Sanitizer::remove_node(). $this_child = $element->firstChild; while ( $this_child && $element->parentNode ) { $prev_child = $this_child->previousSibling; $next_child = $this_child->nextSibling; if ( $this_child instanceof DOMElement ) { $result = $this->sanitize_element( $this_child ); if ( is_array( $result ) ) { $script_components = array_merge( $script_components, $result ); } } elseif ( $this_child instanceof DOMProcessingInstruction ) { $this->remove_invalid_child( $this_child, [ 'code' => self::DISALLOWED_PROCESSING_INSTRUCTION ] ); } if ( ! $this_child->parentNode ) { // Handle case where this child is replaced with children. $this_child = $prev_child ? $prev_child->nextSibling : $element->firstChild; } else { $this_child = $next_child; } } // If the element is still in the tree, process it. // The element can currently be removed from the tree when processing children via AMP_Tag_And_Attribute_Sanitizer::remove_node(). $was_removed = false; if ( $element->parentNode ) { $result = $this->process_node( $element ); if ( is_array( $result ) ) { $script_components = array_merge( $script_components, $result ); } else { $was_removed = true; } } $this->open_elements[ $element->nodeName ]--; if ( $was_removed ) { return null; } return $script_components; } /** * Augment rule spec for validation. * * @since 1.0 * * @param DOMElement $node Node. * @param array $rule_spec Rule spec. * @return array Augmented rule spec. */ private function get_rule_spec_list_to_validate( DOMElement $node, $rule_spec ) { // Expand extension_spec into a set of attr_spec_list. if ( isset( $rule_spec[ AMP_Rule_Spec::TAG_SPEC ]['extension_spec'] ) ) { $extension_spec = $rule_spec[ AMP_Rule_Spec::TAG_SPEC ]['extension_spec']; // This could also be derived from the extension_type in the extension_spec. $custom_attr = 'amp-mustache' === $extension_spec['name'] ? 'custom-template' : 'custom-element'; $rule_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ][ $custom_attr ] = [ AMP_Rule_Spec::VALUE => $extension_spec['name'], AMP_Rule_Spec::MANDATORY => true, ]; $rule_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ]['src'] = [ AMP_Rule_Spec::VALUE_REGEX => implode( '', [ '^', preg_quote( 'https://cdn.ampproject.org/v0/' . $extension_spec['name'] . '-', '/' ), '(' . implode( '|', array_merge( $extension_spec['version'], [ 'latest' ] ) ) . ')', '\.js$', ] ), ]; } // Augment the attribute list according to the parent's reference points, if it has them. if ( ! empty( $node->parentNode ) && isset( $this->allowed_tags[ $node->parentNode->nodeName ] ) ) { foreach ( $this->allowed_tags[ $node->parentNode->nodeName ] as $parent_rule_spec ) { if ( empty( $parent_rule_spec[ AMP_Rule_Spec::TAG_SPEC ]['reference_points'] ) ) { continue; } foreach ( $parent_rule_spec[ AMP_Rule_Spec::TAG_SPEC ]['reference_points'] as $reference_point_spec_name => $reference_point_spec_instance_attrs ) { $reference_point = AMP_Allowed_Tags_Generated::get_reference_point_spec( $reference_point_spec_name ); if ( empty( $reference_point[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ) ) { /* * See special case for amp-selector in AMP_Tag_And_Attribute_Sanitizer::is_amp_allowed_attribute() * where its reference point applies to any descendant elements, not just direct children. */ continue; } foreach ( $reference_point[ AMP_Rule_Spec::ATTR_SPEC_LIST ] as $attr_name => $reference_point_spec_attr ) { $reference_point_spec_attr = array_merge( $reference_point_spec_attr, $reference_point_spec_instance_attrs ); /* * Ignore mandatory constraint for now since this would end up causing other sibling children * getting removed due to missing a mandatory attribute. To sanitize this it would require * higher-level processing to look at an element's surrounding context, similar to how the * sanitizer does not yet handle the mandatory_oneof constraint. */ unset( $reference_point_spec_attr['mandatory'] ); $rule_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ][ $attr_name ] = $reference_point_spec_attr; } } } } return $rule_spec; } /** * Process a node by checking if an element and its attributes are valid, and removing them when invalid. * * Attributes which are not valid are removed. Elements which are not allowed are also removed, * including elements which miss mandatory attributes. * * @param DOMElement $node Node. * @return string[]|null Required scripts, or null if the element was removed. */ private function process_node( DOMElement $node ) { // Remove nodes with tags that have not been put in the allowlist. if ( ! $this->is_amp_allowed_tag( $node ) ) { // If it's not an allowed tag, replace the node with it's children. $this->replace_node_with_children( $node ); // Return early since we don't know anything about this node to validate it. return null; } /* * Compile a list of rule_specs to validate for this node * based on tag name of the node. */ $rule_spec_list_to_validate = []; $validation_errors = []; $rule_spec_list = $this->allowed_tags[ $node->nodeName ]; foreach ( $rule_spec_list as $id => $rule_spec ) { // When there are multiple versions of a rule spec, with one specifically for Bento and another for // non-Bento make sure that only the preferred version is considered. Otherwise, the wrong requires_extension // constraint may be applied. if ( isset( $rule_spec['tag_spec']['bento'] ) && $this->args['prefer_bento'] !== $rule_spec['tag_spec']['bento'] ) { continue; } $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 { $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( $validation_errors ) ) { // If there was only one tag spec candidate that failed, use its error code for removing the node, // 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, $validation_errors[0] ); } else { $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'], // Remove other keys that may make the error unique. $validation_error['required_parent_name'], $validation_error['required_ancestor_name'], $validation_error['required_child_count'], $validation_error['required_min_child_count'], $validation_error['required_attr_value'] ); return $validation_error; }, $validation_errors ), SORT_REGULAR ) ); 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 condition where multiple tag specs fail for different reasons. foreach ( $validation_errors as $validation_error ) { if ( true === $this->remove_invalid_child( $node, $validation_error ) ) { break; // Once removed, ignore remaining errors. } } } } return null; } // The remaining validations all have to do with attributes. $attr_spec_list = []; $tag_spec = []; $cdata = []; /* * If we have exactly one rule_spec, use it's attr_spec_list * to validate the node's attributes. */ if ( 1 === count( $rule_spec_list_to_validate ) ) { $rule_spec = array_pop( $rule_spec_list_to_validate ); $attr_spec_list = $rule_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ]; $tag_spec = $rule_spec[ AMP_Rule_Spec::TAG_SPEC ]; if ( isset( $rule_spec[ AMP_Rule_Spec::CDATA ] ) ) { $cdata = $rule_spec[ AMP_Rule_Spec::CDATA ]; } } else { /* * If there is more than one valid rule_spec for this node, * then try to deduce which one is intended by inspecting * the node's attributes. */ /* * Get a score from each attr_spec_list by seeing how many * attributes and values match the node. */ $attr_spec_scores = []; foreach ( $rule_spec_list_to_validate as $spec_id => $rule_spec ) { $attr_spec_scores[ $spec_id ] = $this->validate_attr_spec_list_for_node( $node, $rule_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ); } // Remove all spec lists that didn't match. $attr_spec_scores = array_filter( $attr_spec_scores ); // If no attribute spec lists match, then the element must be removed. if ( empty( $attr_spec_scores ) ) { $this->remove_node( $node ); return null; } // Get the key(s) to the highest score(s). $spec_ids_sorted = array_keys( $attr_spec_scores, max( $attr_spec_scores ), true ); // If there is exactly one attr_spec with a max score, use that one. if ( 1 === count( $spec_ids_sorted ) ) { $attr_spec_list = $rule_spec_list_to_validate[ $spec_ids_sorted[0] ][ AMP_Rule_Spec::ATTR_SPEC_LIST ]; $tag_spec = $rule_spec_list_to_validate[ $spec_ids_sorted[0] ][ AMP_Rule_Spec::TAG_SPEC ]; if ( isset( $rule_spec_list_to_validate[ $spec_ids_sorted[0] ][ AMP_Rule_Spec::CDATA ] ) ) { $cdata = $rule_spec_list_to_validate[ $spec_ids_sorted[0] ][ AMP_Rule_Spec::CDATA ]; } } else { // This should not happen very often, but... // 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 ) { $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 ); if ( empty( $attr_spec_list ) && isset( $first_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ] ) ) { $attr_spec_list = $first_spec[ AMP_Rule_Spec::ATTR_SPEC_LIST ]; } } } $attr_spec_list = array_merge( $this->globally_allowed_attributes, $attr_spec_list ); // Remove element if it has illegal CDATA. if ( ! empty( $cdata ) && $node instanceof DOMElement ) { $validity = $this->validate_cdata_for_node( $node, $cdata ); if ( true !== $validity ) { $sanitized = $this->remove_invalid_child( $node, array_merge( $validity, [ 'spec_name' => $this->get_spec_name( $node, $tag_spec ) ] ) ); return $sanitized ? null : $this->get_required_script_components( $node, $tag_spec, $attr_spec_list ); } } // Amend spec list with layout. if ( isset( $tag_spec['amp_layout'] ) ) { $attr_spec_list = array_merge( $attr_spec_list, $this->layout_allowed_attributes ); if ( isset( $tag_spec['amp_layout']['supported_layouts'] ) ) { $layouts = wp_array_slice_assoc( Layout::FROM_SPEC, $tag_spec['amp_layout']['supported_layouts'] ); $attr_spec_list['layout'][ AMP_Rule_Spec::VALUE_REGEX_CASEI ] = '(' . implode( '|', $layouts ) . ')'; } } // Enforce unique constraint. if ( ! empty( $tag_spec['unique'] ) ) { $removed = false; $tag_spec_key = wp_json_encode( $tag_spec ); if ( ! empty( $this->visited_unique_tag_specs[ $node->nodeName ][ $tag_spec_key ] ) ) { $removed = $this->remove_invalid_child( $node, [ '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; if ( $removed ) { return null; } } // Remove the element if it is has an invalid layout. $layout_validity = $this->is_valid_layout( $tag_spec, $node ); if ( true !== $layout_validity ) { $sanitized = $this->remove_invalid_child( $node, $layout_validity ); return $sanitized ? null : $this->get_required_script_components( $node, $tag_spec, $attr_spec_list ); } // Identify attribute values that don't conform to the attr_spec. $disallowed_attributes = $this->sanitize_disallowed_attribute_values_in_node( $node, $attr_spec_list ); // Remove all invalid attributes. if ( ! empty( $disallowed_attributes ) ) { /* * Capture all element attributes up front so that differing validation errors result when * one invalid attribute is accepted but the others are still rejected. */ $element_attributes = []; foreach ( $node->attributes as $attribute ) { $element_attributes[ $attribute->nodeName ] = $attribute->nodeValue; } $removed_attributes = []; foreach ( $disallowed_attributes as $disallowed_attribute ) { /** * Returned vars. * * @var DOMAttr $attr_node * @var string $error_code * @var array $error_data */ list( $attr_node, $error_code, $error_data ) = $disallowed_attribute; $validation_error = [ 'code' => $error_code, 'element_attributes' => $element_attributes, ]; if ( self::DISALLOWED_PROPERTY_IN_ATTR_VALUE === $error_code ) { $properties = $this->parse_properties_attribute( $attr_node->nodeValue ); $validation_error['meta_property_name'] = $error_data['name']; if ( ! $this->is_empty_attribute_value( $properties[ $error_data['name'] ] ) ) { $validation_error['meta_property_value'] = $properties[ $error_data['name'] ]; } if ( $this->should_sanitize_validation_error( $validation_error, [ 'node' => $attr_node ] ) ) { unset( $properties[ $error_data['name'] ] ); $node->setAttribute( $attr_node->nodeName, $this->serialize_properties_attribute( $properties ) ); } } elseif ( self::MISSING_REQUIRED_PROPERTY_VALUE === $error_code ) { $validation_error['meta_property_name'] = $error_data['name']; $validation_error['meta_property_value'] = $error_data['value']; $validation_error['meta_property_required_value'] = $error_data['required_value']; if ( $this->should_sanitize_validation_error( $validation_error, [ 'node' => $attr_node ] ) ) { $properties = $this->parse_properties_attribute( $attr_node->nodeValue ); if ( ! empty( $attr_spec_list[ $attr_node->nodeName ]['value_properties'][ $error_data['name'] ]['mandatory'] ) ) { $properties[ $error_data['name'] ] = $error_data['required_value']; } else { unset( $properties[ $error_data['name'] ] ); } $node->setAttribute( $attr_node->nodeName, $this->serialize_properties_attribute( $properties ) ); } } elseif ( self::MISSING_MANDATORY_PROPERTY === $error_code ) { $validation_error['meta_property_name'] = $error_data['name']; $validation_error['meta_property_required_value'] = $error_data['required_value']; if ( $this->should_sanitize_validation_error( $validation_error, [ 'node' => $attr_node ] ) ) { $properties = array_merge( $this->parse_properties_attribute( $attr_node->nodeValue ), [ $error_data['name'] => $error_data['required_value'] ] ); $node->setAttribute( $attr_node->nodeName, $this->serialize_properties_attribute( $properties ) ); } } else { $attr_spec = isset( $attr_spec_list[ $attr_node->nodeName ] ) ? $attr_spec_list[ $attr_node->nodeName ] : []; if ( $this->remove_invalid_attribute( $node, $attr_node, $validation_error, $attr_spec ) ) { $removed_attributes[] = $attr_node; } } } /* * Only run cleanup after the fact to prevent a scenario where invalid markup is kept and so the attribute * is actually not removed. This prevents a "DOMException: Not Found Error" from happening when calling * remove_invalid_attribute() since clean_up_after_attribute_removal() can end up removing invalid link * attributes (like 'target') when there is an invalid 'href' attribute, but if the 'target' attribute is * itself invalid, then if clean_up_after_attribute_removal() is called inside of remove_invalid_attribute() * it can cause a subsequent invocation of remove_invalid_attribute() to try to remove an invalid * attribute that has already been removed from the DOM. */ foreach ( $removed_attributes as $removed_attribute ) { $this->clean_up_after_attribute_removal( $node, $removed_attribute ); } } 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->get_spec_name( $node, $tag_spec ) ); } } // 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( $attr_spec_list, $node ); if ( ! empty( $missing_mandatory_attributes ) ) { $sanitized = $this->remove_invalid_child( $node, [ 'code' => self::ATTR_REQUIRED_BUT_MISSING, 'attributes' => $missing_mandatory_attributes, 'spec_name' => $this->get_spec_name( $node, $tag_spec ), ] ); return $sanitized ? null : $this->get_required_script_components( $node, $tag_spec, $attr_spec_list ); } if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_ANYOF ] ) ) { $anyof_attributes = $this->get_element_attribute_intersection( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_ANYOF ] ); if ( 0 === count( $anyof_attributes ) ) { $sanitized = $this->remove_invalid_child( $node, [ 'code' => self::MANDATORY_ANYOF_ATTR_MISSING, 'mandatory_anyof_attrs' => $tag_spec[ AMP_Rule_Spec::MANDATORY_ANYOF ], // @todo Temporary as value can be looked up via spec name. See https://github.com/ampproject/amp-wp/pull/3817. 'spec_name' => $this->get_spec_name( $node, $tag_spec ), ] ); return $sanitized ? null : $this->get_required_script_components( $node, $tag_spec, $attr_spec_list ); } } if ( ! empty( $tag_spec[ AMP_Rule_Spec::MANDATORY_ONEOF ] ) ) { $oneof_attributes = $this->get_element_attribute_intersection( $node, $tag_spec[ AMP_Rule_Spec::MANDATORY_ONEOF ] ); if ( 0 === count( $oneof_attributes ) ) { $sanitized = $this->remove_invalid_child( $node, [ 'code' => self::MANDATORY_ONEOF_ATTR_MISSING, 'mandatory_oneof_attrs' => $tag_spec[ AMP_Rule_Spec::MANDATORY_ONEOF ], // @todo Temporary as value can be looked up via spec name. See https://github.com/ampproject/amp-wp/pull/3817. 'spec_name' => $this->get_spec_name( $node, $tag_spec ), ] ); return $sanitized ? null : $this->get_required_script_components( $node, $tag_spec, $attr_spec_list ); } elseif ( count( $oneof_attributes ) > 1 ) { $sanitized = $this->remove_invalid_child( $node, [ 'code' => self::DUPLICATE_ONEOF_ATTRS, 'duplicate_oneof_attrs' => $oneof_attributes, 'spec_name' => $this->get_spec_name( $node, $tag_spec ), ] ); return $sanitized ? null : $this->get_required_script_components( $node, $tag_spec, $attr_spec_list ); } } return $this->get_required_script_components( $node, $tag_spec, $attr_spec_list ); } /** * Get required AMP component scripts. * * @param DOMElement $node Element. * @param array $tag_spec Tag spec. * @param array $attr_spec_list Attribute spec list. * @return string[] Script component handles. */ private function get_required_script_components( DOMElement $node, $tag_spec, $attr_spec_list ) { $script_components = []; if ( ! empty( $tag_spec['requires_extension'] ) ) { $script_components = array_merge( $script_components, $tag_spec['requires_extension'] ); } // Add required AMP components for attributes. foreach ( $node->attributes as $attribute ) { if ( isset( $attr_spec_list[ $attribute->nodeName ]['requires_extension'] ) ) { $script_components = array_merge( $script_components, $attr_spec_list[ $attribute->nodeName ]['requires_extension'] ); } } // Manually add components for attributes; this is hard-coded because attributes do not have requires_extension like tags do. See . if ( $node->hasAttribute( 'lightbox' ) ) { $script_components[] = 'amp-lightbox-gallery'; } // Check if element needs amp-bind component. if ( $node instanceof DOMElement && ! in_array( 'amp-bind', $this->script_components, true ) ) { foreach ( $node->attributes as $name => $value ) { if ( Amp::BIND_DATA_ATTR_PREFIX === substr( $name, 0, 14 ) ) { $script_components[] = 'amp-bind'; break; } } } return $script_components; } /** * Whether a node is missing a mandatory attribute. * * @param array $attr_spec The attribute specification. * @param DOMElement $node The DOMElement of the node to check. * @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 ( empty( $attr_spec_rule_value[ AMP_Rule_Spec::MANDATORY ] ) ) { continue; } 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 ) ) { $missing_attributes[] = $attr_name; } } return $missing_attributes; } /** * Validate element for its CDATA. * * @since 0.7 * * @param DOMElement $element Element. * @param array $cdata_spec CDATA. * @return true|array True when valid or error data when invalid. */ private function validate_cdata_for_node( DOMElement $element, $cdata_spec ) { if ( isset( $cdata_spec['max_bytes'] ) && strlen( $element->textContent ) > $cdata_spec['max_bytes'] && // Skip the