PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.4.1
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.4.1
1.4.1 1.4.0 1.3.7 1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 All 35 releases
← All changes | includes/class-minify-filters.php +130 -2 1.3.7 → 1.4.1 View file →
@@ -622,9 +622,17 @@
622 622 // A non-executable type means this tag is data, or is being held by
623 623 // somebody else on purpose. The buffer pass has always checked this;
624 624 // the enqueue path did not, so a consent-blocked or JSON-carrying
625 625 // handle could still be rewritten here. (#274)
626 - if ( in_array( self::extract_type( $tag ), self::NON_EXECUTABLE_TYPES, true ) ) {
626 + //
627 + // Read the type from the tag that carries the src. $tag is before +
628 + // external + after, and Smart Delay parks the before/after snippets
629 + // as text/xspeed-delayed before this filter runs. Reading the first
630 + // `type=` in the whole string found the parked snippet, so every
631 + // handle with its own snippets kept a live src behind parked
632 + // snippets.
633 + $type_open = '' !== (string) $src ? self::open_tag_offsets( $tag ) : null;
634 + if ( in_array( self::extract_type( null !== $type_open ? $type_open['attrs'] : $tag ), self::NON_EXECUTABLE_TYPES, true ) ) {
627 635 return $tag;
628 636 }
629 637 // src= variant: swap src → data-xs-src and add data-xs-delay marker.
630 638 if ( '' !== (string) $src ) {
@@ -2128,9 +2136,9 @@
2128 2136 // has a stored list that knows nothing about consent managers, and
2129 2137 // one that cleared the textarea has no list at all. Neither may
2130 2138 // hide the banner. (#275)
2131 2139 foreach ( self::exclusion_floor() as $needle ) {
2132 - if ( self::target_matches( $needle, $handle, $src ) ) {
2140 + if ( self::floor_matches( $needle, $handle, $src ) ) {
2133 2141 // `continue`, not `break`: a second floor token matching the
2134 2142 // same tag has to be named too, or a filter-added token
2135 2143 // would be lifted by an entry that names only the first.
2136 2144 if ( $named_lifts_floor && self::user_named_consent_manager( $needle, $handle, $src ) ) {
@@ -2155,8 +2163,56 @@
2155 2163 return false;
2156 2164 }
2157 2165
2158 2166 /**
2167 + * target_matches() for a floor token, with the site's own host removed
2168 + * from the URL first.
2169 + *
2170 + * Floor tokens are plugin names, and a plugin's own website is often
2171 + * named after the plugin. On notificationx.com the `notificationx` token
2172 + * matched every same-origin script URL, so Defer JS and Delay JS skipped
2173 + * every script on the site. The path still matches, so a script under
2174 + * /plugins/notificationx/ keeps its protection, and a third-party host
2175 + * such as consent.cookiebot.com still matches in full.
2176 + *
2177 + * @param string $needle Floor token.
2178 + * @param string $handle Script handle.
2179 + * @param string $src Script URL.
2180 + */
2181 + private static function floor_matches( string $needle, string $handle, string $src ): bool {
2182 + if ( '' === $needle ) {
2183 + return false;
2184 + }
2185 + if ( $handle === $needle ) {
2186 + return true;
2187 + }
2188 + foreach ( array( $src, self::original_src( $handle ) ) as $url ) {
2189 + $url = self::without_own_host( $url );
2190 + if ( '' !== $url && false !== stripos( $url, $needle ) ) {
2191 + return true;
2192 + }
2193 + }
2194 + return false;
2195 + }
2196 +
2197 + /**
2198 + * The URL without its scheme and host when the host is the site's own.
2199 + * Any other URL comes back unchanged.
2200 + *
2201 + * @param string $url Script URL.
2202 + */
2203 + private static function without_own_host( string $url ): string {
2204 + if ( '' === $url || ! function_exists( 'home_url' ) ) {
2205 + return $url;
2206 + }
2207 + $host = (string) wp_parse_url( home_url(), PHP_URL_HOST );
2208 + if ( '' === $host ) {
2209 + return $url;
2210 + }
2211 + return (string) preg_replace( '#^(?:https?:)?//' . preg_quote( $host, '#' ) . '(?::\d+)?(?=[/?\#]|$)#i', '', $url );
2212 + }
2213 +
2214 + /**
2159 2215 * Include-list targeting for delay (issue #36): when delay_js_targets
2160 2216 * is non-empty, ONLY matching scripts are delayed — a heavy
2161 2217 * third-party embed can be postponed without delaying the whole
2162 2218 * page's JS. Empty targets = historical behavior (delay everything
@@ -2277,13 +2333,85 @@
2277 2333 private static function smart_delays_handle( string $handle ): bool {
2278 2334 if ( '' === $handle ) {
2279 2335 return false;
2280 2336 }
2337 + // Check the URL delay_script_tag() checks. original_src() is set
2338 + // only when Minify JS rewrote the URL, so with Minify JS off it was
2339 + // '' here. A URL exclusion then passed here and failed there, and
2340 + // the snippets were parked while the script stayed live.
2281 2341 $src = self::original_src( $handle );
2342 + if ( '' === $src ) {
2343 + $src = self::registered_src( $handle );
2344 + }
2282 2345 if ( self::is_excluded_script( $handle, $src, true ) ) {
2283 2346 return false;
2284 2347 }
2285 2348 return self::is_delay_target( $handle, $src );
2349 + }
2350 +
2351 + /**
2352 + * A handle's registered URL, made absolute the way WP_Scripts prints it,
2353 + * without the version query. '' when the handle has no file.
2354 + *
2355 + * @param string $handle Script handle.
2356 + */
2357 + private static function registered_src( string $handle ): string {
2358 + if ( ! function_exists( 'wp_scripts' ) ) {
2359 + return '';
2360 + }
2361 + $scripts = wp_scripts();
2362 + if ( ! $scripts instanceof \WP_Scripts ) {
2363 + return '';
2364 + }
2365 + $reg = $scripts->registered[ $handle ] ?? null;
2366 + $src = ( is_object( $reg ) && is_string( $reg->src ) ) ? $reg->src : '';
2367 + if ( '' !== $src && ! preg_match( '#^(?:https?:)?//#i', $src ) ) {
2368 + $src = (string) ( $scripts->base_url ?? '' ) . $src;
2369 + }
2370 + return $src;
2371 + }
2372 +
2373 + /**
2374 + * Filter: `script_loader_tag`, after every other xSpeed pass. Un-park a
2375 + * handle's before/after snippets when its external tag was not delayed.
2376 + *
2377 + * park_smart_inline() decides before the tag exists, so a later rule
2378 + * that keeps the tag live (an opt-out attribute, a non-executable type,
2379 + * the late opt-out revert, another plugin's filter) left the snippets
2380 + * parked and the script live. The script then ran without the config
2381 + * its `before` snippet sets, which is how Elementor's frontend lost
2382 + * elementorFrontendConfig. This filter makes that state impossible.
2383 + *
2384 + * @param string $tag
2385 + * @param string $handle
2386 + * @param string $src
2387 + */
2388 + public static function unpark_orphaned_smart_inline( $tag, $handle, $src ): string {
2389 + if ( ! is_string( $tag ) || '' === $tag || '' === (string) $handle || false === stripos( $tag, 'text/xspeed-delayed' ) ) {
2390 + return (string) $tag;
2391 + }
2392 + // Only the external tag in this string can carry data-xs-src. Its
2393 + // `src` is gone once delayed, so open_tag_offsets() cannot find it.
2394 + if ( preg_match( '#<script\b[^>]*(?<![-\w])data-xs-src\s*=#i', $tag ) ) {
2395 + return $tag;
2396 + }
2397 + return (string) preg_replace_callback(
2398 + '#<script\b([^>]*\sid\s*=\s*(["\'])' . preg_quote( (string) $handle, '#' ) . '-js-(?:before|after)\2[^>]*)>#i',
2399 + static function ( array $m ): string {
2400 + $attrs = $m[1];
2401 + if ( 'text/xspeed-delayed' !== self::extract_type( $attrs ) ) {
2402 + return $m[0];
2403 + }
2404 + $orig = preg_match( '#\sdata-xs-type\s*=\s*(["\'])([^"\']*)\1#i', $attrs, $t ) ? $t[2] : '';
2405 + $attrs = (string) preg_replace( self::TYPE_ATTR_RE, '', $attrs );
2406 + $attrs = (string) preg_replace( '#\sdata-xs-(?:delay|type)\s*=\s*(["\'])[^"\']*\1#i', '', $attrs );
2407 + if ( '' !== $orig ) {
2408 + $attrs .= ' type="' . esc_attr( $orig ) . '"';
2409 + }
2410 + return '<script' . $attrs . '>';
2411 + },
2412 + $tag
2413 + );
2286 2414 }
2287 2415
2288 2416 /**
2289 2417 * Park a delayed handle's own before/after snippet, in Smart Delay mode.