Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@
add_action( 'login_head', 'wp_resource_hints', 8 );
add_action( 'login_head', 'wp_print_head_scripts', 9 );
add_action( 'login_head', 'print_admin_styles', 9 );
add_action( 'login_head', 'wp_prefetch_admin_assets', 10 );
add_action( 'login_head', 'wp_site_icon', 99 );
add_action( 'login_footer', 'wp_print_footer_scripts', 20 );
add_action( 'login_init', 'send_frame_options_header', 10, 0 );
Expand Down
300 changes: 300 additions & 0 deletions src/wp-includes/script-loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2497,6 +2497,306 @@ function script_concat_settings() {
}
}

/**
* Resolves a registered script or style handle to the URL it would be loaded from.
*
* Mirrors how {@see WP_Scripts::do_item()} and {@see WP_Styles::do_item()} build the
* URL they print, including the version query argument and the {@see 'script_loader_src'}
* and {@see 'style_loader_src'} filters, without printing anything or disturbing the queue.
*
* @since 7.2.0
* @access private
*
* @param WP_Scripts|WP_Styles $dependencies Registry to look the handle up in.
* @param string $handle Handle to resolve.
* @return string[] URLs the handle resolves to. Empty when the handle is not registered,
* aliases other handles without a source of its own, or is filtered away.
*
* @phpstan-return list<non-empty-string>
*/
function _wp_resolve_dependency_urls( $dependencies, string $handle ): array {
if ( ! isset( $dependencies->registered[ $handle ] ) ) {
return array();
}

$obj = $dependencies->registered[ $handle ];

// A handle may alias a set of other handles by having dependencies but no source.
if ( empty( $obj->src ) || ! is_string( $obj->src ) ) {
return array();
}

if ( $dependencies instanceof WP_Styles ) {
$href = $dependencies->_css_href( $obj->src, $obj->ver, $handle );

if ( ! is_string( $href ) || '' === $href ) {
return array();
}

$urls = array( $href );

/*
* On RTL locales a handle may be served by a separate stylesheet, either replacing
* the LTR one or loading alongside it. Follow the same rules WP_Styles::do_item() uses.
*/
if ( 'rtl' === $dependencies->text_direction && ! empty( $obj->extra['rtl'] ) ) {
if ( null === $obj->ver ) {
$ver = '';
} else {
$ver = $obj->ver ? $obj->ver : $dependencies->default_version;
}

if ( isset( $dependencies->args[ $handle ] ) ) {
$ver = $ver ? $ver . '&amp;' . $dependencies->args[ $handle ] : $dependencies->args[ $handle ];
}

if ( is_bool( $obj->extra['rtl'] ) || 'replace' === $obj->extra['rtl'] ) {
$suffix = isset( $obj->extra['suffix'] ) && is_string( $obj->extra['suffix'] ) ? $obj->extra['suffix'] : '';
$rtl_href = str_replace( "{$suffix}.css", "-rtl{$suffix}.css", $dependencies->_css_href( $obj->src, $ver, "$handle-rtl" ) );
} elseif ( is_string( $obj->extra['rtl'] ) ) {
$rtl_href = $dependencies->_css_href( $obj->extra['rtl'], $ver, "$handle-rtl" );
} else {
$rtl_href = '';
}

if ( is_string( $rtl_href ) && '' !== $rtl_href ) {
if ( 'replace' === $obj->extra['rtl'] ) {
$urls = array( $rtl_href );
} else {
$urls[] = $rtl_href;
}
}
}

return $urls;
}

$src = $obj->src;

if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $dependencies->content_url && str_starts_with( $src, $dependencies->content_url ) ) ) {
$src = $dependencies->base_url . $src;
}

$ver_to_add = '';
if ( empty( $obj->ver ) && null !== $obj->ver && is_string( $dependencies->default_version ) ) {
$ver_to_add = $dependencies->default_version;
} elseif ( is_scalar( $obj->ver ) ) {
$ver_to_add = (string) $obj->ver;
}

if ( '' !== $ver_to_add ) {
$src .= ( str_contains( $src, '?' ) ? '&' : '?' ) . 'ver=' . rawurlencode( $ver_to_add );
}

/** This filter is documented in wp-includes/class-wp-scripts.php */
$src = esc_url_raw( apply_filters( 'script_loader_src', $src, $handle ) );

if ( ! is_string( $src ) || '' === $src ) {
return array();
}

return array( $src );
}

/**
* Prints prefetch links on the login screen for the admin assets that concatenation would bundle.
*
* With concatenation disabled the first admin screen after logging in downloads each of these
* files separately, which is what makes an uncached admin load slower than a concatenated one.
* Requesting them while the login form is on screen puts them in the HTTP cache during the time
* the user spends typing credentials, so the redirect that follows finds them already there.
*
* These are resources for the *next* navigation rather than for the login screen itself, which is
* what `rel="prefetch"` describes. `rel="preload"` would fetch them at the current document's
* priority and make cross-navigation reuse depend entirely on the static files' HTTP cache headers,
* which core does not control; browsers also warn about preloaded resources the document never uses.
* A prefetch is already dispatched at the browser's lowest priority, so it stays out of the way of
* the login screen's own render-blocking assets without needing `fetchpriority`.
*
* The `as` attribute is still worth setting: it gives the request the same destination the admin
* screen will later ask for, which is what lets the prefetched response be reused.
*
* Every handle covered here loads on all admin screens rather than only on the Dashboard, so the
* list does not vary with where the login lands. Nothing is printed at all when the login is not
* going to lead to an admin screen: on the password reset, registration and logout flows, on an
* interim login, or when `redirect_to` points outside the admin.
*
* Nothing is printed when concatenation is enabled, since `load-scripts.php` and
* `load-styles.php` already collapse these handles into a handful of requests.
*
* @since 7.2.0
*
* @see wp_preload_resources()
*/
function wp_prefetch_admin_assets(): void {
/*
* Deliberately not the $concatenate_scripts global: script_concat_settings() often runs on a
* login request before 'login_init' fires — anything registering a script on 'init' is enough
* to trigger it — and at that point it evaluates is_admin() as false and settles the global on
* false whatever the constant says. What matters here is what the admin screen this login leads
* to will do, which is the constant together with the SCRIPT_DEBUG override.
*/
$admin_will_concatenate = ( defined( 'CONCATENATE_SCRIPTS' ) ? CONCATENATE_SCRIPTS : true )
&& ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG );

if ( $admin_will_concatenate ) {
return;
}

/*
* Only the login form is followed by an admin screen. The password reset, registration,
* logout confirmation and check-your-email flows all render through 'login_head' too, and
* none of them leads anywhere these assets are wanted. An interim login re-authenticates
* inside a modal on a page that has already loaded them, so it does not need them either.
*/
$login_action = isset( $_REQUEST['action'] ) && is_string( $_REQUEST['action'] )
? sanitize_key( wp_unslash( $_REQUEST['action'] ) )
: 'login';

if ( 'login' !== $login_action || isset( $_REQUEST['interim-login'] ) ) {
return;
}

/*
* Resolve where the login is going to land, the same way wp-login.php will: `redirect_to`
* when one was given, and the admin otherwise. wp_validate_redirect() mirrors what
* wp_safe_redirect() does with a value pointing off-host, which is to fall back to the admin.
*/
$redirect_to = admin_url();

if ( isset( $_REQUEST['redirect_to'] ) && is_string( $_REQUEST['redirect_to'] ) ) {
$redirect_to = wp_validate_redirect( esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ), admin_url() );
}

/*
* When the login lands somewhere other than the admin, such as the front end or a plugin's
* own screen, none of these assets are wanted.
*/
$admin_path = (string) wp_parse_url( admin_url(), PHP_URL_PATH );

if ( '' === $admin_path || ! str_starts_with( (string) wp_parse_url( $redirect_to, PHP_URL_PATH ), $admin_path ) ) {
return;
}

/*
* The handles that load-scripts.php and load-styles.php concatenate on an admin screen.
* Every handle listed here loads on all admin screens, not just the one the login happens
* to land on, so the list does not depend on the destination. Screen-specific handles are
* deliberately left out: `site-health` is concatenated on the Dashboard but nowhere else.
* Handles registered without a source of their own, or not registered at all, are skipped.
*/
$script_handles = array(
'jquery-core',
'jquery-migrate',
'utils',
'hoverIntent',
'wp-dom-ready',
'wp-hooks',
);

$style_handles = array(
'dashicons',
'admin-bar',
'common',
'forms',
'admin-menu',
'dashboard',
'list-tables',
'edit',
'revisions',
'media',
'themes',
'about',
'nav-menus',
'wp-pointer',
'widgets',
'site-icon',
'l10n',
'wp-base-styles',
'wp-tooltip',
'buttons',
'wp-auth-check',
'wp-theme',
'wp-components',
'wp-commands',
);

$resources = array();

foreach ( array(
'script' => $script_handles,
'style' => $style_handles,
) as $as => $handles ) {
$dependencies = 'script' === $as ? wp_scripts() : wp_styles();

foreach ( $handles as $handle ) {
/*
* The login screen shares a handful of these handles and has already printed them by
* the time this runs, so the browser is fetching them anyway. Prefetching them again
* would only add markup.
*/
if ( in_array( $handle, $dependencies->done, true ) ) {
continue;
}

foreach ( _wp_resolve_dependency_urls( $dependencies, $handle ) as $url ) {
$resources[ $url ] = array(
'href' => $url,
'as' => $as,
);
}
}
}

/**
* Filters the admin assets prefetched on the login screen.
*
* Only the `href` and `as` attributes below are printed; any other key is ignored.
* Returning an empty array turns the prefetching off.
*
* @since 7.2.0
*
* @param array $resources {
* Resources to prefetch, keyed by URL.
*
* @type array ...$0 {
* @type string $href URL to prefetch.
* @type string $as How the browser should treat the resource.
* }
* }
* @param string $redirect_to URL the login will redirect to, already run through
* wp_validate_redirect() with the admin as the fallback. Always
* points into the admin, since nothing is prefetched otherwise,
* but may be relative: this is the value as wp_safe_redirect()
* will receive it, so a request-supplied path is passed through
* unchanged and only the fallback is a full URL.
*/
$resources = apply_filters( 'login_prefetch_admin_assets', $resources, $redirect_to );

if ( ! is_array( $resources ) ) {
return;
}

foreach ( $resources as $resource ) {
if ( ! is_array( $resource ) ) {
continue;
}

$href = $resource['href'] ?? '';
$as = $resource['as'] ?? '';

if ( ! is_string( $href ) || '' === $href || ! is_string( $as ) || '' === $as ) {
continue;
}

printf(
"<link rel='prefetch' href='%s' as='%s' />\n",
esc_url( $href ),
esc_attr( $as )
);
}
}

/**
* Handles the enqueueing of block scripts and styles that are common to both
* the editor and the front-end.
Expand Down
Loading