PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.2
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.2
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 1.1.3 1.1.4 1.1.5 1.1.6 All 31 releases
← All changes | includes/class-preloader.php +6 -125 1.3.51.3.2 View file →
@@ -35,24 +35,9 @@
35 35
36 36 public const STATE_KEY = 'xspeed_preloader_state';
37 37 public const STATE_TTL = 86400; // 24h — long enough for slow crawls.
38 38 public const CRON_HOOK = 'xspeed_preloader_tick';
39 - /**
40 - * User-agent for every request the preloader makes.
41 - *
42 - * Deliberately contains no substring from the 7G/8G bad-bot lists. The
43 - * previous value, "xSpeed-Preloader/1.0", matched the `loader` token in
44 - * the alphabetical slice `(linkscan|linkwalker|loader|lwp-download|...)`
45 - * — a match on "Pre*loader*" — so nginx ports of 8G answered every warm
46 - * with 403 and newly published posts were never warmed. Upstream 8G
47 - * v1.5 has since dropped `loader`, but forks and vendored copies (xCloud
48 - * among them) still ship the older slice, so the name has to stay clear
49 - * of it. "Warmer" matches nothing in either list. (#481)
50 - *
51 - * Read through user_agent() rather than using this constant directly, so
52 - * the `xspeed_preloader_user_agent` filter applies.
53 - */
54 - public const USER_AGENT = 'xSpeed-Warmer/1.0 (+cache warmer; admin-initiated)';
39 + public const USER_AGENT = 'xSpeed-Preloader/1.0 (+cache warmer; admin-initiated)';
55 40 public const REQUEST_TIMEOUT = 8;
56 41
57 42 /**
58 43 * Default cap on NEW remote images resolved per warmed page.
@@ -72,102 +57,8 @@
72 57 */
73 58 private const REMOTE_DIMENSION_LIMIT = 20;
74 59
75 60 /**
76 - * The user-agent every preloader request sends.
77 - *
78 - * Filterable because the blocking rule lives on the server, not here: a
79 - * host with its own bad-bot list can clear a warm without patching the
80 - * plugin or waiting for a release. An empty filter return is ignored —
81 - * sending no UA gets a request blocked at least as often. (#481)
82 - */
83 - public static function user_agent(): string {
84 - /**
85 - * Filter the preloader's user-agent string.
86 - *
87 - * @param string $user_agent Default self::USER_AGENT.
88 - */
89 - $ua = apply_filters( 'xspeed_preloader_user_agent', self::USER_AGENT );
90 -
91 - return ( is_string( $ua ) && '' !== trim( $ua ) ) ? trim( $ua ) : self::USER_AGENT;
92 - }
93 -
94 - /**
95 - * Is this status code the signature of a firewall refusing our warmer?
96 - *
97 - * 403 and 406 are what bad-bot rules (7G/8G, mod_security, Wordfence)
98 - * answer with. We only ever warm our OWN origin, and a page a visitor can
99 - * load must be loadable by us too — so these codes mean the request was
100 - * judged by its user-agent, not that the page is missing or broken. (#481)
101 - */
102 - private static function is_firewall_block( int $code ): bool {
103 - return in_array( $code, array( 403, 406 ), true );
104 - }
105 -
106 - /**
107 - * Explain a warm failure in terms the admin can act on.
108 - *
109 - * A bare "HTTP 403" sent people hunting a broken page; the page is fine,
110 - * and the fix is a server rule, so the message has to name the cause and
111 - * the exact UA to allow. (#481)
112 - */
113 - private static function failure_detail( int $code ): string {
114 - if ( ! self::is_firewall_block( $code ) ) {
115 - return sprintf( 'HTTP %d', $code );
116 - }
117 -
118 - return sprintf(
119 - 'HTTP %d — your server\'s firewall is blocking the xSpeed cache warmer by user-agent, so this page was not warmed. Allow the user-agent "%s" (on xCloud this is the 8G firewall\'s bad-bot rule), or change it with the xspeed_preloader_user_agent filter.',
120 - $code,
121 - self::user_agent()
122 - );
123 - }
124 -
125 - /** Option holding the last firewall-shaped warm refusal. */
126 - public const FIREWALL_BLOCK_OPTION = 'xspeed_preloader_firewall_block';
127 -
128 - /**
129 - * Record that the origin refused a warm by user-agent, for ui_notices().
130 - *
131 - * An option rather than a transient: the condition is a server rule that
132 - * persists until someone changes it, and a notice that expired on its own
133 - * would let a site go back to never warming, silently. Cleared by
134 - * clear_firewall_block() on the first warm that succeeds. (#481)
135 - */
136 - private static function remember_firewall_block( string $url, int $code ): void {
137 - if ( ! function_exists( 'update_option' ) ) {
138 - return;
139 - }
140 - update_option(
141 - self::FIREWALL_BLOCK_OPTION,
142 - array(
143 - 'url' => $url,
144 - 'code' => $code,
145 - 'user_agent' => self::user_agent(),
146 - 'ts' => time(),
147 - ),
148 - false
149 - );
150 - }
151 -
152 - /** Forget the firewall block once a warm gets through. */
153 - public static function clear_firewall_block(): void {
154 - if ( function_exists( 'delete_option' ) && self::firewall_block() ) {
155 - delete_option( self::FIREWALL_BLOCK_OPTION );
156 - }
157 - }
158 -
159 - /** The last firewall-shaped refusal, or null when there isn't one. */
160 - public static function firewall_block(): ?array {
161 - if ( ! function_exists( 'get_option' ) ) {
162 - return null;
163 - }
164 - $block = get_option( self::FIREWALL_BLOCK_OPTION, null );
165 -
166 - return ( is_array( $block ) && ! empty( $block['code'] ) ) ? $block : null;
167 - }
168 -
169 - /**
170 61 * How many new remote images one warmed page may resolve.
171 62 */
172 63 private static function remote_dimension_limit(): int {
173 64 /**
@@ -389,9 +280,9 @@
389 280 $url,
390 281 array(
391 282 'timeout' => self::REQUEST_TIMEOUT,
392 283 'sslverify' => false,
393 - 'user-agent' => self::user_agent(),
284 + 'user-agent' => self::USER_AGENT,
394 285 'blocking' => true,
395 286 )
396 287 );
397 288 if ( is_wp_error( $response ) ) {
@@ -405,19 +296,13 @@
405 296 $code = (int) wp_remote_retrieve_response_code( $response );
406 297 if ( $code >= 400 ) {
407 298 Activity_Log::record(
408 299 'preloader_warm_failed',
409 - sprintf( 'Warm %s failed (%s): %s', $cause, $url, self::failure_detail( $code ) ),
300 + sprintf( 'Warm %s failed (%s): HTTP %d', $cause, $url, $code ),
410 301 Activity_Log::WARN
411 302 );
412 - if ( self::is_firewall_block( $code ) ) {
413 - self::remember_firewall_block( $url, $code );
414 - }
415 303 return false;
416 304 }
417 - // A warm that got through proves the firewall is no longer refusing us,
418 - // so the notice must go — otherwise it outlives the problem. (#481)
419 - self::clear_firewall_block();
420 305 Activity_Log::record(
421 306 'preloader_warmed_one',
422 307 sprintf( 'Warmed %s (%s)', $url, $cause ),
423 308 Activity_Log::INFO
@@ -430,9 +315,9 @@
430 315 $url,
431 316 array(
432 317 'timeout' => self::REQUEST_TIMEOUT,
433 318 'sslverify' => false,
434 - 'user-agent' => self::user_agent(),
319 + 'user-agent' => self::USER_AGENT,
435 320 'headers' => array(
436 321 'Accept' => 'text/html,application/xhtml+xml',
437 322 ),
438 323 'blocking' => true,
@@ -452,19 +337,15 @@
452 337 $code = (int) wp_remote_retrieve_response_code( $response );
453 338 if ( $code >= 400 ) {
454 339 $state['errors'][] = array(
455 340 'url' => $url,
456 - 'error' => self::failure_detail( $code ),
341 + 'error' => sprintf( 'HTTP %d', $code ),
457 342 'ts' => time(),
458 343 );
459 344 $state['errors'] = array_slice( $state['errors'], -20 );
460 - if ( self::is_firewall_block( $code ) ) {
461 - self::remember_firewall_block( $url, $code );
462 - }
463 345 return;
464 346 }
465 347
466 - self::clear_firewall_block();
467 348 self::warm_remote_dimensions( (string) wp_remote_retrieve_body( $response ) );
468 349 }
469 350
470 351 /**
@@ -725,9 +606,9 @@
725 606 $sitemap_url,
726 607 array(
727 608 'timeout' => self::REQUEST_TIMEOUT,
728 609 'sslverify' => false,
729 - 'user-agent' => self::user_agent(),
610 + 'user-agent' => self::USER_AGENT,
730 611 )
731 612 );
732 613 if ( is_wp_error( $res ) ) {
733 614 // Record WHY, don't just vanish. "Unreachable" and "valid but