diff --git a/pwa.php b/pwa.php index b1f19d415..1390cec9d 100644 --- a/pwa.php +++ b/pwa.php @@ -72,6 +72,7 @@ require_once PWA_PLUGIN_DIR . '/wp-includes/integrations/class-wp-service-worker-scripts-integration.php'; require_once PWA_PLUGIN_DIR . '/wp-includes/integrations/class-wp-service-worker-styles-integration.php'; require_once PWA_PLUGIN_DIR . '/wp-includes/integrations/class-wp-service-worker-fonts-integration.php'; +require_once PWA_PLUGIN_DIR . '/wp-includes/integrations/class-wp-service-worker-admin-assets-integration.php'; /** WordPress Service Worker Functions */ require_once PWA_PLUGIN_DIR . '/wp-includes/service-workers.php'; diff --git a/tests/test-class-wp-service-workers.php b/tests/test-class-wp-service-workers.php index fd415ae86..ed485a976 100644 --- a/tests/test-class-wp-service-workers.php +++ b/tests/test-class-wp-service-workers.php @@ -64,7 +64,7 @@ public function test_serve_request_front() { add_action( 'wp_admin_service_worker', array( $this, 'register_foo_sw' ) ); ob_start(); - wp_service_workers()->serve_request( WP_Service_Workers::SCOPE_FRONT ); + wp_service_workers()->serve_request(); $output = ob_get_clean(); $this->assertContains( $this->return_foo_sw(), $output ); $this->assertContains( $this->return_bar_sw(), $output ); @@ -87,7 +87,8 @@ public function test_serve_request_admin() { add_action( 'wp_admin_service_worker', array( $this, 'register_foo_sw' ) ); ob_start(); - wp_service_workers()->serve_request( WP_Service_Workers::SCOPE_ADMIN ); + set_current_screen( 'admin-ajax' ); + wp_service_workers()->serve_request(); $output = ob_get_clean(); $this->assertContains( $this->return_foo_sw(), $output ); @@ -98,17 +99,6 @@ public function test_serve_request_admin() { ); } - /** - * Test serve_request for invalid scope. - * - * @covers WP_Service_Workers::serve_request() - */ - public function test_serve_request_invalid_scope() { - ob_start(); - wp_service_workers()->serve_request( 'bad' ); - $this->assertContains( 'invalid_scope_requested', ob_get_clean() ); - } - /** * Test serve_request for bad src callback. * @@ -125,7 +115,8 @@ public function test_serve_request_bad_src_callback() { ); ob_start(); - wp_service_workers()->serve_request( WP_Service_Workers::SCOPE_ADMIN ); + set_current_screen( 'admin-ajax' ); + wp_service_workers()->serve_request(); $output = ob_get_clean(); $this->assertContains( 'Service worker src is invalid', $output ); @@ -147,7 +138,7 @@ public function test_serve_request_bad_src_url() { ); ob_start(); - wp_service_workers()->serve_request( WP_Service_Workers::SCOPE_FRONT ); + wp_service_workers()->serve_request(); $output = ob_get_clean(); $this->assertContains( 'Service worker src is invalid', $output ); diff --git a/wp-includes/class-wp-service-worker-scripts.php b/wp-includes/class-wp-service-worker-scripts.php index 489e2866f..abfedc90d 100644 --- a/wp-includes/class-wp-service-worker-scripts.php +++ b/wp-includes/class-wp-service-worker-scripts.php @@ -13,8 +13,8 @@ * * @see WP_Dependencies * - * @method WP_Service_Worker_Precaching_Routes_Component precaching_routes() - * @method WP_Service_Worker_Caching_Routes_Component caching_routes() + * @method WP_Service_Worker_Precaching_Routes precaching_routes() + * @method WP_Service_Worker_Caching_Routes caching_routes() */ class WP_Service_Worker_Scripts extends WP_Scripts implements WP_Service_Worker_Registry { diff --git a/wp-includes/class-wp-service-workers.php b/wp-includes/class-wp-service-workers.php index 47c7e78f8..2893d4856 100644 --- a/wp-includes/class-wp-service-workers.php +++ b/wp-includes/class-wp-service-workers.php @@ -78,31 +78,19 @@ public function get_registry() { /** * Get the current scope for the service worker request. * - * @global WP $wp - * - * @return int Scope. Either SCOPE_FRONT, SCOPE_ADMIN, or if neither then 0. + * @todo We don't really need this. A simple call to is_admin() is all that is required. + * @return int Scope. Either SCOPE_FRONT or SCOPE_ADMIN. */ public function get_current_scope() { - global $wp; - if ( ! isset( $wp->query_vars[ self::QUERY_VAR ] ) || ! is_numeric( $wp->query_vars[ self::QUERY_VAR ] ) ) { - return 0; - } - $scope = (int) $wp->query_vars[ self::QUERY_VAR ]; - if ( self::SCOPE_FRONT === $scope ) { - return self::SCOPE_FRONT; - } elseif ( self::SCOPE_ADMIN === $scope ) { - return self::SCOPE_ADMIN; - } - return 0; + return is_admin() ? self::SCOPE_ADMIN : self::SCOPE_FRONT; } /** * Get service worker logic for scope. * * @see wp_service_worker_loaded() - * @param int $scope Scope of the Service Worker. */ - public function serve_request( $scope ) { + public function serve_request() { /* * Per Workbox : * "Generally, most developers will want to set the Cache-Control header to no-cache, @@ -114,7 +102,7 @@ public function serve_request( $scope ) { @header( 'Content-Type: text/javascript; charset=utf-8' ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged, WordPress.PHP.NoSilencedErrors.Discouraged - if ( self::SCOPE_FRONT === $scope ) { + if ( ! is_admin() ) { wp_enqueue_scripts(); /** @@ -134,7 +122,13 @@ public function serve_request( $scope ) { * @param WP_Service_Worker_Scripts $scripts Instance to register service worker behavior with. */ do_action( 'wp_front_service_worker', $this->scripts ); - } elseif ( self::SCOPE_ADMIN === $scope ) { + } else { + $hook_name = 'service-worker'; + set_current_screen( $hook_name ); + + /** This action is documented in wp-admin/admin-header.php */ + do_action( 'admin_enqueue_scripts', $hook_name ); + /** * Fires before serving the wp-admin service worker, when its scripts should be registered, caching routes established, and assets precached. * @@ -145,13 +139,7 @@ public function serve_request( $scope ) { do_action( 'wp_admin_service_worker', $this->scripts ); } - if ( self::SCOPE_FRONT !== $scope && self::SCOPE_ADMIN !== $scope ) { - status_header( 400 ); - echo '/* invalid_scope_requested */'; - return; - } - - printf( "/* PWA v%s */\n\n", esc_html( PWA_VERSION ) ); + printf( "/* PWA v%s-%s */\n\n", esc_html( PWA_VERSION ), is_admin() ? 'admin' : 'front' ); ob_start(); $this->scripts->do_items( array_keys( $this->scripts->registered ) ); diff --git a/wp-includes/components/class-wp-service-worker-error-response-component.php b/wp-includes/components/class-wp-service-worker-error-response-component.php index 0dfd958cc..f728960c6 100644 --- a/wp-includes/components/class-wp-service-worker-error-response-component.php +++ b/wp-includes/components/class-wp-service-worker-error-response-component.php @@ -39,8 +39,7 @@ public function serve( WP_Service_Worker_Scripts $scripts ) { // Ensure the user-specific offline/500 pages are precached, and thet they update when user logs out or switches to another user. $revision .= sprintf( ';user-%d', get_current_user_id() ); - $current_scope = wp_service_workers()->get_current_scope(); - if ( WP_Service_Workers::SCOPE_FRONT === $current_scope ) { + if ( ! is_admin() ) { $offline_error_template_file = pwa_locate_template( array( 'offline.php', 'error.php' ) ); $offline_error_precache_entry = array( 'url' => add_query_arg( 'wp_error_template', 'offline', home_url( '/' ) ), @@ -117,7 +116,7 @@ public function serve( WP_Service_Worker_Scripts $scripts ) { } $blacklist_patterns = array(); - if ( WP_Service_Workers::SCOPE_FRONT === $current_scope ) { + if ( ! is_admin() ) { $blacklist_patterns[] = '^' . preg_quote( untrailingslashit( wp_parse_url( admin_url(), PHP_URL_PATH ) ), '/' ) . '($|\?.*|/.*)'; } diff --git a/wp-includes/components/class-wp-service-worker-precaching-routes.php b/wp-includes/components/class-wp-service-worker-precaching-routes.php index fa6deb343..dc5e24387 100644 --- a/wp-includes/components/class-wp-service-worker-precaching-routes.php +++ b/wp-includes/components/class-wp-service-worker-precaching-routes.php @@ -45,6 +45,29 @@ public function register( $url, $args = array() ) { ); } + /** + * Register Emoji script. + * + * Short-circuit if SCRIPT_DEBUG (hence file not built) or print_emoji_detection_script has been removed. + * + * @since 0.2 + * + * @return bool Whether emoji script was registered. + */ + public function register_emoji_script() { + if ( SCRIPT_DEBUG || false === has_action( is_admin() ? 'admin_print_scripts' : 'wp_head', 'print_emoji_detection_script' ) ) { + return false; + } + + $this->register( + includes_url( 'js/wp-emoji-release.min.js' ), + array( + 'revision' => get_bloginfo( 'version' ), + ) + ); + return true; + } + /** * Gets all registered routes. * diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php index 824fc5bdb..d89a1d04a 100644 --- a/wp-includes/default-filters.php +++ b/wp-includes/default-filters.php @@ -13,8 +13,12 @@ } add_action( 'parse_query', 'wp_service_worker_loaded' ); +add_action( 'wp_ajax_wp_service_worker', 'wp_ajax_wp_service_worker' ); +add_action( 'wp_ajax_nopriv_wp_service_worker', 'wp_ajax_wp_service_worker' ); add_action( 'parse_query', 'wp_hide_admin_bar_offline' ); add_action( 'wp_head', 'wp_add_error_template_no_robots' ); add_action( 'error_head', 'wp_add_error_template_no_robots' ); add_action( 'wp_default_service_workers', 'wp_default_service_workers' ); + +add_action( 'admin_init', 'wp_disable_script_concatenation' ); diff --git a/wp-includes/integrations/class-wp-service-worker-admin-assets-integration.php b/wp-includes/integrations/class-wp-service-worker-admin-assets-integration.php new file mode 100644 index 000000000..cf6335c17 --- /dev/null +++ b/wp-includes/integrations/class-wp-service-worker-admin-assets-integration.php @@ -0,0 +1,148 @@ +flag_admin_assets_with_precache( wp_scripts()->registered ); + $this->flag_admin_assets_with_precache( wp_styles()->registered ); + + $routes = array_merge( + $this->get_routes_from_file_list( $admin_images, 'wp-admin' ), + $this->get_routes_from_file_list( $inc_images, 'wp-includes' ), + $this->get_woff_file_list(), + $this->get_tinymce_file_list() + ); + + foreach ( $routes as $options ) { + if ( isset( $options['url'] ) ) { + $url = $options['url']; + unset( $options['url'] ); + $scripts->precaching_routes()->register( $url, $options ); + } + } + } + + /** + * Defines the scope of this integration by setting `$this->scope`. + * + * @since 0.2 + */ + protected function define_scope() { + $this->scope = WP_Service_Workers::SCOPE_ADMIN; + } + + /** + * Flags admin assets with precache. + * + * @param _WP_Dependency[] $dependencies Array of _WP_Dependency objects. + * @return array Array of routes. + */ + protected function flag_admin_assets_with_precache( $dependencies ) { + $routes = array(); + foreach ( $dependencies as $handle => $params ) { + + // Only precache scripts from wp-admin and wp-includes (and Gutenberg). + if ( preg_match( '#/(wp-admin|wp-includes|wp-content/plugins/gutenberg)/#', $params->src ) ) { + $params->add_data( 'precache', true ); + } + } + return $routes; + } + + /** + * Get static list of .woff files to precache. + * + * @todo These should be also available to the frontend. So this should go into WP_Service_Worker_Precaching_Routes. + * @return array + */ + protected function get_woff_file_list() { + return array( + array( + 'revision' => get_bloginfo( 'version' ), + 'url' => '/wp-includes/fonts/dashicons.woff', + ), + array( + 'revision' => get_bloginfo( 'version' ), + 'url' => '/wp-includes/js/tinymce/skins/lightgray/fonts/tinymce-small.woff', + ), + array( + 'revision' => get_bloginfo( 'version' ), + 'url' => '/wp-includes/js/tinymce/skins/lightgray/fonts/tinymce.woff', + ), + ); + } + + /** + * Get list of TinyMCE files for precaching. + * + * @return array Routes. + */ + protected function get_tinymce_file_list() { + global $tinymce_version; + $tinymce_routes = array(); + $tinymce_files = list_files( ABSPATH . WPINC . '/js/tinymce/' ); + + foreach ( $tinymce_files as $tinymce_file ) { + $basename = basename( $tinymce_file ); + if ( preg_match( '#\.min\.(css|js)$#', $tinymce_file ) ) { + $url = includes_url( preg_replace( '/.*' . WPINC . '/', '', $tinymce_file ) ); + + $tinymce_routes[] = array( + 'url' => $url, + 'revision' => $tinymce_version, + ); + } + } + return $tinymce_routes; + } + + /** + * Get routes from file paths list. + * + * @param array $list List of file paths. + * @param string $folder Folder -- either 'wp-admin' or 'wp-includes'. + * @return array List of routes. + */ + protected function get_routes_from_file_list( $list, $folder ) { + $routes = array(); + foreach ( $list as $filename ) { + $ext = pathinfo( $filename, PATHINFO_EXTENSION ); + if ( ! in_array( $ext, array( 'png', 'gif', 'svg' ), true ) ) { + continue; + } + + $routes[] = array( + 'url' => strstr( $filename, '/' . $folder ), + 'revision' => get_bloginfo( 'version' ), + ); + } + + return $routes; + } +} diff --git a/wp-includes/integrations/class-wp-service-worker-base-integration.php b/wp-includes/integrations/class-wp-service-worker-base-integration.php index 4513c5f9b..f4adc5f27 100644 --- a/wp-includes/integrations/class-wp-service-worker-base-integration.php +++ b/wp-includes/integrations/class-wp-service-worker-base-integration.php @@ -77,4 +77,27 @@ protected function get_attachment_image_urls( $attachment_id, $image_size ) { return array(); } + + /** + * Determines whether a URL is for a local file. + * + * @since 0.2 + * + * @param string $url URL. + * @return bool Whether local. + */ + protected function is_local_file_url( $url ) { + $re_proto = '#^\w+(?://)#'; + $url = preg_replace( $re_proto, '', $url ); + $home_url = preg_replace( $re_proto, '', home_url( '/' ) ); + $site_url = preg_replace( $re_proto, '', site_url( '/' ) ); + + return ( + '/' === substr( $url, 0, 1 ) + || + substr( $url, 0, strlen( $home_url ) ) === $home_url + || + substr( $url, 0, strlen( $site_url ) ) === $site_url + ); + } } diff --git a/wp-includes/integrations/class-wp-service-worker-scripts-integration.php b/wp-includes/integrations/class-wp-service-worker-scripts-integration.php index bbc6c1fde..51c763f83 100644 --- a/wp-includes/integrations/class-wp-service-worker-scripts-integration.php +++ b/wp-includes/integrations/class-wp-service-worker-scripts-integration.php @@ -73,10 +73,13 @@ public function register( WP_Service_Worker_Scripts $scripts ) { /** This filter is documented in wp-includes/class.wp-scripts.php */ $url = apply_filters( 'script_loader_src', $url, $handle ); - if ( $url ) { + if ( $url && $this->is_local_file_url( $url ) ) { $scripts->precaching_routes()->register( $url, $revision ); } } + + $scripts->precaching_routes()->register_emoji_script(); + wp_scripts()->to_do = $original_to_do; // Restore original scripts to do. } diff --git a/wp-includes/integrations/class-wp-service-worker-styles-integration.php b/wp-includes/integrations/class-wp-service-worker-styles-integration.php index 8a6bc921f..f4f4d8fc9 100644 --- a/wp-includes/integrations/class-wp-service-worker-styles-integration.php +++ b/wp-includes/integrations/class-wp-service-worker-styles-integration.php @@ -73,7 +73,7 @@ public function register( WP_Service_Worker_Scripts $scripts ) { /** This filter is documented in wp-includes/class.wp-styles.php */ $url = apply_filters( 'style_loader_src', $url, $handle ); - if ( $url ) { + if ( $url && $this->is_local_file_url( $url ) ) { $scripts->precaching_routes()->register( $url, $revision ); } } diff --git a/wp-includes/js/service-worker-precaching.js b/wp-includes/js/service-worker-precaching.js index 934b5029c..93aadc2e0 100644 --- a/wp-includes/js/service-worker-precaching.js +++ b/wp-includes/js/service-worker-precaching.js @@ -8,6 +8,7 @@ wp.serviceWorker.precaching.addRoute( { ignoreUrlParametersMatching: [ /^utm_/, + /^wp-mce-/, /^ver$/ ] // @todo Add urlManipulation which allows for the list of ignoreUrlParametersMatching to be supplied with each entry. diff --git a/wp-includes/service-workers.php b/wp-includes/service-workers.php index 320e49605..4a0f231f3 100644 --- a/wp-includes/service-workers.php +++ b/wp-includes/service-workers.php @@ -91,10 +91,17 @@ function wp_get_service_worker_url( $scope = WP_Service_Workers::SCOPE_FRONT ) { $scope = WP_Service_Workers::SCOPE_FRONT; } - return add_query_arg( - array( WP_Service_Workers::QUERY_VAR => $scope ), - home_url( '/', 'https' ) - ); + if ( WP_Service_Workers::SCOPE_FRONT === $scope ) { + return add_query_arg( + array( WP_Service_Workers::QUERY_VAR => $scope ), + home_url( '/', 'https' ) + ); + } else { + return add_query_arg( + array( 'action' => WP_Service_Workers::QUERY_VAR ), + admin_url( 'admin-ajax.php' ) + ); + } } /** @@ -131,7 +138,9 @@ function wp_print_service_workers() { navigator.serviceWorker.register( , - ); + ).then( function() { + document.cookie = 'wordpress_sw_installed=1; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT; secure; samesite=strict'; + } ); } ); } @@ -170,19 +179,33 @@ function wp_print_service_worker_error_details_script( $callback ) { } /** - * If it's a service worker script page, display that. + * Serve the service worker for the frontend if requested. * * @since 0.1 * @see rest_api_loaded() + * @see wp_ajax_wp_service_worker() */ function wp_service_worker_loaded() { - $scope = wp_service_workers()->get_current_scope(); - if ( 0 !== $scope ) { - wp_service_workers()->serve_request( $scope ); + global $wp; + if ( isset( $wp->query_vars[ WP_Service_Workers::QUERY_VAR ] ) ) { + wp_service_workers()->serve_request(); exit; } } +/** + * Serve admin service worker. + * + * This will be moved to wp-admin/includes/admin-actions.php + * + * @since 0.2 + * @see wp_service_worker_loaded() + */ +function wp_ajax_wp_service_worker() { + wp_service_workers()->serve_request(); + exit; +} + /** * JSON-encodes with pretty printing. * @@ -217,6 +240,10 @@ function wp_default_service_workers( $scripts ) { 'wp-fonts' => new WP_Service_Worker_Fonts_Integration(), ); + if ( ! SCRIPT_DEBUG ) { + $integrations['wp-admin-assets'] = new WP_Service_Worker_Admin_Assets_Integration(); + } + /** * Filters the service worker integrations to initialize. * @@ -270,3 +297,24 @@ function wp_default_service_workers( $scripts ) { } } } + +/** + * Disables concatenating scripts to leverage caching the assets via Service Worker instead. + */ +function wp_disable_script_concatenation() { + global $concatenate_scripts; + + /* + * This cookie is set when the service worker registers successfully, avoiding unnecessary result + * for browsers that don't support service workers. Note that concatenation only applies in the admin, + * for authenticated users without full-page caching. + */ + if ( isset( $_COOKIE['wordpress_sw_installed'] ) ) { + $concatenate_scripts = false; // WPCS: Override OK. + } + + // @todo This is just here for debugging purposes. + if ( isset( $_GET['wp_concatenate_scripts'] ) ) { // WPCS: csrf ok. + $concatenate_scripts = rest_sanitize_boolean( $_GET['wp_concatenate_scripts'] ); // WPCS: csrf ok, override ok. + } +}