From bc5770700487d398e2f0020b3733136947fc89db Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 7 Jul 2018 21:41:08 -0500 Subject: [PATCH 01/27] Install service worker for AMP pages to cache AMP CDN assets --- amp.php | 2 + assets/js/amp-service-worker-asset-cache.js | 36 +++++ includes/class-amp-autoloader.php | 1 + includes/class-amp-service-workers.php | 161 ++++++++++++++++++++ includes/class-amp-theme-support.php | 7 - 5 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 assets/js/amp-service-worker-asset-cache.js create mode 100644 includes/class-amp-service-workers.php diff --git a/amp.php b/amp.php index de6f6507131..ae39869556c 100644 --- a/amp.php +++ b/amp.php @@ -148,6 +148,8 @@ function amp_init() { AMP_Theme_Support::init(); AMP_Post_Type_Support::add_post_type_support(); + AMP_Service_Workers::init(); + add_filter( 'request', 'amp_force_query_var_value' ); add_action( 'admin_init', 'AMP_Options_Manager::register_settings' ); add_action( 'wp_loaded', 'amp_editor_core_blocks' ); diff --git a/assets/js/amp-service-worker-asset-cache.js b/assets/js/amp-service-worker-asset-cache.js new file mode 100644 index 00000000000..3b2873871e3 --- /dev/null +++ b/assets/js/amp-service-worker-asset-cache.js @@ -0,0 +1,36 @@ +/* global Promise */ +( () => { + /* + * Use a stale-while-revalidate strategy to cache responses from the AMP CDN. + * This should eventually be implemented using Workbox. + */ + const CACHE = 'amp'; + + self.addEventListener( 'fetch', ( event ) => { + if ( 'GET' !== event.request.method ) { + return; + } + const url = new URL( event.request.url ); + if ( 'https://cdn.ampproject.org' !== url.origin ) { + return; + } + event.respondWith( fromCache( event.request ) ); + event.waitUntil( update( event.request ) ); + } ); + + function fromCache( request ) { + return caches.open( CACHE ).then( ( cache ) => { + return cache.match( request ).then( ( matching ) => { + return matching || Promise.reject( 'no-match' ); + } ); + } ); + } + + function update( request ) { + return caches.open( CACHE ).then( ( cache ) => { + return fetch( request ).then( ( response ) => { + return cache.put( request, response ); + } ); + } ); + } +} )(); diff --git a/includes/class-amp-autoloader.php b/includes/class-amp-autoloader.php index 1a83f70f3d7..22886beb77a 100644 --- a/includes/class-amp-autoloader.php +++ b/includes/class-amp-autoloader.php @@ -31,6 +31,7 @@ class AMP_Autoloader { private static $_classmap = array( 'AMP_Editor_Blocks' => 'includes/admin/class-amp-editor-blocks', 'AMP_Theme_Support' => 'includes/class-amp-theme-support', + 'AMP_Service_Workers' => 'includes/class-amp-service-workers', 'AMP_Response_Headers' => 'includes/class-amp-response-headers', 'AMP_Comment_Walker' => 'includes/class-amp-comment-walker', 'AMP_Template_Customizer' => 'includes/admin/class-amp-customizer', diff --git a/includes/class-amp-service-workers.php b/includes/class-amp-service-workers.php new file mode 100644 index 00000000000..e3c4ef64649 --- /dev/null +++ b/includes/class-amp-service-workers.php @@ -0,0 +1,161 @@ +register( + 'amp-asset-caching', + amp_get_asset_url( 'js/amp-service-worker-asset-cache.js' ) + ); + } + + /** + * Install service worker(s). + * + * @since ? + * @see wp_print_service_workers() + * @link https://github.com/xwp/pwa-wp + */ + public static function install_service_worker() { + if ( ! function_exists( 'wp_service_workers' ) || ! function_exists( 'wp_get_service_worker_url' ) ) { + return; + } + + $scopes = wp_service_workers()->get_scopes(); + if ( empty( $scopes ) ) { + return; // No service worker scripts are installed. + } + + // Find the scope that has the longest match with the current path. + $current_url_path = wp_parse_url( amp_get_current_url(), PHP_URL_PATH ); + $max_matched_scope = ''; + foreach ( $scopes as $scope ) { + if ( strlen( $scope ) > strlen( $max_matched_scope ) && substr( $current_url_path, 0, strlen( $scope ) ) === $scope ) { + $max_matched_scope = $scope; + } + } + + // None of the registered scripts' scopes are a match for the current URL path. + if ( empty( $max_matched_scope ) ) { + return; + } + + $src = wp_get_service_worker_url( $max_matched_scope ); + $iframe_src = add_query_arg( + self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR, + $max_matched_scope, + home_url( '/', 'https' ) + ); + ?> + + + query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] ) ) { + return; + } + + $scope = $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ]; + $scopes = wp_service_workers()->get_scopes(); + if ( ! in_array( $scope, $scopes, true ) ) { + wp_die( + esc_html__( 'No service workers registered for the requested scope.', 'amp' ), + esc_html__( 'Service Worker Installation', 'amp' ), + array( 'response' => 404 ) + ); + } + ?> + + + + + <?php esc_html_e( 'Service Worker Installation', 'amp' ); ?> + + + navigator.serviceWorker.register( %s, %s );', + wp_json_encode( wp_get_service_worker_url( $scope ) ), + wp_json_encode( compact( 'scope' ) ) + ) + ?> + + + '; - /** * Response cache group name. * From d0ed4524676ad884cdbf5675aca26cdefea5de33 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 9 Jul 2018 22:47:50 -0700 Subject: [PATCH 02/27] Update service worker integration to use proposed 'front' scope --- includes/class-amp-service-workers.php | 53 +++++++++----------------- 1 file changed, 19 insertions(+), 34 deletions(-) diff --git a/includes/class-amp-service-workers.php b/includes/class-amp-service-workers.php index e3c4ef64649..5388c39ff6b 100644 --- a/includes/class-amp-service-workers.php +++ b/includes/class-amp-service-workers.php @@ -22,7 +22,7 @@ class AMP_Service_Workers { * Init. */ public static function init() { - if ( ! class_exists( 'WP_Service_Workers' ) ) { + if ( ! function_exists( 'wp_register_service_worker' ) ) { return; } @@ -31,6 +31,10 @@ public static function init() { return $vars; } ); + wp_register_service_worker( + 'amp-asset-caching', + amp_get_asset_url( 'js/amp-service-worker-asset-cache.js' ) + ); add_action( 'wp_default_service_workers', array( __CLASS__, 'register_script' ) ); add_action( 'parse_request', array( __CLASS__, 'handle_service_worker_iframe_install' ) ); add_action( 'wp', array( __CLASS__, 'add_install_hooks' ) ); @@ -52,22 +56,10 @@ public static function add_install_hooks() { add_action( 'amp_post_template_footer', array( __CLASS__, 'install_service_worker' ) ); } - /** - * Register service worker script. - * - * @param WP_Service_Workers $workers Workers. - */ - public static function register_script( WP_Service_Workers $workers ) { - $workers->register( - 'amp-asset-caching', - amp_get_asset_url( 'js/amp-service-worker-asset-cache.js' ) - ); - } - /** * Install service worker(s). * - * @since ? + * @since 1.0 * @see wp_print_service_workers() * @link https://github.com/xwp/pwa-wp */ @@ -76,29 +68,22 @@ public static function install_service_worker() { return; } - $scopes = wp_service_workers()->get_scopes(); - if ( empty( $scopes ) ) { - return; // No service worker scripts are installed. - } - - // Find the scope that has the longest match with the current path. - $current_url_path = wp_parse_url( amp_get_current_url(), PHP_URL_PATH ); - $max_matched_scope = ''; - foreach ( $scopes as $scope ) { - if ( strlen( $scope ) > strlen( $max_matched_scope ) && substr( $current_url_path, 0, strlen( $scope ) ) === $scope ) { - $max_matched_scope = $scope; + // Get the frontend-scoped service worker scripts. + $front_handles = array(); + foreach ( wp_service_workers()->registered as $handle => $item ) { + if ( 'all' === $item->args['scope'] || 'front' === $item->args['scope'] ) { + $front_handles[] = $handle; } } - // None of the registered scripts' scopes are a match for the current URL path. - if ( empty( $max_matched_scope ) ) { - return; + if ( empty( $front_handles ) ) { + return; // No service worker scripts are installed. } - $src = wp_get_service_worker_url( $max_matched_scope ); + $src = wp_get_service_worker_url( 'front' ); $iframe_src = add_query_arg( self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR, - $max_matched_scope, + 'front', home_url( '/', 'https' ) ); ?> @@ -122,9 +107,8 @@ public static function handle_service_worker_iframe_install() { return; } - $scope = $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ]; - $scopes = wp_service_workers()->get_scopes(); - if ( ! in_array( $scope, $scopes, true ) ) { + $scope = $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ]; + if ( 'front' !== $scope && 'admin' !== $scope ) { wp_die( esc_html__( 'No service workers registered for the requested scope.', 'amp' ), esc_html__( 'Service Worker Installation', 'amp' ), @@ -139,12 +123,13 @@ public static function handle_service_worker_iframe_install() { <?php esc_html_e( 'Service Worker Installation', 'amp' ); ?> + navigator.serviceWorker.register( %s, %s );', wp_json_encode( wp_get_service_worker_url( $scope ) ), wp_json_encode( compact( 'scope' ) ) - ) + ); ?> From 43ba855ac87d73c1998f017d4d3ce09410a0d237 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 9 Jul 2018 23:08:35 -0700 Subject: [PATCH 03/27] Improve stale-while-revalidate from Google offline-cookbook; add Google Fonts caching --- .eslintrc | 3 +- .jshintignore | 1 + .jshintrc | 5 ++- assets/js/amp-service-worker-asset-cache.js | 43 ++++++++++----------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/.eslintrc b/.eslintrc index 157c270c5c3..06110c4aaa8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -21,7 +21,8 @@ "globals": { "wp": true, "window": true, - "document": true + "document": true, + "Set": true }, "settings": { "react": { diff --git a/.jshintignore b/.jshintignore index 3ffce3e2c81..c39fa64c39d 100644 --- a/.jshintignore +++ b/.jshintignore @@ -2,3 +2,4 @@ **/node_modules/** **/vendor/** **/assets/js/amp-blocks-compiled.js +blocks/**/*.js diff --git a/.jshintrc b/.jshintrc index 991ccbf7396..9e6292f25a8 100644 --- a/.jshintrc +++ b/.jshintrc @@ -3,7 +3,6 @@ "curly": true, "eqeqeq": true, "eqnull": true, - "es3": true, "esversion": 6, "expr": true, "immed": true, @@ -22,6 +21,8 @@ "Backbone": false, "jQuery": false, "JSON": false, - "wp": false + "wp": false, + "caches": false, + "self": false } } diff --git a/assets/js/amp-service-worker-asset-cache.js b/assets/js/amp-service-worker-asset-cache.js index 3b2873871e3..90d6c8459fb 100644 --- a/assets/js/amp-service-worker-asset-cache.js +++ b/assets/js/amp-service-worker-asset-cache.js @@ -1,36 +1,35 @@ -/* global Promise */ ( () => { /* - * Use a stale-while-revalidate strategy to cache responses from the AMP CDN. + * Use a stale-while-revalidate strategy to cache responses from the AMP CDN and Google Fonts. * This should eventually be implemented using Workbox. */ - const CACHE = 'amp'; + + const cachedOrigins = new Set( [ + 'https://fonts.googleapis.com', + 'https://cdn.ampproject.org', + 'https://fonts.gstatic.com' + ] ); self.addEventListener( 'fetch', ( event ) => { if ( 'GET' !== event.request.method ) { return; } const url = new URL( event.request.url ); - if ( 'https://cdn.ampproject.org' !== url.origin ) { + if ( ! cachedOrigins.has( url.origin ) ) { return; } - event.respondWith( fromCache( event.request ) ); - event.waitUntil( update( event.request ) ); - } ); - function fromCache( request ) { - return caches.open( CACHE ).then( ( cache ) => { - return cache.match( request ).then( ( matching ) => { - return matching || Promise.reject( 'no-match' ); - } ); - } ); - } - - function update( request ) { - return caches.open( CACHE ).then( ( cache ) => { - return fetch( request ).then( ( response ) => { - return cache.put( request, response ); - } ); - } ); - } + // Props jakearchibald: https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate + event.respondWith( + caches.open( 'amp' ).then( ( cache ) => { + return cache.match( event.request ).then( ( response ) => { + var fetchPromise = fetch( event.request ).then( ( networkResponse ) => { + cache.put( event.request, networkResponse.clone() ); + return networkResponse; + } ); + return response || fetchPromise; + } ); + } ) + ); + } ); } )(); From 71cb091f68f971072784c60753517f5b44bbb3f0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 11 Jul 2018 18:35:51 -0700 Subject: [PATCH 04/27] Update scopes to use constants --- includes/class-amp-service-workers.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/includes/class-amp-service-workers.php b/includes/class-amp-service-workers.php index 5388c39ff6b..69e0f0f2daa 100644 --- a/includes/class-amp-service-workers.php +++ b/includes/class-amp-service-workers.php @@ -71,7 +71,7 @@ public static function install_service_worker() { // Get the frontend-scoped service worker scripts. $front_handles = array(); foreach ( wp_service_workers()->registered as $handle => $item ) { - if ( 'all' === $item->args['scope'] || 'front' === $item->args['scope'] ) { + if ( $item->args['scope'] & WP_Service_Workers::SCOPE_FRONT ) { // Yes, bitwise AND intended. $front_handles[] = $handle; } } @@ -80,10 +80,10 @@ public static function install_service_worker() { return; // No service worker scripts are installed. } - $src = wp_get_service_worker_url( 'front' ); + $src = wp_get_service_worker_url( WP_Service_Workers::SCOPE_FRONT ); $iframe_src = add_query_arg( self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR, - 'front', + WP_Service_Workers::SCOPE_FRONT, home_url( '/', 'https' ) ); ?> @@ -103,18 +103,21 @@ public static function install_service_worker() { * @link https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#data-iframe-src-(optional) */ public static function handle_service_worker_iframe_install() { - if ( empty( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] ) ) { + if ( ! isset( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] ) ) { return; } - $scope = $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ]; - if ( 'front' !== $scope && 'admin' !== $scope ) { + $scope = intval( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] ); + if ( WP_Service_Workers::SCOPE_ADMIN !== $scope && WP_Service_Workers::SCOPE_FRONT !== $scope ) { wp_die( esc_html__( 'No service workers registered for the requested scope.', 'amp' ), esc_html__( 'Service Worker Installation', 'amp' ), array( 'response' => 404 ) ); } + + $front_scope = home_url( '/', 'relative' ); + ?> @@ -128,7 +131,7 @@ public static function handle_service_worker_iframe_install() { printf( '', wp_json_encode( wp_get_service_worker_url( $scope ) ), - wp_json_encode( compact( 'scope' ) ) + wp_json_encode( array( 'scope' => $front_scope ) ) ); ?> From f708adf445472cf04ee499c21431ff09f54c5f2d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 24 Aug 2018 16:38:48 -0700 Subject: [PATCH 05/27] Use PWA plugin API for runtime caching --- assets/js/amp-service-worker-asset-cache.js | 35 --------- includes/class-amp-service-workers.php | 81 +++++++++++++++++++-- 2 files changed, 74 insertions(+), 42 deletions(-) delete mode 100644 assets/js/amp-service-worker-asset-cache.js diff --git a/assets/js/amp-service-worker-asset-cache.js b/assets/js/amp-service-worker-asset-cache.js deleted file mode 100644 index 90d6c8459fb..00000000000 --- a/assets/js/amp-service-worker-asset-cache.js +++ /dev/null @@ -1,35 +0,0 @@ -( () => { - /* - * Use a stale-while-revalidate strategy to cache responses from the AMP CDN and Google Fonts. - * This should eventually be implemented using Workbox. - */ - - const cachedOrigins = new Set( [ - 'https://fonts.googleapis.com', - 'https://cdn.ampproject.org', - 'https://fonts.gstatic.com' - ] ); - - self.addEventListener( 'fetch', ( event ) => { - if ( 'GET' !== event.request.method ) { - return; - } - const url = new URL( event.request.url ); - if ( ! cachedOrigins.has( url.origin ) ) { - return; - } - - // Props jakearchibald: https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/#stale-while-revalidate - event.respondWith( - caches.open( 'amp' ).then( ( cache ) => { - return cache.match( event.request ).then( ( response ) => { - var fetchPromise = fetch( event.request ).then( ( networkResponse ) => { - cache.put( event.request, networkResponse.clone() ); - return networkResponse; - } ); - return response || fetchPromise; - } ); - } ) - ); - } ); -} )(); diff --git a/includes/class-amp-service-workers.php b/includes/class-amp-service-workers.php index 69e0f0f2daa..72ef1319aef 100644 --- a/includes/class-amp-service-workers.php +++ b/includes/class-amp-service-workers.php @@ -22,22 +22,89 @@ class AMP_Service_Workers { * Init. */ public static function init() { - if ( ! function_exists( 'wp_register_service_worker' ) ) { + if ( ! class_exists( 'WP_Service_Workers' ) ) { return; } add_filter( 'query_vars', function( $vars ) { - $vars[] = self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR; + $vars[] = AMP_Service_Workers::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR; return $vars; } ); - wp_register_service_worker( - 'amp-asset-caching', - amp_get_asset_url( 'js/amp-service-worker-asset-cache.js' ) - ); - add_action( 'wp_default_service_workers', array( __CLASS__, 'register_script' ) ); add_action( 'parse_request', array( __CLASS__, 'handle_service_worker_iframe_install' ) ); add_action( 'wp', array( __CLASS__, 'add_install_hooks' ) ); + add_action( 'wp_front_service_worker', array( __CLASS__, 'add_amp_runtime_caching' ) ); + } + + /** + * Configure the front service worker for AMP. + * + * @link https://gist.github.com/sebastianbenz/1d449dee039202d8b7464f1131eae449 + * + * @param WP_Service_Workers $service_workers Service workers. + */ + public static function add_amp_runtime_caching( WP_Service_Workers $service_workers ) { + + // Add AMP scripts to runtime cache which will then get stale-while-revalidate strategy. + $service_workers->register( 'amp-runtime-precaching', array( __CLASS__, 'get_runtime_precache_script' ) ); + + // Serve the AMP Runtime from cache and check for an updated version in the background. + $service_workers->register_precached_route( + '^https:\/\/cdn\.ampproject\.org\/.*', + WP_Service_Workers::STRATEGY_STALE_WHILE_REVALIDATE + ); + } + + /** + * Get runtime precache script. + * + * Note that the PWA plugin handles the precaching of custom logo, custom header, + * and custom background. The PWA plugin also automatically adds runtime caching + * for Google Fonts. The PWA plugin also handles precaching & serving of the + * offline/500 error pages, enabling navigation preload, + * + * @link https://gist.github.com/sebastianbenz/1d449dee039202d8b7464f1131eae449 + * + * @return string Runtime precache script. + */ + public static function get_runtime_precache_script() { + + // List of AMP scripts that we know will be used in WordPress always. + $precached_handles = array( + 'amp-runtime', + 'amp-bind', // Used by comments. + 'amp-form', // Used by comments. + ); + + $theme_support = AMP_Theme_Support::get_theme_support_args(); + if ( ! empty( $theme_support['comments_live_list'] ) ) { + $precached_handles[] = 'amp-live-list'; + } + + if ( amp_get_analytics() ) { + $precached_handles[] = 'amp-analytics'; + } + + $urls = array(); + foreach ( $precached_handles as $handle ) { + if ( wp_script_is( $handle, 'registered' ) ) { + $urls[] = wp_scripts()->registered[ $handle ]->src; + } + } + + ob_start(); + ?> + + ', '' ), '', ob_get_clean() ); } /** From 0d68201acbb4a03b4b0fecb427746a68e81c569e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 24 Aug 2018 16:40:07 -0700 Subject: [PATCH 06/27] Add runtime caching of images --- includes/class-amp-service-workers.php | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/includes/class-amp-service-workers.php b/includes/class-amp-service-workers.php index 72ef1319aef..31247946ff1 100644 --- a/includes/class-amp-service-workers.php +++ b/includes/class-amp-service-workers.php @@ -34,6 +34,7 @@ public static function init() { add_action( 'parse_request', array( __CLASS__, 'handle_service_worker_iframe_install' ) ); add_action( 'wp', array( __CLASS__, 'add_install_hooks' ) ); add_action( 'wp_front_service_worker', array( __CLASS__, 'add_amp_runtime_caching' ) ); + add_action( 'wp_front_service_worker', array( __CLASS__, 'add_image_runtime_caching' ) ); } /** @@ -55,6 +56,32 @@ public static function add_amp_runtime_caching( WP_Service_Workers $service_work ); } + /** + * Configure the front service worker for AMP. + * + * @link https://gist.github.com/sebastianbenz/1d449dee039202d8b7464f1131eae449 + * + * @param WP_Service_Workers $service_workers Service workers. + */ + public static function add_image_runtime_caching( WP_Service_Workers $service_workers ) { + $service_workers->register_cached_route( + '\.(?:png|gif|jpe?g|svg|webp)$', + WP_Service_Workers::STRATEGY_CACHE_FIRST, + array( + 'cacheName' => 'images', + 'plugins' => array( + 'cacheableResponse' => array( + 'statuses' => array( 0, 200 ), + ), + 'expiration' => array( + 'maxEntries' => 60, + 'maxAgeSeconds' => MONTH_IN_SECONDS, + ), + ), + ) + ); + } + /** * Get runtime precache script. * From 02fd0a7b04e29651e0f63d4a5b09ae22210a54e2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 25 Aug 2018 06:40:34 -0700 Subject: [PATCH 07/27] Ensure font stylesheets are requested in CORS mode in both AMP and non-AMP documents --- amp.php | 3 ++ includes/amp-helper-functions.php | 42 +++++++++++++++++++ .../sanitizers/class-amp-style-sanitizer.php | 12 ------ tests/test-amp-style-sanitizer.php | 14 +++++-- tests/test-class-amp-theme-support.php | 7 +--- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/amp.php b/amp.php index 290952e266d..8e5ed459bb9 100644 --- a/amp.php +++ b/amp.php @@ -102,6 +102,9 @@ function amp_deactivate() { // Ensure async and custom-element/custom-template attributes are present on script tags. add_filter( 'script_loader_tag', 'amp_filter_script_loader_tag', PHP_INT_MAX, 2 ); +// Ensure crossorigin=anonymous is added to font links. +add_filter( 'style_loader_tag', 'amp_filter_font_style_loader_tag_with_crossorigin_anonymous', 10, 4 ); + /** * Set up AMP. * diff --git a/includes/amp-helper-functions.php b/includes/amp-helper-functions.php index 13dd181f764..6b4bee6d273 100644 --- a/includes/amp-helper-functions.php +++ b/includes/amp-helper-functions.php @@ -482,6 +482,48 @@ function amp_filter_script_loader_tag( $tag, $handle ) { return $tag; } +/** + * Opt-in to CORS Mode for the font stylesheets. + * + * This ensures that a service worker caching the external stylesheet will not inflate the storage quota. + * This must be done in AMP and non-AMP alike because in paired mode the service worker could cache the + * font stylesheets in a non-AMP document without CORS (crossorigin="anonymous") in which case the + * service worker could then fail to serve the cached font resources in an AMP document with the warning: + * + * > The FetchEvent resulted in a network error response: an "opaque" response was used for a request whose type is not no-cors + * + * @since 1.0 + * @link https://developers.google.com/web/tools/workbox/guides/storage-quota#beware_of_opaque_responses + * @link https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests#cross-origin_requests_and_opaque_responses + * @todo This should be proposed for WordPress core. + * + * @param string $tag Link tag HTML. + * @param string $handle Dependency handle. + * @param string $href Link URL. + * @return string Link tag HTML. + */ +function amp_filter_font_style_loader_tag_with_crossorigin_anonymous( $tag, $handle, $href ) { + static $allowed_font_src_regex = null; + unset( $handle ); + if ( ! $allowed_font_src_regex ) { + $spec_name = 'link rel=stylesheet for fonts'; // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + foreach ( AMP_Allowed_Tags_Generated::get_allowed_tag( 'link' ) as $spec_rule ) { + if ( isset( $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) && $spec_name === $spec_rule[ AMP_Rule_Spec::TAG_SPEC ]['spec_name'] ) { + $allowed_font_src_regex = '@^(' . $spec_rule[ AMP_Rule_Spec::ATTR_SPEC_LIST ]['href']['value_regex'] . ')$@'; + break; + } + } + } + + $href = preg_replace( '#^(http:)?(?=//)#', 'https:', $href ); + + if ( preg_match( $allowed_font_src_regex, $href ) && false === strpos( $tag, 'crossorigin=' ) ) { + $tag = preg_replace( '/(?<=setAttribute( 'href', $normalized_url ); } - /* - * Opt-in to CORS Mode for the stylesheet. This ensures that a service worker caching the external - * stylesheet will not inflate the storage quota. - * - * See: - * - https://developers.google.com/web/tools/workbox/guides/storage-quota#beware_of_opaque_responses - * - https://developers.google.com/web/tools/workbox/guides/handle-third-party-requests#cross-origin_requests_and_opaque_responses - */ - if ( ! $element->hasAttribute( 'crossorigin' ) ) { - $element->setAttribute( 'crossorigin', 'anonymous' ); - } - /* * Make sure rel=preconnect link is present for Google Fonts stylesheet. * Note that core themes normally do this already, per . diff --git a/tests/test-amp-style-sanitizer.php b/tests/test-amp-style-sanitizer.php index 10e6015db33..7b023abc57b 100644 --- a/tests/test-amp-style-sanitizer.php +++ b/tests/test-amp-style-sanitizer.php @@ -1184,12 +1184,16 @@ public function get_font_urls() { /** * Tests that font URLs get validated. * + * @covers \amp_filter_font_style_loader_tag_with_crossorigin_anonymous() * @dataProvider get_font_urls * @param string $url Font URL. * @param array $error_codes Error codes. */ public function test_font_urls( $url, $error_codes ) { - $dom = AMP_DOM_Utils::get_dom( sprintf( '', $url ) ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + $tag = sprintf( '', esc_url( $url ) ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + $tag = amp_filter_font_style_loader_tag_with_crossorigin_anonymous( $tag, 'font', $url ); + + $dom = AMP_DOM_Utils::get_dom( sprintf( '%s', $tag ) ); $validation_errors = array(); @@ -1220,11 +1224,14 @@ public function test_font_urls( $url, $error_codes ) { * Test addition of crossorigin attribute to external stylesheet links. * * @covers AMP_Style_Sanitizer::process_link_element() + * @covers \amp_filter_font_style_loader_tag_with_crossorigin_anonymous() */ public function test_cors_enabled_stylesheet_url() { // Test supplying crossorigin attribute. - $document = AMP_DOM_Utils::get_dom( '' ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + $url = 'https://fonts.googleapis.com/css?family=Tangerine'; + $link = amp_filter_font_style_loader_tag_with_crossorigin_anonymous( "", 'font', $url ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + $document = AMP_DOM_Utils::get_dom( "$link" ); $sanitizer = new AMP_Style_Sanitizer( $document, array( 'use_document_element' => true ) ); $sanitizer->sanitize(); $link = $document->getElementsByTagName( 'link' )->item( 0 ); @@ -1232,7 +1239,8 @@ public function test_cors_enabled_stylesheet_url() { $this->assertEquals( 'anonymous', $link->getAttribute( 'crossorigin' ) ); // Test that existing crossorigin attribute is not overridden. - $document = AMP_DOM_Utils::get_dom( '' ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + $link = amp_filter_font_style_loader_tag_with_crossorigin_anonymous( "", 'font', $url ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet + $document = AMP_DOM_Utils::get_dom( "$link" ); // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet $sanitizer = new AMP_Style_Sanitizer( $document, array( 'use_document_element' => true ) ); $sanitizer->sanitize(); $link = $document->getElementsByTagName( 'link' )->item( 0 ); diff --git a/tests/test-class-amp-theme-support.php b/tests/test-class-amp-theme-support.php index 527708a9989..9488f548dd4 100644 --- a/tests/test-class-amp-theme-support.php +++ b/tests/test-class-amp-theme-support.php @@ -1464,7 +1464,7 @@ public function test_prepare_response() { '', '##s', - '', + '', '#s', '',