OwlCyberSecurity - MANAGER
Edit File: class-amp-core-block-handler.php
<?php /** * Class AMP_Core_Block_Handler * * @package AMP */ use AmpProject\Dom\Document; use AmpProject\Html\Attribute; /** * Class AMP_Core_Block_Handler * * @since 1.0 * @internal */ class AMP_Core_Block_Handler extends AMP_Base_Embed_Handler { /** * Attribute to store the original width on a video or iframe just before WordPress removes it. * * @see AMP_Core_Block_Handler::preserve_widget_text_element_dimensions() * @see AMP_Core_Block_Handler::process_text_widgets() * @var string */ const AMP_PRESERVED_WIDTH_ATTRIBUTE_NAME = 'amp-preserved-width'; /** * Attribute to store the original height on a video or iframe just before WordPress removes it. * * @see AMP_Core_Block_Handler::preserve_widget_text_element_dimensions() * @see AMP_Core_Block_Handler::process_text_widgets() * @var string */ const AMP_PRESERVED_HEIGHT_ATTRIBUTE_NAME = 'amp-preserved-height'; /** * Count of the category widgets encountered. * * @var int */ private $category_widget_count = 0; /** * Count of the navigation blocks encountered. * * @var int */ private $navigation_block_count = 0; /** * Methods to ampify blocks. * * @var array */ protected $block_ampify_methods = [ 'core/categories' => 'ampify_categories_block', 'core/archives' => 'ampify_archives_block', 'core/video' => 'ampify_video_block', 'core/file' => 'ampify_file_block', 'core/gallery' => 'ampify_gallery_block', 'core/navigation' => 'ampify_navigation_block', ]; /** * Register embed. */ public function register_embed() { add_filter( 'render_block', [ $this, 'filter_rendered_block' ], 0, 2 ); add_filter( 'widget_text_content', [ $this, 'preserve_widget_text_element_dimensions' ], PHP_INT_MAX ); } /** * Unregister embed. */ public function unregister_embed() { remove_filter( 'render_block', [ $this, 'filter_rendered_block' ], 0 ); remove_filter( 'widget_text_content', [ $this, 'preserve_widget_text_element_dimensions' ], PHP_INT_MAX ); } /** * Filters the content of a single block to make it AMP valid. * * @param string $block_content The block content about to be appended. * @param array $block The full block, including name and attributes. * @return string Filtered block content. */ public function filter_rendered_block( $block_content, $block ) { if ( ! isset( $block['blockName'] ) ) { return $block_content; // @codeCoverageIgnore } if ( isset( $block['attrs'] ) && 'core/shortcode' !== $block['blockName'] ) { $injected_attributes = ''; $prop_attribute_mapping = [ 'ampCarousel' => 'data-amp-carousel', 'ampLightbox' => 'data-amp-lightbox', ]; foreach ( $prop_attribute_mapping as $prop => $attr ) { if ( isset( $block['attrs'][ $prop ] ) ) { $property_value = $block['attrs'][ $prop ]; if ( is_bool( $property_value ) ) { $property_value = $property_value ? 'true' : 'false'; } $injected_attributes .= sprintf( ' %s="%s"', $attr, esc_attr( $property_value ) ); } } if ( $injected_attributes ) { $block_content = preg_replace( '/(<\w+)/', '$1' . $injected_attributes, $block_content, 1 ); } } if ( isset( $this->block_ampify_methods[ $block['blockName'] ] ) ) { $method_name = $this->block_ampify_methods[ $block['blockName'] ]; $block_content = $this->{$method_name}( $block_content, $block ); } elseif ( 'core/image' === $block['blockName'] || 'core/audio' === $block['blockName'] ) { /* * While the video block placeholder just outputs an empty video element, the placeholders for image and * audio blocks output empty <img> and <audio> respectively. These will result in AMP validation errors, * so we need to empty out the block content to prevent this from happening. Note that <source> is used * for <img> because eventually the image block could use <picture>. */ if ( ! preg_match( '/src=|<source/', $block_content ) ) { $block_content = ''; } } return $block_content; } /** * Fix rendering of categories block when displayAsDropdown. * * This excludes the disallowed JS scrips, adds <form> tags, and uses on:change for <select>. * * @see render_block_core_categories() * * @param string $block_content Block content. * @return string Rendered. */ public function ampify_categories_block( $block_content ) { static $block_id = 0; $block_id++; $form_id = "wp-block-categories-dropdown-{$block_id}-form"; // Remove output of build_dropdown_script_block_core_categories(). $block_content = preg_replace( '#<script.+?</script>#s', '', $block_content ); $form = sprintf( '<form action="%s" method="get" target="_top" id="%s">', esc_url( home_url() ), esc_attr( $form_id ) ); $block_content = preg_replace( '#(<select)(.+</select>)#s', $form . '$1' . sprintf( ' on="change:%1$s.submit"', esc_attr( $form_id ) ) . '$2</form>', $block_content, 1 ); return $block_content; } /** * Fix rendering of archives block when displayAsDropdown. * * This replaces disallowed script with the use of on:change for <select>. * * @see render_block_core_archives() * * @param string $block_content Block content. * @return string Rendered. */ public function ampify_archives_block( $block_content ) { // Eliminate use of uniqid(). Core should be using wp_unique_id() here. static $block_id = 0; $block_id++; $block_content = preg_replace( '/(?<="wp-block-archives-)\w+(?=")/', $block_id, $block_content ); // Replace onchange with on attribute. $block_content = preg_replace( '/onchange=".+?"/', 'on="change:AMP.navigateTo(url=event.value)"', $block_content ); return $block_content; } /** * Ampify video block. * * Inject the video attachment's dimensions if available. This prevents having to try to look up the attachment * post by the video URL in `\AMP_Video_Sanitizer::filter_video_dimensions()`. * * @see \AMP_Video_Sanitizer::filter_video_dimensions() * * @param string $block_content The block content about to be appended. * @param array $block The full block, including name and attributes. * @return string Filtered block content. */ public function ampify_video_block( $block_content, $block ) { if ( empty( $block['attrs']['id'] ) || 'attachment' !== get_post_type( $block['attrs']['id'] ) ) { return $block_content; } $meta_data = wp_get_attachment_metadata( $block['attrs']['id'] ); if ( ! isset( $meta_data['width'], $meta_data['height'] ) ) { return $block_content; } $block_content = preg_replace_callback( '/(?<=<video)\s[^>]+/', static function ( $matches ) use ( $meta_data ) { $attrs = $matches[0]; if ( ! preg_match( '/\s(width|height|style)=/', $attrs ) ) { $attrs .= sprintf( ' width="%1$d" height="%2$d" style="aspect-ratio:%1$d/%2$d"', $meta_data['width'], $meta_data['height'] ); } return $attrs; }, $block_content, 1 ); return $block_content; } /** * Ampify file block. * * Fix handling of PDF previews by dequeuing wp-block-library-file and ensuring preview element has 100% width. * * @see \AMP_Object_Sanitizer::sanitize_pdf() * * @param string $block_content The block content about to be appended. * @param array $block The full block, including name and attributes. * @return string Filtered block content. */ public function ampify_file_block( $block_content, $block ) { if ( empty( $block['attrs']['displayPreview'] ) || empty( $block['attrs']['href'] ) || '.pdf' !== substr( wp_parse_url( $block['attrs']['href'], PHP_URL_PATH ), -4 ) ) { return $block_content; } add_action( 'wp_print_scripts', [ $this, 'dequeue_block_library_file_script' ], 0 ); add_action( 'wp_print_footer_scripts', [ $this, 'dequeue_block_library_file_script' ], 0 ); // In Twenty Twenty the PDF embed fails to render due to the parent of the embed having // the style rule `display: flex`. Ensuring the element has 100% width fixes that issue. $block_content = preg_replace( ':(?=</div>):', '<style id="amp-wp-file-block">.wp-block-file > .wp-block-file__embed { width:100% }</style>', $block_content, 1 ); return $block_content; } /** * Ampify gallery block. * * Apply data-amp-lightbox attribute only to descendant image blocks. * * @since 2.2.1 * * @param string $block_content The block content about to be appended. * @param array $block The full block, including name and attributes. * @return string Filtered block content. */ public function ampify_gallery_block( $block_content, $block ) { // Skip legacy gallery blocks. if ( ! empty( $block['attrs']['ids'] ) ) { return $block_content; } $block_content = preg_replace( '/\sdata-amp-lightbox="\w+"/', '', $block_content ); // Bail out early if there are no images in the gallery or the lightbox feature is not enabled. if ( empty( $block['innerBlocks'] ) || empty( $block['attrs']['ampLightbox'] ) ) { return $block_content; } // Add data attributes to figure elements that are nested in the gallery block. // Note that the first match is the gallery block itself which doesn't need the data-amp-lightbox attribute. $figure_count = 0; $block_content = preg_replace_callback( '/(?<=<figure\s)/', static function () use ( &$figure_count ) { return 0 < $figure_count++ ? 'data-amp-lightbox="true" ' : ''; }, $block_content ); return $block_content; } /** * Dequeue wp-block-library-file script. */ public function dequeue_block_library_file_script() { wp_dequeue_script( 'wp-block-library-file' ); } /** * Ampify navigation block contained by <nav> element. * * @since 2.2.1 * * @param string $block_content The block content about to be appended. * @param array $block The full block, including name and attributes. * * @return string Filtered block content. */ public function ampify_navigation_block( $block_content, $block ) { if ( 0 === $this->navigation_block_count ) { add_action( 'wp_print_scripts', [ $this, 'dequeue_block_navigation_view_script' ], 0 ); add_action( 'wp_print_footer_scripts', [ $this, 'dequeue_block_navigation_view_script' ], 0 ); } $is_interactive_block = str_contains( preg_match( '/(?<=<nav)\s[^>]+/', $block_content, $matches ) ? $matches[0] : '', 'data-wp-interactive' ); $this->navigation_block_count++; $modal_state_property = "modal_{$this->navigation_block_count}_expanded"; // Set `aria-expanded` value of submenus whenever AMP state changes. $submenu_toggles_count = 0; $block_content = preg_replace_callback( '/(?<=<button)\s[^>]+/', static function ( $matches ) use ( $modal_state_property, &$submenu_toggles_count ) { $new_block_content = $matches[0]; if ( false === strpos( $new_block_content, 'wp-block-navigation-submenu__toggle' ) ) { return $new_block_content; } $submenu_toggles_count++; $submenu_state_property = str_replace( 'expanded', 'submenu_' . $submenu_toggles_count . '_expanded', $modal_state_property ); // Set `aria-expanded` value of submenus whenever AMP state changes. return str_replace( ' aria-expanded', sprintf( ' on="tap:AMP.setState({ %1$s: !%1$s })" [aria-expanded]="%1$s ? \'true\' : \'false\'" aria-expanded', esc_attr( $submenu_state_property ) ), $new_block_content ); }, $block_content ); // In case of the "Mobile" option value, the `overlayMenu` attribute is not set at all. if ( ! empty( $block['attrs']['overlayMenu'] ) && 'never' === $block['attrs']['overlayMenu'] ) { return $block_content; } $block_content = preg_replace_callback( '/(?<=<button)\s[^>]+/', static function ( $matches ) use ( $modal_state_property, $is_interactive_block ) { $new_block_content = $matches[0]; // Skip submenu toggles. if ( false !== strpos( $new_block_content, 'wp-block-navigation-submenu__toggle' ) ) { return $new_block_content; } if ( $is_interactive_block ) { // Replace `data-wp-on--click` or `data-wp-on-async--click` with AMP state on submenu open button. if ( false !== strpos( $new_block_content, 'wp-block-navigation__responsive-container-open' ) ) { $new_block_content = preg_replace( '/\sdata-wp-on(?:-async)?--click="[^"]+"/', sprintf( ' on="tap:AMP.setState({ %1$s: !%1$s })"', esc_attr( $modal_state_property ) ), $new_block_content ); } // Replace `data-wp-on--click` or `data-wp-on-async--click` with AMP state on submenu close button. if ( false !== strpos( $new_block_content, 'wp-block-navigation__responsive-container-close' ) ) { $new_block_content = preg_replace( '/\sdata-wp-on(?:-async)?--click="[^"]+"/', sprintf( ' on="tap:AMP.setState({ %1$s: !%1$s })"', esc_attr( $modal_state_property ) ), $new_block_content ); } } else { // Replace micromodal toggle logic bound with buttons with AMP state to open the modal. $new_block_content = preg_replace( '/\sdata-micromodal-trigger="modal-\w+"/', sprintf( ' on="tap:AMP.setState({ %1$s: !%1$s })"', esc_attr( $modal_state_property ) ), $new_block_content ); // Replace micromodal toggle logic bound with buttons with AMP state to close the modal. $new_block_content = str_replace( ' data-micromodal-close', sprintf( ' on="tap:AMP.setState({ %1$s: !%1$s })"', esc_attr( $modal_state_property ) ), $new_block_content ); } // Set `aria-expanded` value whenever AMP state changes. return str_replace( ' aria-expanded', sprintf( ' [aria-expanded]="%s ? \'true\' : \'false\'" aria-expanded', esc_attr( $modal_state_property ) ), $new_block_content ); }, $block_content ); // Change a responsive container class name and aria-hidden value based on the AMP state. $block_content = preg_replace_callback( '/(?><.+\sclass="([^"]*wp-block-navigation__responsive-container(?>\s[^"]*)?)"[^>]*>)/', static function ( $matches ) use ( $modal_state_property ) { $new_block_content = str_replace( ' class=', sprintf( ' [aria-hidden]="%1$s ? \'false\' : \'true\'" aria-hidden="true" [class]="%1$s ? \'%2$s is-menu-open has-modal-open\' : \'%2$s\'" class=', esc_attr( $modal_state_property ), esc_attr( $matches[1] ) ), $matches[0] ); return $new_block_content; }, $block_content ); return $block_content; } /** * Dequeue wp-block-navigation-view script. * * @since 2.2.1 */ public function dequeue_block_navigation_view_script() { wp_dequeue_script( 'wp-block-navigation-view' ); wp_dequeue_script( 'wp-block-navigation-view-2' ); } /** * Sanitize widgets that are not added via Gutenberg. * * @param Document $dom Document. * @param array $args Args passed to sanitizer. */ public function sanitize_raw_embeds( Document $dom, $args = [] ) { $this->process_categories_widgets( $dom ); $this->process_archives_widgets( $dom, $args ); $this->process_text_widgets( $dom ); } /** * Process "Categories" widgets. * * @since 2.0 * * @param Document $dom Document. */ private function process_categories_widgets( Document $dom ) { $selects = $dom->xpath->query( '//form/select[ @name = "cat" ]' ); foreach ( $selects as $select ) { if ( ! $select instanceof DOMElement ) { continue; // @codeCoverageIgnore } $form = $select->parentNode; if ( ! $form instanceof DOMElement || ! $form->parentNode instanceof DOMElement ) { continue; // @codeCoverageIgnore } $script = $dom->xpath->query( './/script[ contains( text(), "onCatChange" ) ]', $form->parentNode )->item( 0 ); if ( ! $script instanceof DOMElement ) { continue; // @codeCoverageIgnore } $this->category_widget_count++; $id = sprintf( 'amp-wp-widget-categories-%d', $this->category_widget_count ); $form->setAttribute( 'id', $id ); AMP_DOM_Utils::add_amp_action( $select, 'change', sprintf( '%s.submit', $id ) ); $script->parentNode->removeChild( $script ); } } /** * Process "Archives" widgets. * * @since 2.0 * * @param Document $dom Select node retrieved from the widget. * @param array $args Args passed to sanitizer. */ private function process_archives_widgets( Document $dom, $args = [] ) { $selects = $dom->xpath->query( '//select[ @name = "archive-dropdown" and starts-with( @id, "archives-dropdown-" ) ]' ); foreach ( $selects as $select ) { if ( ! $select instanceof DOMElement ) { continue; // @codeCoverageIgnore } $script = $dom->xpath->query( './/script[ contains( text(), "onSelectChange" ) ]', $select->parentNode )->item( 0 ); if ( $script ) { $script->parentNode->removeChild( $script ); } elseif ( $select->hasAttribute( 'onchange' ) ) { // Special condition for WordPress<=5.1. $select->removeAttribute( 'onchange' ); } else { continue; } AMP_DOM_Utils::add_amp_action( $select, 'change', 'AMP.navigateTo(url=event.value)' ); // When AMP-to-AMP linking is enabled, ensure links go to the AMP version. if ( ! empty( $args['amp_to_amp_linking_enabled'] ) ) { foreach ( $dom->xpath->query( '//option[ @value != "" ]', $select ) as $option ) { /** * Option element. * * @var DOMElement $option */ $option->setAttribute( 'value', amp_add_paired_endpoint( $option->getAttribute( 'value' ) ) ); } } } } /** * Preserve dimensions of elements in a Text widget to later restore to circumvent WordPress core stripping them out. * * Core strips out the dimensions to prevent the element being made too wide for the sidebar. This is not a concern * in AMP because of responsive sizing. So this logic is here to undo what core is doing. * * @since 2.0 * @see WP_Widget_Text::inject_video_max_width_style() * @see AMP_Core_Block_Handler::process_text_widgets() * * @param string $content Content. * @return string Content. */ public function preserve_widget_text_element_dimensions( $content ) { $content = preg_replace_callback( '#<(video|iframe|object|embed)\s[^>]*>#si', static function ( $matches ) { $html = $matches[0]; $html = preg_replace( '/(?=\sheight="(\d+)")/', ' ' . self::AMP_PRESERVED_HEIGHT_ATTRIBUTE_NAME . '="$1" ', $html ); $html = preg_replace( '/(?=\swidth="(\d+)")/', ' ' . self::AMP_PRESERVED_WIDTH_ATTRIBUTE_NAME . '="$1" ', $html ); return $html; }, $content ); return $content; } /** * Process "Text" widgets. * * @since 2.0 * @see AMP_Core_Block_Handler::preserve_widget_text_element_dimensions() * * @param Document $dom Select node retrieved from the widget. */ private function process_text_widgets( Document $dom ) { foreach ( $dom->xpath->query( '//div[ @class = "textwidget" ]' ) as $text_widget ) { // Restore the width/height attributes which were preserved in preserve_widget_text_element_dimensions. foreach ( $dom->xpath->query( sprintf( './/*[ @%s or @%s ]', self::AMP_PRESERVED_WIDTH_ATTRIBUTE_NAME, self::AMP_PRESERVED_HEIGHT_ATTRIBUTE_NAME ), $text_widget ) as $element ) { /** @var DOMElement $element */ if ( $element->hasAttribute( self::AMP_PRESERVED_WIDTH_ATTRIBUTE_NAME ) ) { $element->setAttribute( Attribute::WIDTH, $element->getAttribute( self::AMP_PRESERVED_WIDTH_ATTRIBUTE_NAME ) ); $element->removeAttribute( self::AMP_PRESERVED_WIDTH_ATTRIBUTE_NAME ); } if ( $element->hasAttribute( self::AMP_PRESERVED_HEIGHT_ATTRIBUTE_NAME ) ) { $element->setAttribute( Attribute::HEIGHT, $element->getAttribute( self::AMP_PRESERVED_HEIGHT_ATTRIBUTE_NAME ) ); $element->removeAttribute( self::AMP_PRESERVED_HEIGHT_ATTRIBUTE_NAME ); } } /* * Remove inline width style which is added to video shortcode but which overruns the container. * Normally this width gets overridden by wp-mediaelement.css to be max-width: 100%, but since * MediaElement.js is not used in AMP this stylesheet is not included. In any case, videos in AMP are * responsive so this is built-in. Note also the style rule for .wp-video in amp-default.css. */ foreach ( $dom->xpath->query( './/div[ @class = "wp-video" and @style ]', $text_widget ) as $element ) { /** @var DOMElement $element */ $element->removeAttribute( 'style' ); } } } }