You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
12 KiB

<?php
/**
* AMP Service Workers.
*
* @package AMP
* @since 1.1
*/
/**
* Class AMP_Service_Worker.
*/
class AMP_Service_Worker {
/**
* Query var that is used to signal a request to install the service worker in an iframe.
*
* @link https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#data-iframe-src-(optional)
*/
const INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR = 'amp_install_service_worker_iframe';
/**
* Init.
*/
public static function init() {
if ( ! class_exists( 'WP_Service_Workers' ) ) {
return;
}
// Shim support for service worker installation from PWA feature plugin.
add_filter( 'query_vars', [ __CLASS__, 'add_query_var' ] );
add_action( 'parse_request', [ __CLASS__, 'handle_service_worker_iframe_install' ] );
add_action( 'wp', [ __CLASS__, 'add_install_hooks' ] );
$theme_support = AMP_Theme_Support::get_theme_support_args();
if ( isset( $theme_support['service_worker'] ) && false === $theme_support['service_worker'] ) {
return;
}
/*
* The default-enabled options reflect which features are not commented-out in the AMP-by-Example service worker.
* See <https://github.com/ampproject/amp-by-example/blob/e093edb401b1617859b5365e80b639d81b06f058/boilerplate-generator/templates/files/serviceworkerJs.js>.
*/
$enabled_options = [
'cdn_script_caching' => true,
'image_caching' => false,
'google_fonts_caching' => false,
];
if ( isset( $theme_support['service_worker'] ) && is_array( $theme_support['service_worker'] ) ) {
$enabled_options = array_merge(
$enabled_options,
$theme_support['service_worker']
);
}
if ( $enabled_options['cdn_script_caching'] ) {
add_action( 'wp_front_service_worker', [ __CLASS__, 'add_cdn_script_caching' ] );
}
if ( $enabled_options['image_caching'] ) {
add_action( 'wp_front_service_worker', [ __CLASS__, 'add_image_caching' ] );
}
if ( $enabled_options['google_fonts_caching'] ) {
add_action( 'wp_front_service_worker', [ __CLASS__, 'add_google_fonts_caching' ] );
}
}
/**
* Add query var for iframe service worker request.
*
* @param array $vars Query vars.
* @return array Amended query vars.
*/
public static function add_query_var( $vars ) {
$vars[] = self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR;
return $vars;
}
/**
* Add runtime caching for scripts loaded from the AMP CDN with a stale-while-revalidate strategy.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js
*
* @param WP_Service_Worker_Scripts $service_workers Service worker registry.
*/
public static function add_cdn_script_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
/* translators: %s: WP_Service_Worker_Cache_Registry. */
_doing_it_wrong( __METHOD__, sprintf( esc_html__( 'Please update to PWA v0.2. Expected argument to be %s.', 'amp' ), 'WP_Service_Worker_Cache_Registry' ), '1.1' );
return;
}
// Add AMP scripts to runtime cache which will then get stale-while-revalidate strategy.
$service_workers->register(
'amp-cdn-runtime-caching',
static function() {
$urls = AMP_Service_Worker::get_precached_script_cdn_urls();
if ( empty( $urls ) ) {
return '';
}
$js = file_get_contents( AMP__DIR__ . '/assets/js/amp-service-worker-runtime-precaching.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_read_file_get_contents
$js = preg_replace( '#/\*\s*global.+?\*/#', '', $js );
$js = str_replace(
'URLS',
wp_json_encode( $urls ),
$js
);
return $js;
}
);
// Serve the AMP Runtime from cache and check for an updated version in the background. See <https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L54-L58>.
$service_workers->caching_routes()->register(
'^https:\/\/cdn\.ampproject\.org\/.*',
[
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE,
]
);
}
/**
* Add runtime image caching from the origin with a cache-first strategy.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L60-L74
*
* @param WP_Service_Worker_Scripts $service_workers Service workers.
*/
public static function add_image_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Please update to PWA v0.2. Expected argument to be WP_Service_Worker_Scripts.', 'amp' ), '1.1' );
return;
}
$service_workers->caching_routes()->register(
'^' . preg_quote( set_url_scheme( content_url( '/' ), 'https' ), '/' ) . '[^\?]+?\.(?:png|gif|jpg|jpeg|svg|webp)(\?.*)?$',
[
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
'cacheName' => 'images',
'plugins' => [
'cacheableResponse' => [
'statuses' => [ 0, 200 ],
],
'expiration' => [
'maxEntries' => 60,
'maxAgeSeconds' => MONTH_IN_SECONDS,
],
],
]
);
}
/**
* Add runtime caching of Google Fonts with stale-while-revalidate strategy for stylesheets and cache-first strategy for webfont files.
*
* @link https://developers.google.com/web/tools/workbox/guides/common-recipes#google_fonts
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L76-L103
* @link https://github.com/xwp/pwa-wp/blob/master/integrations/class-wp-service-worker-fonts-integration.php
*
* @param WP_Service_Worker_Scripts $service_workers Service workers.
*/
public static function add_google_fonts_caching( $service_workers ) {
if ( ! ( $service_workers instanceof WP_Service_Worker_Scripts ) ) {
_doing_it_wrong( __METHOD__, esc_html__( 'Please update to PWA v0.2. Expected argument to be WP_Service_Worker_Scripts.', 'amp' ), '1.1' );
return;
}
// The PWA plugin also automatically adds runtime caching for Google Fonts when WP_SERVICE_WORKER_INTEGRATIONS_ENABLED is set.
if ( class_exists( 'WP_Service_Worker_Fonts_Integration' ) ) {
return;
}
// Cache the Google Fonts stylesheets with a stale while revalidate strategy.
$service_workers->caching_routes()->register(
'^https:\/\/fonts\.googleapis\.com',
[
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_STALE_WHILE_REVALIDATE,
'cacheName' => 'google-fonts-stylesheets',
]
);
// Cache the Google Fonts webfont files with a cache first strategy for 1 year.
$service_workers->caching_routes()->register(
'^https:\/\/fonts\.gstatic\.com',
[
'strategy' => WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
'cacheName' => 'google-fonts-webfonts',
'plugins' => [
'cacheableResponse' => [
'statuses' => [ 0, 200 ],
],
'expiration' => [
'maxAgeSeconds' => YEAR_IN_SECONDS,
'maxEntries' => 30,
],
],
]
);
}
/**
* Register URLs that will be precached in the runtime cache. (Yes, this sounds somewhat strange.)
*
* Note that the PWA plugin handles the precaching of custom logo, custom header,
* and custom background. The PWA plugin also handles precaching & serving of the
* offline/500 error pages and enabling navigation preload.
*
* @link https://github.com/ampproject/amp-by-example/blob/4593af61609898043302a101826ddafe7206bfd9/boilerplate-generator/templates/files/serviceworkerJs.js#L9-L22
* @see AMP_Service_Worker::add_cdn_script_caching()
*
* @return array Runtime pre-cached URLs.
*/
public static function get_precached_script_cdn_urls() {
// List of AMP scripts that we know will be used in WordPress always.
$precached_handles = [
'amp-runtime',
'amp-bind', // Used by comments.
'amp-form', // Used by comments.
'amp-install-serviceworker',
];
$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 = [];
foreach ( $precached_handles as $handle ) {
if ( wp_script_is( $handle, 'registered' ) ) {
$urls[] = wp_scripts()->registered[ $handle ]->src;
}
}
return $urls;
}
/**
* Add hooks to install the service worker from AMP page.
*/
public static function add_install_hooks() {
if ( current_theme_supports( 'amp' ) && is_amp_endpoint() ) {
add_action( 'wp_footer', [ __CLASS__, 'install_service_worker' ] );
// Prevent validation error due to the script that installs the service worker on non-AMP pages.
foreach ( [ 'wp_print_scripts', 'wp_print_footer_scripts' ] as $action ) {
$priority = has_action( $action, 'wp_print_service_workers' );
if ( false !== $priority ) {
remove_action( $action, 'wp_print_service_workers', $priority );
}
}
}
// Reader mode integration.
add_action( 'amp_post_template_footer', [ __CLASS__, 'install_service_worker' ] );
add_filter(
'amp_post_template_data',
static function ( $data ) {
$data['amp_component_scripts']['amp-install-serviceworker'] = true;
return $data;
}
);
}
/**
* Install service worker(s).
*
* @since 1.1
* @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;
}
$src = wp_get_service_worker_url( WP_Service_Workers::SCOPE_FRONT );
$iframe_src = add_query_arg(
self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR,
WP_Service_Workers::SCOPE_FRONT,
home_url( '/', 'https' )
);
?>
<amp-install-serviceworker
src="<?php echo esc_url( $src ); ?>"
data-iframe-src="<?php echo esc_url( $iframe_src ); ?>"
layout="nodisplay"
>
</amp-install-serviceworker>
<?php
}
/**
* Handle request to install service worker via iframe.
*
* @see wp_print_service_workers()
* @link https://www.ampproject.org/docs/reference/components/amp-install-serviceworker#data-iframe-src-(optional)
*/
public static function handle_service_worker_iframe_install() {
if ( ! isset( $GLOBALS['wp']->query_vars[ self::INSTALL_SERVICE_WORKER_IFRAME_QUERY_VAR ] ) ) {
return;
}
$scope = (int) $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' ),
[ 'response' => 404 ]
);
}
$front_scope = home_url( '/', 'relative' );
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><?php esc_html_e( 'Service Worker Installation', 'amp' ); ?></title>
</head>
<body>
<?php esc_html_e( 'Installing service worker...', 'amp' ); ?>
<?php
printf(
'<script>navigator.serviceWorker.register( %s, %s );</script>',
wp_json_encode( wp_get_service_worker_url( $scope ) ),
wp_json_encode( [ 'scope' => $front_scope ] )
);
?>
</body>
</html>
<?php
// Die in a way that can be unit tested.
add_filter(
'wp_die_handler',
static function() {
return static function() {
die();
};
},
1
);
wp_die();
}
}