diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index c8643749fb..06a973b0bb 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -43,6 +43,9 @@ add_action( 'admin_head', 'wp_color_scheme_settings' ); add_action( 'admin_head', 'wp_site_icon' ); add_action( 'admin_head', '_ipad_meta' ); +// Prerendering. +add_filter( 'admin_head', 'wp_resource_hints' ); + add_action( 'admin_print_scripts-post.php', 'wp_page_reload_on_back_button_js' ); add_action( 'admin_print_scripts-post-new.php', 'wp_page_reload_on_back_button_js' ); diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 70da2f2e2a..fe716165fa 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -239,6 +239,7 @@ add_action( 'wp_head', 'wp_print_styles', 8 ); add_action( 'wp_head', 'wp_print_head_scripts', 9 ); add_action( 'wp_head', 'wp_generator' ); add_action( 'wp_head', 'rel_canonical' ); +add_action( 'wp_head', 'wp_resource_hints' ); add_action( 'wp_head', 'wp_shortlink_wp_head', 10, 0 ); add_action( 'wp_head', 'wp_site_icon', 99 ); add_action( 'wp_footer', 'wp_print_footer_scripts', 20 ); diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php index db0dfafa74..222f248252 100644 --- a/src/wp-includes/general-template.php +++ b/src/wp-includes/general-template.php @@ -2787,6 +2787,97 @@ function wp_site_icon() { } } +/** + * Prints resource hints to browsers for pre-fetching, pre-rendering and pre-connecting to web sites. + * + * Gives hints to browsers to prefetch specific pages or render them in the background, + * to perform DNS lookups or to begin the connection handshake (DNS, TCP, TLS) in the background. + * + * These performance improving indicators work by using ``. + * + * @since 4.6.0 + */ +function wp_resource_hints() { + $hints = array( + 'dns-prefetch' => wp_resource_hints_scripts_styles(), + 'preconnect' => array( 's.w.org' ), + 'prefetch' => array(), + 'prerender' => array(), + ); + + foreach ( $hints as $relation_type => $urls ) { + /** + * Filters domains and URLs for resource hints. + * + * @since 4.6.0 + * + * @param array $urls URLs to print for resource hints. + * @param string $relation_type The relation type the URLs are printed for, e.g. 'preconnect' or 'prerender'. + */ + $urls = apply_filters( 'wp_resource_hints', $urls, $relation_type ); + $urls = array_unique( $urls ); + + foreach ( $urls as $url ) { + $url = esc_url( $url, array( 'http', 'https' ) ); + + if ( in_array( $relation_type, array( 'preconnect', 'dns-prefetch' ) ) ) { + $parsed = parse_url( $url ); + + if ( ! empty( $parsed['scheme'] ) ) { + $url = $parsed['scheme'] . '://' . $parsed['host']; + } else { + $url = $parsed['host']; + } + } + + printf( "\r\n", $relation_type, $url ); + } + } +} + +/** + * Adds dns-prefetch for all scripts and styles enqueued from external hosts. + * + * @since 4.6.0 + */ +function wp_resource_hints_scripts_styles() { + global $wp_scripts, $wp_styles; + + $unique_hosts = array(); + + if ( is_object( $wp_scripts ) && ! empty( $wp_scripts->registered ) ) { + foreach ( $wp_scripts->registered as $registered_script ) { + $src = $registered_script->src; + // Make sure the URL has a scheme, otherwise parse_url() could fail to pass the host. + if ( '//' == substr( $src, 0, 2 ) ) { + $src = set_url_scheme( $src ); + } + + $this_host = parse_url( $src, PHP_URL_HOST ); + if ( ! empty( $this_host ) && ! in_array( $this_host, $unique_hosts ) && $this_host !== $_SERVER['SERVER_NAME'] ) { + $unique_hosts[] = $this_host; + } + } + } + + if ( is_object( $wp_styles ) && ! empty( $wp_styles->registered ) ) { + foreach ( $wp_styles->registered as $registered_style ) { + $src = $registered_style->src; + // Make sure the URL has a scheme, otherwise parse_url() could fail to pass the host. + if ( '//' == substr( $src, 0, 2 ) ) { + $src = set_url_scheme( $src ); + } + + $this_host = parse_url( $src, PHP_URL_HOST ); + if ( ! empty( $this_host ) && ! in_array( $this_host, $unique_hosts ) && $this_host !== $_SERVER['SERVER_NAME'] ) { + $unique_hosts[] = $this_host; + } + } + } + + return $unique_hosts; +} + /** * Whether the user should have a WYSIWIG editor. * diff --git a/tests/phpunit/tests/general/resourceHints.php b/tests/phpunit/tests/general/resourceHints.php new file mode 100644 index 0000000000..5f0dc354d1 --- /dev/null +++ b/tests/phpunit/tests/general/resourceHints.php @@ -0,0 +1,152 @@ +old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null; + $this->old_wp_styles = isset( $GLOBALS['wp_styles'] ) ? $GLOBALS['wp_styles'] : null; + + remove_action( 'wp_default_scripts', 'wp_default_scripts' ); + remove_action( 'wp_default_styles', 'wp_default_styles' ); + + $GLOBALS['wp_scripts'] = new WP_Scripts(); + $GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' ); + $GLOBALS['wp_styles'] = new WP_Styles(); + $GLOBALS['wp_styles']->default_version = get_bloginfo( 'version' ); + } + + function tearDown() { + $GLOBALS['wp_scripts'] = $this->old_wp_scripts; + $GLOBALS['wp_styles'] = $this->old_wp_styles; + add_action( 'wp_default_scripts', 'wp_default_scripts' ); + add_action( 'wp_default_styles', 'wp_default_styles' ); + parent::tearDown(); + } + + function test_should_have_defaults_on_frontend() { + $expected = "\r\n"; + + $this->expectOutputString( $expected ); + + wp_resource_hints(); + } + + function test_dns_prefetching() { + $expected = "\r\n" . + "\r\n" . + "\r\n" . + "\r\n"; + + add_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_domains' ), 10, 2 ); + + $actual = get_echo( 'wp_resource_hints' ); + + remove_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_domains' ) ); + + $this->assertEquals( $expected, $actual ); + } + + function _add_dns_prefetch_domains( $hints, $method ) { + if ( 'dns-prefetch' === $method ) { + $hints[] = 'http://wordpress.org'; + $hints[] = 'https://google.com'; + $hints[] = '//make.wordpress.org'; + } + + return $hints; + } + + function test_prerender() { + $expected = "\r\n" . + "\r\n" . + "\r\n" . + "\r\n"; + + add_filter( 'wp_resource_hints', array( $this, '_add_prerender_urls' ), 10, 2 ); + + $actual = get_echo( 'wp_resource_hints' ); + + remove_filter( 'wp_resource_hints', array( $this, '_add_prerender_urls' ) ); + + $this->assertEquals( $expected, $actual ); + } + + function _add_prerender_urls( $hints, $method ) { + if ( 'prerender' === $method ) { + $hints[] = 'https://make.wordpress.org/great-again'; + $hints[] = 'http://jobs.wordpress.net'; + $hints[] = '//core.trac.wordpress.org'; + } + + return $hints; + } + + function test_parse_url_dns_prefetch() { + $expected = "\r\n" . + "\r\n"; + + add_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_long_urls' ), 10, 2 ); + + $actual = get_echo( 'wp_resource_hints' ); + + remove_filter( 'wp_resource_hints', array( $this, '_add_dns_prefetch_long_urls' ) ); + + $this->assertEquals( $expected, $actual ); + } + + function _add_dns_prefetch_long_urls( $hints, $method ) { + if ( 'dns-prefetch' === $method ) { + $hints[] = 'http://make.wordpress.org/wp-includes/css/editor.css'; + } + + return $hints; + } + + /** + * @group foo + */ + function test_dns_prefetch_styles() { + $expected = "\r\n" . + "\r\n"; + + $args = array( + 'family' => 'Open+Sans:400', + 'subset' => 'latin', + ); + + wp_enqueue_style( 'googlefonts', add_query_arg( $args, '//fonts.googleapis.com/css' ) ); + + $actual = get_echo( 'wp_resource_hints' ); + + wp_dequeue_style( 'googlefonts' ); + + $this->assertEquals( $expected, $actual ); + + } + + function test_dns_prefetch_scripts() { + $expected = "\r\n" . + "\r\n"; + + $args = array( + 'family' => 'Open+Sans:400', + 'subset' => 'latin', + ); + + wp_enqueue_script( 'googlefonts', add_query_arg( $args, '//fonts.googleapis.com/css' ) ); + + $actual = get_echo( 'wp_resource_hints' ); + + wp_dequeue_style( 'googlefonts' ); + + $this->assertEquals( $expected, $actual ); + } + +}