<?php
/**
 * AMP Service Workers.
 *
 * @package AMP
 * @since 1.1
 */

/**
 * Class AMP_Service_Worker.
 *
 * @internal
 */
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' ] );
		}
	}

	/**
	 * Register a caching route.
	 *
	 * @param WP_Service_Worker_Scripts $service_workers Service workers.
	 * @param string                    $route           Route.
	 * @param string                    $strategy        Strategy name.
	 * @param array                     $args            Strategy args.
	 * @param array                     $plugins         Plugins.
	 */
	private static function register_caching_route( WP_Service_Worker_Scripts $service_workers, $route, $strategy, $args = [], $plugins = [] ) {
		$caching_routes = $service_workers->caching_routes();
		if ( defined( 'PWA_VERSION' ) && version_compare( PWA_VERSION, '0.6', '<' ) ) {
			$args['strategy'] = $strategy;
			$args['plugins']  = $plugins;
			$caching_routes->register( $route, $args );
		} else {
			$args = array_merge( $args, $plugins );
			$caching_routes->register( $route, $strategy, $args );
		}
	}

	/**
	 * 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>.
		self::register_caching_route(
			$service_workers,
			'^https:\/\/cdn\.ampproject\.org\/.*',
			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;
		}

		self::register_caching_route(
			$service_workers,
			'^' . preg_quote( set_url_scheme( content_url( '/' ), 'https' ), '/' ) . '[^\?]+?\.(?:png|gif|jpg|jpeg|svg|webp)(\?.*)?$',
			WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
			[
				'cacheName' => 'images',
			],
			[
				'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/GoogleChromeLabs/pwa-wp/blob/d0eb52a2f348259123f39941093813f1351c0e21/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.
		self::register_caching_route(
			$service_workers,
			'^https:\/\/fonts\.googleapis\.com',
			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.
		self::register_caching_route(
			$service_workers,
			'^https:\/\/fonts\.gstatic\.com',
			WP_Service_Worker_Caching_Routes::STRATEGY_CACHE_FIRST,
			[
				'cacheName' => 'google-fonts-webfonts',
			],
			[
				'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 ( ! amp_is_request() ) {
			return;
		}

		// 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 );
			}
		}

		add_action( 'wp_footer', [ __CLASS__, 'install_service_worker' ] );
		add_action( 'amp_post_template_footer', [ __CLASS__, 'install_service_worker' ] );
	}

	/**
	 * 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>
				<meta name="robots" content="noindex">
			</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();
	}
}
