diff --git a/amp.php b/amp.php index b837d2a6efa..0acc2bfa9c9 100644 --- a/amp.php +++ b/amp.php @@ -106,7 +106,6 @@ function amp_after_setup_theme() { * @global string $pagenow */ function amp_init() { - global $pagenow; /** * Triggers on init when AMP plugin is active. @@ -128,9 +127,7 @@ function amp_init() { if ( class_exists( 'Jetpack' ) && ! ( defined( 'IS_WPCOM' ) && IS_WPCOM ) ) { require_once AMP__DIR__ . '/jetpack-helper.php'; } - if ( isset( $pagenow ) && 'wp-comments-post.php' === $pagenow ) { - amp_prepare_comment_post(); - } + amp_handle_xhr_request(); } // Make sure the `amp` query var has an explicit value. diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index 930f8133647..4694b6953ce 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -400,23 +400,36 @@ function amp_print_schemaorg_metadata() { } /** - * Hook into a comment submission of an AMP XHR post request. - * - * This only runs on wp-comments-post.php. + * Hook into a form submissions, such as comment the form or some other . * * @since 0.7.0 + * @global string $pagenow */ -function amp_prepare_comment_post() { +function amp_handle_xhr_request() { + global $pagenow; if ( ! isset( $_GET['__amp_source_origin'] ) ) { // WPCS: CSRF ok. Beware of AMP_Theme_Support::purge_amp_query_vars(). return; } - // Add amp comment hooks. - add_filter( 'comment_post_redirect', function() { + if ( isset( $pagenow ) && 'wp-comments-post.php' === $pagenow ) { // We don't need any data, so just send a success. - wp_send_json_success(); - }, PHP_INT_MAX, 2 ); + add_filter( 'comment_post_redirect', function() { + // We don't need any data, so just send a success. + wp_send_json_success(); + }, PHP_INT_MAX, 2 ); + amp_handle_xhr_headers_output(); + } elseif ( isset( $_GET['_wp_amp_action_xhr_converted'] ) ) { // WPCS: CSRF ok. + add_filter( 'wp_redirect', 'amp_intercept_post_request_redirect', PHP_INT_MAX, 2 ); + amp_handle_xhr_headers_output(); + } +} +/** + * Handle the AMP XHR headers and output errors. + * + * @since 0.7.0 + */ +function amp_handle_xhr_headers_output() { // Add die handler for AMP error display. add_filter( 'wp_die_handler', function() { /** @@ -429,8 +442,10 @@ function amp_prepare_comment_post() { if ( is_wp_error( $error ) ) { $error = $error->get_error_message(); } - $error = strip_tags( $error, 'strong' ); - wp_send_json( compact( 'error' ) ); + $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() ) ), + ) ); }; } ); @@ -438,3 +453,39 @@ function amp_prepare_comment_post() { $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 92c3646f3d7..407401a951f 100644 --- a/includes/class-amp-theme-support.php +++ b/includes/class-amp-theme-support.php @@ -102,7 +102,7 @@ public static function init() { self::register_paired_hooks(); } - self::purge_amp_query_vars(); // Note that amp_prepare_comment_post() still looks at $_GET['__amp_source_origin']. + 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(); @@ -214,6 +214,7 @@ public static function register_hooks() { public static function purge_amp_query_vars() { $query_vars = array( '__amp_source_origin', + '_wp_amp_action_xhr_converted', 'amp_latest_update_time', ); diff --git a/includes/sanitizers/class-amp-form-sanitizer.php b/includes/sanitizers/class-amp-form-sanitizer.php index 2240906fa13..05c6252f0cc 100644 --- a/includes/sanitizers/class-amp-form-sanitizer.php +++ b/includes/sanitizers/class-amp-form-sanitizer.php @@ -87,7 +87,11 @@ public function sanitize() { } elseif ( 'post' === $method ) { $node->removeAttribute( 'action' ); if ( ! $xhr_action ) { + // record that action was converted tp action-xhr. + $action_url = add_query_arg( '_wp_amp_action_xhr_converted', 1, $action_url ); $node->setAttribute( 'action-xhr', $action_url ); + // Append error handler if not found. + $this->ensure_submit_error_element( $node ); } elseif ( 'http://' === substr( $xhr_action, 0, 7 ) ) { $node->setAttribute( 'action-xhr', substr( $xhr_action, 5 ) ); } @@ -108,4 +112,30 @@ public function sanitize() { } } } + + /** + * Checks if the form has an error handler else create one if not. + * + * @link https://www.ampproject.org/docs/reference/components/amp-form#success/error-response-rendering + * @since 0.7 + * + * @param DOMElement $form The form node to check. + */ + public function ensure_submit_error_element( $form ) { + $templates = $form->getElementsByTagName( 'template' ); + for ( $i = $templates->length - 1; $i >= 0; $i-- ) { + if ( $templates->item( $i )->parentNode->hasAttribute( 'submit-error' ) ) { + return; // Found error template, do nothing. + } + } + + $div = $this->dom->createElement( 'div' ); + $template = $this->dom->createElement( 'template' ); + $mustache = $this->dom->createTextNode( '{{{error}}}' ); + $div->setAttribute( 'submit-error', '' ); + $template->setAttribute( 'type', 'amp-mustache' ); + $template->appendChild( $mustache ); + $div->appendChild( $template ); + $form->appendChild( $div ); + } } diff --git a/tests/test-amp-form-sanitizer.php b/tests/test-amp-form-sanitizer.php index c0674d2ea6e..071931eeaee 100644 --- a/tests/test-amp-form-sanitizer.php +++ b/tests/test-amp-form-sanitizer.php @@ -46,7 +46,7 @@ public function get_data() { ), 'form_with_post_method_http_action_and_no_target' => array( '
', - '', + '', ), 'form_with_post_method_http_action_and_blank_target' => array( '', @@ -58,7 +58,7 @@ public function get_data() { ), 'form_with_post_method_https_action_and_custom_target' => array( '', - '', + '', ), ); } diff --git a/tests/test-amp-helper-functions.php b/tests/test-amp-helper-functions.php index f7cde3d0e7a..4eed7183a7f 100644 --- a/tests/test-amp-helper-functions.php +++ b/tests/test-amp-helper-functions.php @@ -362,4 +362,69 @@ 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() ); + + } }