diff --git a/amp.php b/amp.php index 66b44f38762..0e59906c557 100644 --- a/amp.php +++ b/amp.php @@ -87,15 +87,7 @@ function amp_after_setup_theme() { define( 'AMP_QUERY_VAR', apply_filters( 'amp_query_var', 'amp' ) ); } - add_action( 'init', 'amp_init' ); - add_action( 'widgets_init', 'AMP_Theme_Support::register_widgets' ); // @todo Let this be called by AMP_Theme_Support::init(). - add_action( 'init', 'AMP_Theme_Support::setup_commenting' ); // @todo Let this be called by AMP_Theme_Support::init(). - add_action( 'admin_init', 'AMP_Options_Manager::register_settings' ); - add_action( 'wp_loaded', 'amp_post_meta_box' ); - add_action( 'wp_loaded', 'amp_add_options_menu' ); - add_action( 'parse_query', 'amp_correct_query_when_is_front_page' ); - AMP_Post_Type_Support::add_post_type_support(); - AMP_Validation_Utils::init(); + add_action( 'init', 'amp_init', 0 ); // Must be 0 because widgets_init happens at init priority 1. } add_action( 'after_setup_theme', 'amp_after_setup_theme', 5 ); @@ -103,7 +95,6 @@ function amp_after_setup_theme() { * Init AMP. * * @since 0.1 - * @global string $pagenow */ function amp_init() { @@ -118,8 +109,14 @@ function amp_init() { add_rewrite_endpoint( AMP_QUERY_VAR, EP_PERMALINK ); + AMP_Theme_Support::init(); + AMP_Post_Type_Support::add_post_type_support(); + AMP_Validation_Utils::init(); add_filter( 'request', 'amp_force_query_var_value' ); - add_action( 'wp', 'amp_maybe_add_actions' ); + add_action( 'admin_init', 'AMP_Options_Manager::register_settings' ); + add_action( 'wp_loaded', 'amp_post_meta_box' ); + add_action( 'wp_loaded', 'amp_add_options_menu' ); + add_action( 'parse_query', 'amp_correct_query_when_is_front_page' ); // Redirect the old url of amp page to the updated url. add_filter( 'old_slug_redirect_url', 'amp_redirect_old_slug_to_new_url' ); @@ -127,7 +124,9 @@ function amp_init() { if ( class_exists( 'Jetpack' ) && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) { require_once AMP__DIR__ . '/jetpack-helper.php'; } - amp_handle_xhr_request(); + + // Add actions for legacy post templates. + add_action( 'wp', 'amp_maybe_add_actions' ); } // Make sure the `amp` query var has an explicit value. @@ -150,15 +149,9 @@ function amp_force_query_var_value( $query_vars ) { * @return void */ function amp_maybe_add_actions() { - $is_amp_endpoint = is_amp_endpoint(); - // Add hooks for when a themes that support AMP. + // Short-circuit when theme supports AMP, as everything is handled by AMP_Theme_Support. if ( current_theme_supports( 'amp' ) ) { - if ( $is_amp_endpoint ) { - AMP_Theme_Support::init(); - } else { - amp_add_frontend_actions(); - } return; } @@ -168,6 +161,8 @@ function amp_maybe_add_actions() { return; } + $is_amp_endpoint = is_amp_endpoint(); + /** * Queried post object. * diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index d22d49cec8b..a7c2331487d 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -484,94 +484,3 @@ function amp_print_schemaorg_metadata() { get_error_message(); - } - $amp_mustache_allowed_html_tags = array( 'strong', 'b', 'em', 'i', 'u', 's', 'small', 'mark', 'del', 'ins', 'sup', 'sub' ); - wp_send_json( array( - 'error' => wp_kses( $error, array_fill_keys( $amp_mustache_allowed_html_tags, array() ) ), - ) ); - }; - } ); - - // Send AMP header. - $origin = esc_url_raw( wp_unslash( $_GET['__amp_source_origin'] ) ); // WPCS: CSRF ok. - header( 'AMP-Access-Control-Allow-Source-Origin: ' . $origin, true ); -} - -/** - * Intercept the response to a non-comment POST request. - * - * @since 0.7.0 - * @param string $location The location to redirect to. - */ -function amp_intercept_post_request_redirect( $location ) { - - // Make sure relative redirects get made absolute. - $parsed_location = array_merge( - array( - 'scheme' => 'https', - 'host' => wp_parse_url( home_url(), PHP_URL_HOST ), - 'path' => strtok( wp_unslash( $_SERVER['REQUEST_URI'] ), '?' ), - ), - wp_parse_url( $location ) - ); - - $absolute_location = $parsed_location['scheme'] . '://' . $parsed_location['host']; - if ( isset( $parsed_location['port'] ) ) { - $absolute_location .= ':' . $parsed_location['port']; - } - $absolute_location .= $parsed_location['path']; - if ( isset( $parsed_location['query'] ) ) { - $absolute_location .= '?' . $parsed_location['query']; - } - if ( isset( $parsed_location['fragment'] ) ) { - $absolute_location .= '#' . $parsed_location['fragment']; - } - - header( 'AMP-Redirect-To: ' . $absolute_location ); - header( 'Access-Control-Expose-Headers: AMP-Redirect-To' ); - // Send json success as no data is required. - wp_send_json_success(); -} diff --git a/includes/class-amp-theme-support.php b/includes/class-amp-theme-support.php index 48ef2f8367b..2bee8dc416e 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -71,6 +71,20 @@ class AMP_Theme_Support { * Initialize. */ public static function init() { + if ( ! current_theme_supports( 'amp' ) ) { + return; + } + + self::purge_amp_query_vars(); + self::handle_xhr_request(); + + if ( ! is_amp_endpoint() ) { + amp_add_frontend_actions(); + } else { + self::setup_commenting(); + add_action( 'widgets_init', array( __CLASS__, 'register_widgets' ) ); + } + require_once AMP__DIR__ . '/includes/amp-post-template-actions.php'; // Validate theme support usage. @@ -95,7 +109,6 @@ public static function init() { self::register_paired_hooks(); } - self::purge_amp_query_vars(); // Note that amp_prepare_xhr_post() still looks at $_GET['__amp_source_origin']. self::register_hooks(); self::$embed_handlers = self::register_content_embed_handlers(); self::$sanitizer_classes = amp_get_content_sanitizers(); @@ -128,6 +141,18 @@ public static function is_paired_available() { return true; } + /** + * Determine whether the user is in the Customizer preview iframe. + * + * @since 0.7 + * + * @return bool Whether in Customizer preview iframe. + */ + public static function is_customize_preview_iframe() { + global $wp_customize; + return is_customize_preview() && $wp_customize->get_messenger_channel(); + } + /** * Register hooks for paired mode. */ @@ -159,11 +184,16 @@ public static function register_hooks() { * install is not on utf-8 and we may need to do a encoding conversion. */ add_action( 'wp_head', array( __CLASS__, 'add_amp_component_scripts' ), 10 ); - add_action( 'wp_head', array( __CLASS__, 'print_amp_styles' ) ); + add_action( 'wp_print_styles', array( __CLASS__, 'print_amp_styles' ), 0 ); // Print boilerplate before theme and plugin stylesheets. add_action( 'wp_enqueue_scripts', array( __CLASS__, 'enqueue_amp_default_styles' ), 9 ); add_action( 'wp_head', 'amp_add_generator_metadata', 20 ); add_action( 'wp_head', 'amp_print_schemaorg_metadata' ); + if ( is_customize_preview() ) { + add_action( 'wp_enqueue_scripts', array( __CLASS__, 'dequeue_customize_preview_scripts' ), 1000 ); + } + add_filter( 'customize_partial_render', array( __CLASS__, 'filter_customize_partial_render' ) ); + add_action( 'wp_footer', 'amp_print_analytics' ); /* @@ -178,6 +208,7 @@ public static function register_hooks() { */ add_action( 'template_redirect', array( __CLASS__, 'start_output_buffering' ), 0 ); + // Commenting hooks. add_filter( 'wp_list_comments_args', array( __CLASS__, 'amp_set_comments_walker' ), PHP_INT_MAX ); add_filter( 'comment_form_defaults', array( __CLASS__, 'filter_comment_form_defaults' ) ); add_filter( 'comment_reply_link', array( __CLASS__, 'filter_comment_reply_link' ), 10, 4 ); @@ -248,13 +279,100 @@ public static function purge_amp_query_vars() { } /** - * Set up commenting. + * Hook into a form submissions, such as comment the form or some other . + * + * @since 0.7.0 + * @global string $pagenow */ - public static function setup_commenting() { - if ( ! current_theme_supports( AMP_QUERY_VAR ) ) { + public static function handle_xhr_request() { + global $pagenow; + if ( empty( self::$purged_amp_query_vars['__amp_source_origin'] ) ) { return; } + if ( isset( $pagenow ) && 'wp-comments-post.php' === $pagenow ) { + // We don't need any data, so just send a success. + add_filter( 'comment_post_redirect', function() { + // We don't need any data, so just send a success. + wp_send_json_success(); + }, PHP_INT_MAX ); + self::handle_xhr_headers_output(); + } elseif ( ! empty( self::$purged_amp_query_vars['_wp_amp_action_xhr_converted'] ) ) { + add_filter( 'wp_redirect', array( __CLASS__, 'intercept_post_request_redirect' ), PHP_INT_MAX ); + self::handle_xhr_headers_output(); + } + } + + /** + * Handle the AMP XHR headers and output errors. + * + * @since 0.7.0 + */ + public static function handle_xhr_headers_output() { + // Add die handler for AMP error display. + add_filter( 'wp_die_handler', function() { + /** + * New error handler for AMP form submission. + * + * @param WP_Error|string $error The error to handle. + */ + return function( $error ) { + status_header( 400 ); + if ( is_wp_error( $error ) ) { + $error = $error->get_error_message(); + } + $amp_mustache_allowed_html_tags = array( 'strong', 'b', 'em', 'i', 'u', 's', 'small', 'mark', 'del', 'ins', 'sup', 'sub' ); + wp_send_json( array( + 'error' => wp_kses( $error, array_fill_keys( $amp_mustache_allowed_html_tags, array() ) ), + ) ); + }; + } ); + + // Send AMP header. + $origin = esc_url_raw( self::$purged_amp_query_vars['__amp_source_origin'] ); + header( 'AMP-Access-Control-Allow-Source-Origin: ' . $origin, true ); + } + + /** + * Intercept the response to a non-comment POST request. + * + * @since 0.7.0 + * @param string $location The location to redirect to. + */ + public static function intercept_post_request_redirect( $location ) { + + // Make sure relative redirects get made absolute. + $parsed_location = array_merge( + array( + 'scheme' => 'https', + 'host' => wp_parse_url( home_url(), PHP_URL_HOST ), + 'path' => strtok( wp_unslash( $_SERVER['REQUEST_URI'] ), '?' ), + ), + wp_parse_url( $location ) + ); + + $absolute_location = $parsed_location['scheme'] . '://' . $parsed_location['host']; + if ( isset( $parsed_location['port'] ) ) { + $absolute_location .= ':' . $parsed_location['port']; + } + $absolute_location .= $parsed_location['path']; + if ( isset( $parsed_location['query'] ) ) { + $absolute_location .= '?' . $parsed_location['query']; + } + if ( isset( $parsed_location['fragment'] ) ) { + $absolute_location .= '#' . $parsed_location['fragment']; + } + + header( 'AMP-Redirect-To: ' . $absolute_location ); + header( 'Access-Control-Expose-Headers: AMP-Redirect-To' ); + // Send json success as no data is required. + wp_send_json_success(); + } + + /** + * Set up commenting. + */ + public static function setup_commenting() { /* * Temporarily force comments to be listed in descending order. * @@ -729,6 +847,27 @@ protected static function ensure_required_markup( DOMDocument $dom ) { } } + /** + * Dequeue Customizer assets which are not necessary outside the preview iframe. + * + * Prevent enqueueing customize-preview styles if not in customizer preview iframe. + * These are only needed for when there is live editing of content, such as selective refresh. + * + * @since 0.7 + */ + public static function dequeue_customize_preview_scripts() { + + // Dequeue styles unnecessary unless in customizer preview iframe when editing (such as for edit shortcuts). + if ( ! self::is_customize_preview_iframe() ) { + wp_dequeue_style( 'customize-preview' ); + foreach ( wp_styles()->registered as $handle => $dependency ) { + if ( in_array( 'customize-preview', $dependency->deps, true ) ) { + wp_dequeue_style( $handle ); + } + } + } + } + /** * Start output buffering. * @@ -763,6 +902,31 @@ public static function finish_output_buffering() { echo self::prepare_response( ob_get_clean() ); // WPCS: xss ok. } + /** + * Filter rendered partial to convert to AMP. + * + * @see WP_Customize_Partial::render() + * + * @param string|mixed $partial Rendered partial. + * @return string|mixed Filtered partial. + * @global int $content_width + */ + public static function filter_customize_partial_render( $partial ) { + global $content_width; + if ( is_string( $partial ) && preg_match( '/<\w/', $partial ) ) { + $dom = AMP_DOM_Utils::get_dom_from_content( $partial ); + $args = array( + 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, // Back-compat. + 'use_document_element' => false, + 'allow_dirty_styles' => true, + 'allow_dirty_scripts' => false, + ); + AMP_Content_Sanitizer::sanitize_document( $dom, self::$sanitizer_classes, $args ); // @todo Include script assets in response? + $partial = AMP_DOM_Utils::get_content_from_dom( $dom ); + } + return $partial; + } + /** * Process response to ensure AMP validity. * @@ -794,6 +958,8 @@ public static function prepare_response( $response, $args = array() ) { 'content_max_width' => ! empty( $content_width ) ? $content_width : AMP_Post_Template::CONTENT_MAX_WIDTH, // Back-compat. 'use_document_element' => true, 'remove_invalid_callback' => null, + 'allow_dirty_styles' => self::is_customize_preview_iframe(), // Dirty styles only needed when editing (e.g. for edit shortcodes). + 'allow_dirty_scripts' => is_customize_preview(), // Scripts are always needed to inject changeset UUID. ), $args ); diff --git a/includes/sanitizers/class-amp-base-sanitizer.php b/includes/sanitizers/class-amp-base-sanitizer.php index 4b96a62ff6e..de5c08c8881 100644 --- a/includes/sanitizers/class-amp-base-sanitizer.php +++ b/includes/sanitizers/class-amp-base-sanitizer.php @@ -48,6 +48,13 @@ abstract class AMP_Base_Sanitizer { * @type string[] $amp_allowed_tags * @type string[] $amp_globally_allowed_attributes * @type string[] $amp_layout_allowed_attributes + * @type array $amp_allowed_tags + * @type array $amp_globally_allowed_attributes + * @type array $amp_layout_allowed_attributes + * @type array $amp_bind_placeholder_prefix + * @type bool $allow_dirty_styles + * @type bool $allow_dirty_scripts + * @type callable $remove_invalid_callback * } */ protected $args; diff --git a/includes/sanitizers/class-amp-style-sanitizer.php b/includes/sanitizers/class-amp-style-sanitizer.php index 92e9831d454..f085144cf87 100644 --- a/includes/sanitizers/class-amp-style-sanitizer.php +++ b/includes/sanitizers/class-amp-style-sanitizer.php @@ -152,6 +152,11 @@ public function get_stylesheets() { public function sanitize() { $elements = array(); + // Do nothing if inline styles are allowed. + if ( ! empty( $this->args['allow_dirty_styles'] ) ) { + return; + } + /* * Note that xpath is used to query the DOM so that the link and style elements will be * in document order. DOMNode::compareDocumentPosition() is not yet implemented. diff --git a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php index c9a13367ab9..18f4412882e 100644 --- a/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php +++ b/includes/sanitizers/class-amp-tag-and-attribute-sanitizer.php @@ -103,6 +103,65 @@ public function __construct( $dom, $args = array() ) { parent::__construct( $dom, $args ); + if ( ! empty( $this->args['allow_dirty_styles'] ) ) { + + // Allow style attribute on all elements. + $this->args['amp_globally_allowed_attributes']['style'] = array(); + + // Allow style elements. + $this->args['amp_allowed_tags']['style'][] = array( + 'attr_spec_list' => array( + 'type' => array( + 'value_casei' => 'text/css', + ), + ), + 'cdata' => array(), + 'tag_spec' => array( + 'spec_name' => 'style for Customizer preview', + ), + ); + + // Allow stylesheet links. + $this->args['amp_allowed_tags']['link'][] = array( + 'attr_spec_list' => array( + 'async' => array(), + 'crossorigin' => array(), + 'href' => array( + 'mandatory' => true, + ), + 'integrity' => array(), + 'media' => array(), + 'rel' => array( + 'dispatch_key' => 2, + 'mandatory' => true, + 'value_casei' => 'stylesheet', + ), + 'type' => array( + 'value_casei' => 'text/css', + ), + ), + 'tag_spec' => array( + 'spec_name' => 'link rel=stylesheet for Customizer preview', // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + ), + ); + } + + // Allow scripts if requested. + if ( ! empty( $this->args['allow_dirty_scripts'] ) ) { + $this->args['amp_allowed_tags']['script'][] = array( + 'attr_spec_list' => array( + 'type' => array(), + 'src' => array(), + 'async' => array(), + 'defer' => array(), + ), + 'cdata' => array(), + 'tag_spec' => array( + 'spec_name' => 'scripts for Customizer preview', + ), + ); + } + // Prepare whitelists. $this->allowed_tags = $this->args['amp_allowed_tags']; foreach ( AMP_Rule_Spec::$additional_allowed_tags as $tag_name => $tag_rule_spec ) { diff --git a/tests/test-amp-helper-functions.php b/tests/test-amp-helper-functions.php index 26a4963e27d..d6846c33708 100644 --- a/tests/test-amp-helper-functions.php +++ b/tests/test-amp-helper-functions.php @@ -372,69 +372,4 @@ public function test_amp_get_schemaorg_metadata() { $this->assertArrayHasKey( 'did_amp_schemaorg_metadata', $metadata ); $this->assertEquals( 'George', $metadata['author']['name'] ); } - - /** - * Test amp_intercept_post_request_redirect(). - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * @covers amp_intercept_post_request_redirect() - */ - public function test_amp_intercept_post_request_redirect() { - if ( ! function_exists( 'xdebug_get_headers' ) ) { - $this->markTestSkipped( 'xdebug is required for this test' ); - } - - add_theme_support( 'amp' ); - $url = get_home_url(); - - add_filter( 'wp_doing_ajax', '__return_true' ); - add_filter( 'wp_die_ajax_handler', function () { - return '__return_false'; - } ); - - ob_start(); - amp_intercept_post_request_redirect( $url ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - - $this->assertContains( 'AMP-Redirect-To: ' . $url, xdebug_get_headers() ); - $this->assertContains( 'Access-Control-Expose-Headers: AMP-Redirect-To', xdebug_get_headers() ); - - ob_start(); - amp_intercept_post_request_redirect( '/new-location/' ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $this->assertContains( 'AMP-Redirect-To: https://example.org/new-location/', xdebug_get_headers() ); - - ob_start(); - amp_intercept_post_request_redirect( '//example.com/new-location/' ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $headers = xdebug_get_headers(); - $this->assertContains( 'AMP-Redirect-To: https://example.com/new-location/', $headers ); - - ob_start(); - amp_intercept_post_request_redirect( '' ); - $this->assertEquals( '{"success":true}', ob_get_clean() ); - $this->assertContains( 'AMP-Redirect-To: https://example.org', xdebug_get_headers() ); - } - - /** - * Test amp_handle_xhr_request(). - * - * @runInSeparateProcess - * @preserveGlobalState disabled - * @covers amp_handle_xhr_headers_output() - */ - public function test_amp_handle_xhr_request() { - global $pagenow; - if ( ! function_exists( 'xdebug_get_headers' ) ) { - $this->markTestSkipped( 'xdebug is required for this test' ); - } - - $_GET['__amp_source_origin'] = 'https://example.org'; - $pagenow = 'wp-comments-post.php'; - - amp_handle_xhr_request(); - $this->assertContains( 'AMP-Access-Control-Allow-Source-Origin: https://example.org', xdebug_get_headers() ); - - } } diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php index cab00e90668..3555afca149 100644 --- a/tests/test-class-amp-theme-support.php +++ b/tests/test-class-amp-theme-support.php @@ -90,11 +90,15 @@ public function test_register_widgets() { /** * Test prepare_response. * + * @global WP_Widget_Factory $wp_widget_factory * @covers AMP_Theme_Support::prepare_response() */ public function test_prepare_response() { add_theme_support( 'amp' ); AMP_Theme_Support::init(); + $wp_widget_factory = new WP_Widget_Factory(); + wp_widgets_init(); + ob_start(); ?> @@ -261,4 +265,69 @@ public function test_get_amp_scripts() { $scripts ); } + + /** + * Test intercept_post_request_redirect(). + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * @covers AMP_Theme_Support::intercept_post_request_redirect() + */ + public function test_intercept_post_request_redirect() { + if ( ! function_exists( 'xdebug_get_headers' ) ) { + $this->markTestSkipped( 'xdebug is required for this test' ); + } + + add_theme_support( 'amp' ); + $url = get_home_url(); + + add_filter( 'wp_doing_ajax', '__return_true' ); + add_filter( 'wp_die_ajax_handler', function () { + return '__return_false'; + } ); + + ob_start(); + AMP_Theme_Support::intercept_post_request_redirect( $url ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + + $this->assertContains( 'AMP-Redirect-To: ' . $url, xdebug_get_headers() ); + $this->assertContains( 'Access-Control-Expose-Headers: AMP-Redirect-To', xdebug_get_headers() ); + + ob_start(); + AMP_Theme_Support::intercept_post_request_redirect( '/new-location/' ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $this->assertContains( 'AMP-Redirect-To: https://example.org/new-location/', xdebug_get_headers() ); + + ob_start(); + AMP_Theme_Support::intercept_post_request_redirect( '//example.com/new-location/' ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $headers = xdebug_get_headers(); + $this->assertContains( 'AMP-Redirect-To: https://example.com/new-location/', $headers ); + + ob_start(); + AMP_Theme_Support::intercept_post_request_redirect( '' ); + $this->assertEquals( '{"success":true}', ob_get_clean() ); + $this->assertContains( 'AMP-Redirect-To: https://example.org', xdebug_get_headers() ); + } + + /** + * Test handle_xhr_request(). + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * @covers AMP_Theme_Support::handle_xhr_request() + */ + public function test_handle_xhr_request() { + global $pagenow; + if ( ! function_exists( 'xdebug_get_headers' ) ) { + $this->markTestSkipped( 'xdebug is required for this test' ); + } + + $_GET['__amp_source_origin'] = 'https://example.org'; + $pagenow = 'wp-comments-post.php'; + AMP_Theme_Support::purge_amp_query_vars(); + + AMP_Theme_Support::handle_xhr_request(); + $this->assertContains( 'AMP-Access-Control-Allow-Source-Origin: https://example.org', xdebug_get_headers() ); + } }