OwlCyberSecurity - MANAGER
Edit File: class-amp-iframe-sanitizer.php
<?php /** * Class AMP_Iframe_Sanitizer * * @package AMP */ use AmpProject\AmpWP\ValidationExemption; use AmpProject\DevMode; use AmpProject\Html\Attribute; use AmpProject\Layout; /** * Class AMP_Iframe_Sanitizer * * Converts <iframe> tags to <amp-iframe> * * @internal */ class AMP_Iframe_Sanitizer extends AMP_Base_Sanitizer { use AMP_Noscript_Fallback; /** * Default values for sandboxing IFrame. * * | Sandbox Token | Included | Rationale * |-------------------------------------------|----------|---------- * | `allow-downloads` | Yes | Useful for downloading documents, etc. * | `allow-downloads-without-user-activation` | No | Experimental per MDN. Bad UX. * | `allow-forms` | Yes | For embeds like polls. * | `allow-modals` | Yes | For apps to show `confirm()`, etc. * | `allow-orientation-lock` | Yes | Since we `allowfullscreen`, useful for games, etc. * | `allow-pointer-lock` | Yes | Useful for games. * | `allow-popups` | Yes | To open YouTube video in new window, for example. * | `allow-popups-to-escape-sandbox` | Yes | Useful for ads. * | `allow-presentation` | Yes | To cast YouTube videos, for example. * | `allow-same-origin` | Yes | Removed if iframe is same origin. * | `allow-scripts` | Yes | An iframe's primary use case is custom JS. * | `allow-storage-access-by-user-activation` | No | Experimental per MDN. * | `allow-top-navigation` | No | Poor user experience. * | `allow-top-navigation-by-user-activation` | Yes | Key for clicking `target=_top` links in iframes. * * @since 0.2 * @since 2.0.5 Updated to include majority of other sandbox values which are included by default if sandbox is not provided. * @link https://html.spec.whatwg.org/multipage/iframe-embed-object.html#attr-iframe-sandbox * * @const string */ const SANDBOX_DEFAULTS = 'allow-downloads allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts allow-top-navigation-by-user-activation'; /** * Tag. * * @since 0.2 * * @var string HTML <iframe> tag to identify and replace with AMP version. */ public static $tag = 'iframe'; /** * Default args. * * @var array { * Default args. * * @type bool $add_placeholder Whether to add a placeholder element. * @type bool $add_noscript_fallback Whether to add a noscript fallback. * @type string $current_origin The current origin serving the page. Normally this will be the $_SERVER[HTTP_HOST]. * @type string $alias_origin An alternative origin which can be supplied which is used when encountering same-origin iframes. * @type bool $native_iframe_used Whether an HTML5 iframe element should be used instead of amp-iframe. * } */ protected $DEFAULT_ARGS = [ 'add_placeholder' => false, 'add_noscript_fallback' => true, 'current_origin' => null, 'alias_origin' => null, 'native_iframe_used' => false, ]; /** * Get mapping of HTML selectors to the AMP component selectors which they may be converted into. * * @return array Mapping. */ public function get_selector_conversion_mapping() { if ( $this->args['native_iframe_used'] ) { return []; } return [ 'iframe' => [ 'amp-iframe', ], ]; } /** * Sanitize the <iframe> elements from the HTML contained in this instance's Dom\Document. * * @since 0.2 */ public function sanitize() { $nodes = $this->dom->getElementsByTagName( self::$tag ); $num_nodes = $nodes->length; if ( 0 === $num_nodes ) { return; } if ( $this->args['add_noscript_fallback'] ) { $this->initialize_noscript_allowed_attributes( self::$tag ); } // Ensure origins are normalized. $this->args['current_origin'] = $this->get_origin_from_url( $this->args['current_origin'] ); if ( ! empty( $this->args['alias_origin'] ) ) { $this->args['alias_origin'] = $this->get_origin_from_url( $this->args['alias_origin'] ); } for ( $i = $num_nodes - 1; $i >= 0; $i-- ) { /** * Iframe element. * * @var DOMElement $node */ $node = $nodes->item( $i ); // Skip element if already inside of an AMP element as a noscript fallback, or if it has a dev mode exemption. if ( $this->is_inside_amp_noscript( $node ) || DevMode::hasExemptionForNode( $node ) ) { continue; } // If using native <iframe> instead of converting to <amp-iframe>, just mark the element as being unvalidated. if ( $this->args['native_iframe_used'] ) { ValidationExemption::mark_node_as_px_verified( $node ); continue; } $normalized_attributes = AMP_DOM_Utils::get_node_attributes_as_assoc_array( $node ); $normalized_attributes = $this->set_layout( $normalized_attributes ); $normalized_attributes = $this->normalize_attributes( $normalized_attributes ); /** * If the src doesn't exist, remove the node. Either it never * existed or was invalidated while filtering attributes above. * * @todo: add an arg to allow for a fallback element in this instance (note that filter cannot be used inside a sanitizer). * @see: https://github.com/ampproject/amphtml/issues/2261 */ if ( empty( $normalized_attributes['src'] ) ) { $this->remove_invalid_child( $node, [ 'code' => AMP_Tag_And_Attribute_Sanitizer::ATTR_REQUIRED_BUT_MISSING, 'attributes' => [ 'src' ], 'spec_name' => 'amp-iframe', ] ); continue; } $this->did_convert_elements = true; if ( empty( $normalized_attributes[ Attribute::LAYOUT ] ) && ! empty( $normalized_attributes[ Attribute::HEIGHT ] ) && ! empty( $normalized_attributes[ Attribute::WIDTH ] ) ) { // Set layout to responsive if the iframe is aligned to full width. $figure_node = null; if ( $node->parentNode instanceof DOMElement && 'figure' === $node->parentNode->tagName ) { $figure_node = $node->parentNode; } if ( $node->parentNode->parentNode instanceof DOMElement && 'figure' === $node->parentNode->parentNode->tagName ) { $figure_node = $node->parentNode->parentNode; } if ( ! empty( $this->args['align_wide_support'] ) && $figure_node && preg_match( '/(^|\s)(alignwide|alignfull)(\s|$)/', $figure_node->getAttribute( Attribute::CLASS_ ) ) ) { $normalized_attributes[ Attribute::LAYOUT ] = Layout::RESPONSIVE; } else { $normalized_attributes[ Attribute::LAYOUT ] = Layout::INTRINSIC; } $this->add_or_append_attribute( $normalized_attributes, 'class', 'amp-wp-enforced-sizes' ); } // Remove the ID from the original node so that PHP DOM doesn't fail to set it on the replacement element. $node->removeAttribute( Attribute::ID ); $new_node = AMP_DOM_Utils::create_node( $this->dom, 'amp-iframe', $normalized_attributes ); // Find existing placeholder/overflow. $placeholder_node = null; $overflow_node = null; foreach ( iterator_to_array( $node->childNodes ) as $child ) { if ( ! ( $child instanceof DOMElement ) ) { continue; } if ( $child->hasAttribute( 'placeholder' ) ) { $placeholder_node = $node->removeChild( $child ); } elseif ( $child->hasAttribute( 'overflow' ) ) { $overflow_node = $node->removeChild( $child ); } } // Add placeholder. if ( $placeholder_node || true === $this->args['add_placeholder'] ) { if ( ! $placeholder_node ) { $placeholder_node = $this->build_placeholder(); // @todo Can a better placeholder default be devised? } $new_node->appendChild( $placeholder_node ); } // Add overflow. if ( $new_node->hasAttribute( 'resizable' ) && ! $overflow_node ) { $overflow_node = $this->dom->createElement( 'button' ); $overflow_node->setAttribute( 'overflow', '' ); if ( $node->hasAttribute( 'data-amp-overflow-text' ) ) { $overflow_text = $node->getAttribute( 'data-amp-overflow-text' ); } else { $overflow_text = __( 'Show all', 'amp' ); } $overflow_node->appendChild( $this->dom->createTextNode( $overflow_text ) ); } if ( $overflow_node ) { $new_node->appendChild( $overflow_node ); } $node->parentNode->replaceChild( $new_node, $node ); if ( $this->args['add_noscript_fallback'] ) { $node->setAttribute( 'src', $normalized_attributes['src'] ); // AMP is stricter than HTML5 for this attribute, so make sure we use a normalized value. if ( $node->hasAttribute( 'frameborder' ) ) { $node->setAttribute( 'frameborder', $normalized_attributes['frameborder'] ); } // Preserve original node in noscript for no-JS environments. $this->append_old_node_noscript( $new_node, $node, $this->dom ); } } } /** * Normalize HTML attributes for <amp-iframe> elements. * * @param string[] $attributes { * Attributes. * * @type string $src IFrame URL - Empty if HTTPS required per $this->args['require_https_src'] * @type int $width <iframe> width attribute - Set to numeric value if px or % * @type int $height <iframe> height attribute - Set to numeric value if px or % * @type string $sandbox <iframe> `sandbox` attribute - Pass along if found; default to value of self::SANDBOX_DEFAULTS * @type string $class <iframe> `class` attribute - Pass along if found * @type string $sizes <iframe> `sizes` attribute - Pass along if found * @type string $id <iframe> `id` attribute - Pass along if found * @type int $frameborder <iframe> `frameborder` attribute - Filter to '0' or '1'; default to '0' * @type bool $allowfullscreen <iframe> `allowfullscreen` attribute - Convert 'false' to empty string '' * @type bool $allowtransparency <iframe> `allowtransparency` attribute - Convert 'false' to empty string '' * @type string $type <iframe> `type` attribute - Pass along if value is not `text/html` * } * @return array Returns HTML attributes; normalizes src, dimensions, frameborder, sandbox, allowtransparency and allowfullscreen */ private function normalize_attributes( $attributes ) { $out = []; $remove_allow_same_origin = false; foreach ( $attributes as $name => $value ) { switch ( $name ) { case 'src': // Make the URL absolute since relative URLs are not allowed in amp-iframe. if ( '/' === substr( $value, 0, 1 ) && '/' !== substr( $value, 1, 1 ) ) { $value = untrailingslashit( $this->args['current_origin'] ) . $value; } $value = $this->maybe_enforce_https_src( $value, true ); // Handle case where iframe source origin is the same as the host page's origin. if ( $this->get_origin_from_url( $value ) === $this->args['current_origin'] ) { if ( ! empty( $this->args['alias_origin'] ) ) { $value = preg_replace( '#^\w+://[^/]+#', $this->args['alias_origin'], $value ); } else { $remove_allow_same_origin = true; } } $out[ $name ] = $value; break; case 'width': case 'height': $out[ $name ] = $this->sanitize_dimension( $value, $name ); break; case 'frameborder': $out[ $name ] = $this->sanitize_boolean_digit( $value ); break; case 'allowfullscreen': case 'allowtransparency': if ( 'false' !== strtolower( $value ) ) { $out[ $name ] = ''; } break; case 'mozallowfullscreen': case 'webkitallowfullscreen': // Omit these since amp-iframe will add them if needed if the `allowfullscreen` attribute is present. break; case 'loading': /* * The `amp-iframe` component already does lazy-loading by default; trigger a validation error only * if the value is not `lazy`. */ if ( 'lazy' !== strtolower( $value ) ) { $out[ $name ] = $value; } break; case 'security': /* * Omit the `security` attribute as it now been superseded by the `sandbox` attribute. It is * (apparently) only supported by IE <https://stackoverflow.com/a/20071528>. */ break; case 'marginwidth': case 'marginheight': // These attributes have been obsolete since HTML5. If they have the value `0` they can be omitted. if ( '0' !== $value ) { $out[ $name ] = $value; } break; case 'data-amp-resizable': $out['resizable'] = ''; break; case 'data-amp-overflow-text': // No need to copy. break; case 'type': /* * Omit the `type` attribute if its value is `text/html`. Popular embed providers such as Amazon * Kindle use this non-standard attribute, which is apparently a vestige from usage on <object>. */ if ( 'text/html' !== strtolower( $value ) ) { $out[ $name ] = $value; } break; default: $out[ $name ] = $value; break; } } if ( ! isset( $out['sandbox'] ) ) { $out['sandbox'] = self::SANDBOX_DEFAULTS; } // Remove allow-same-origin from sandbox if required. if ( $remove_allow_same_origin ) { $out['sandbox'] = trim( preg_replace( '/(^|\s)allow-same-origin(\s|$)/', ' ', $out['sandbox'] ) ); } return $out; } /** * Obtain the origin part of a given URL (scheme, host, port). * * @param string $url URL. * @return string|null Origin URL or null if parse failed. */ private function get_origin_from_url( $url ) { $parsed_url = wp_parse_url( $url ); if ( ! isset( $parsed_url['host'] ) ) { return null; } if ( ! isset( $parsed_url['scheme'] ) ) { $parsed_url['scheme'] = wp_parse_url( $this->args['current_origin'], PHP_URL_SCHEME ); } $origin = $parsed_url['scheme'] . '://'; $origin .= $parsed_url['host']; if ( isset( $parsed_url['port'] ) ) { $origin .= ':' . $parsed_url['port']; } return $origin; } /** * Builds a DOMElement to use as a placeholder for an <iframe>. * * Important: The element returned must not be block-level (e.g. div) as the PHP DOM parser * will move it out from inside any containing paragraph. So this is why a span is used. * * @since 0.2 * * @return DOMElement|false */ private function build_placeholder() { return AMP_DOM_Utils::create_node( $this->dom, 'span', [ 'placeholder' => '', 'class' => 'amp-wp-iframe-placeholder', ] ); } /** * Sanitizes a boolean character (or string) into a '0' or '1' character. * * @param mixed $value A boolean character to sanitize. If a string containing more than a single * character is provided, only the first character is taken into account. * * @return string Returns either '0' or '1'. */ private function sanitize_boolean_digit( $value ) { // Default to false if the value was forgotten. if ( empty( $value ) ) { return '0'; } // Default to false if the value has an unexpected type. if ( ! is_string( $value ) && ! is_numeric( $value ) ) { return '0'; } // See: https://github.com/ampproject/amp-wp/issues/2335#issuecomment-493209861. switch ( substr( (string) $value, 0, 1 ) ) { case '1': case 'y': case 'Y': return '1'; } return '0'; } }