.js path long before
* `script_loader_tag` (priority 20/30) runs, so the delay + exclusion
* checks only ever see the hashed URL. A user targeting a script by
* URL substring — the obvious thing to do, and what the UI invites —
* would silently stop matching the moment minification was enabled.
* Minifier::rewrite_script() records the original here so those
* checks can test both. (FBS field report against 1.1.2)
*
* @var array
*/
private static $original_src = array();
/**
* Record a script's URL as it was BEFORE minification rewrote it.
* Called from Minifier::rewrite_script().
*
* @param string $handle Script handle.
* @param string $src Original (pre-minify) URL.
*/
public static function remember_original_src( string $handle, string $src ): void {
if ( '' !== $handle && '' !== $src ) {
self::$original_src[ $handle ] = $src;
}
}
/**
* The pre-minify URL for a handle, or '' when we never rewrote it
* (external script, minification off, or a handle we didn't touch).
*
* @param string $handle Script handle.
*/
public static function original_src( string $handle ): string {
return isset( self::$original_src[ $handle ] ) ? self::$original_src[ $handle ] : '';
}
/**
* Reset the remembered URLs. Test-only seam.
*/
public static function reset_original_src(): void {
self::$original_src = array();
}
/**
* Does a user-supplied target match this script?
*
* A target is either a script handle (exact) or a URL substring. The
* URL is checked against BOTH the current src and the pre-minify src,
* so a target written against the real asset path keeps working once
* minification starts rewriting URLs to hashed cache paths.
*
* @param string $needle Target from the user's list.
* @param string $handle Script handle.
* @param string $src Current (possibly rewritten) src.
*/
private static function target_matches( string $needle, string $handle, string $src ): bool {
if ( '' === $needle ) {
return false;
}
if ( $handle === $needle ) {
return true;
}
if ( '' !== $src && false !== stripos( $src, $needle ) ) {
return true;
}
$original = self::original_src( $handle );
return '' !== $original && false !== stripos( $original, $needle );
}
/**
* Filter: `script_loader_tag` — add defer="defer" to non-excluded
* scripts. WordPress passes the full
fallback so
* users with JS disabled still get styles applied (via media="all").
*
* @param string $tag
* @param string $handle
*/
public static function async_style_tag( $tag, $handle ): string {
if ( ! is_string( $tag ) || '' === $tag ) {
return (string) $tag;
}
if ( self::skip_in_non_frontend_context() ) {
return $tag;
}
// Only operate on with a media attribute
// we can swap. Skip anything custom (preload, etc.) — we don't
// want to fight with explicit author intent.
if ( false === stripos( $tag, 'rel=\'stylesheet\'' ) && false === stripos( $tag, 'rel="stylesheet"' ) ) {
return $tag;
}
// Avoid double-wrapping.
if ( false !== stripos( $tag, 'data-xs-async' ) ) {
return $tag;
}
$async = (string) preg_replace_callback(
'#\bmedia\s*=\s*(["\'])([^"\']*)\1#i',
static function ( $m ) {
$orig = $m[2];
return 'media="print" onload="this.media=\'' . esc_attr( $orig ) . '\'" data-xs-async="' . esc_attr( $orig ) . '"';
},
$tag,
1
);
// If no media= was present (rare), inject one.
if ( $async === $tag ) {
$async = (string) preg_replace(
'#.
return $async . '';
}
/**
* Filter: `style_loader_src` + `script_loader_src` — strip the
* ?ver=X.Y query string that WP appends for cache busting. Some
* CDNs / reverse proxies cache better when the URL has no query.
*
* Skip URLs whose query carries non-ver params — those might be
* intentional (e.g. a CDN providing per-image transforms).
*
* @param string $src
*/
public static function strip_version_query( $src ): string {
if ( ! is_string( $src ) || '' === $src ) {
return (string) $src;
}
if ( self::skip_in_non_frontend_context() ) {
return $src;
}
$parts = wp_parse_url( $src );
if ( ! is_array( $parts ) || empty( $parts['query'] ) ) {
return $src;
}
parse_str( $parts['query'], $query );
if ( ! is_array( $query ) ) {
return $src;
}
// Only strip 'ver' — keep anything else the asset URL needs.
unset( $query['ver'] );
$new_query = http_build_query( $query );
$new_url = ( $parts['scheme'] ?? 'http' ) . '://' . ( $parts['host'] ?? '' );
if ( isset( $parts['port'] ) ) {
$new_url .= ':' . $parts['port'];
}
$new_url .= $parts['path'] ?? '';
if ( '' !== $new_query ) {
$new_url .= '?' . $new_query;
}
if ( ! empty( $parts['fragment'] ) ) {
$new_url .= '#' . $parts['fragment'];
}
return $new_url;
}
/**
* Defensive context guard for filter callbacks. Mirrors the registration-
* time bail in Minifier::__construct() so a late context flip (admin page
* render kicked off mid-request, REST_REQUEST set after plugins_loaded,
* etc.) doesn't let frontend tag rewrites leak into wp-admin / AJAX /
* REST / cron responses.
*
* Specifically prevents the React admin bundle's