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/modules/Cloudflare/CloudflareModule.php +138 -27 1.1.41.3.2 View file →
@@ -36,22 +36,35 @@
36 36 private const HEALTH_OPTION = 'xspeed_cloudflare_health';
37 37
38 38 public function ui_metadata(): array {
39 39 return array(
40 - 'label' => 'Cloudflare',
40 + 'label' => __( 'Cloudflare', 'xspeed' ),
41 41 'icon' => 'Cloud',
42 - 'description' => 'Connect a Cloudflare zone for automatic edge purging when xSpeed clears its cache, plus a dev-mode toggle.',
42 + 'description' => __( 'Connect a Cloudflare zone for automatic edge purging when xSpeed clears its cache, plus a dev-mode toggle.', 'xspeed' ),
43 43 'custom_panel' => 'CloudflarePanel',
44 44 );
45 45 }
46 46
47 + /**
48 + * @inheritDoc
49 + *
50 + * Nothing exempt. It is inert without Cloudflare credentials, and where
51 + * credentials exist the user set them up for a CDN rather than for the page
52 + * cache we stood down from — but "inert today" is a weak reason to leave a
53 + * switch on that nobody asked for, and on a site where the host DID take
54 + * the page cache it is not inert at all.
55 + */
56 + public function conflict_safe_exempt(): array {
57 + return array();
58 + }
59 +
47 60 public function settings_schema(): array {
48 61 return array(
49 62 'enabled' => array(
50 63 'type' => 'bool',
51 64 'default' => false,
52 - 'label' => 'Enable Cloudflare integration',
53 - 'description' => 'Use the credentials below to verify your zone and run purges.',
65 + 'label' => __( 'Enable Cloudflare integration', 'xspeed' ),
66 + 'description' => __( 'Use the credentials below to verify your zone and run purges.', 'xspeed' ),
54 67 ),
55 68 'auth_method' => array(
56 69 'type' => 'enum',
57 70 'default' => 'token',
@@ -59,17 +72,17 @@
59 72 'option_labels' => array(
60 73 'token' => 'API Token',
61 74 'key' => 'Global API Key',
62 75 ),
63 - 'label' => 'Authentication',
64 - 'description' => 'API Tokens (scoped, recommended) or the legacy Global API Key with your account email.',
76 + 'label' => __( 'Authentication', 'xspeed' ),
77 + 'description' => __( 'API Tokens (scoped, recommended) or the legacy Global API Key with your account email.', 'xspeed' ),
65 78 'dependsOn' => array( 'field' => 'enabled' ),
66 79 ),
67 80 'api_token' => array(
68 81 'type' => 'secret',
69 82 'default' => '',
70 - 'label' => 'API Token',
71 - 'description' => 'Create a token at dash.cloudflare.com → My Profile → API Tokens. Needs "Zone → Cache Purge" + "Zone Settings" permissions.',
83 + 'label' => __( 'API Token', 'xspeed' ),
84 + 'description' => __( 'Create a token at dash.cloudflare.com → My Profile → API Tokens. Needs "Zone → Cache Purge" + "Zone Settings" permissions.', 'xspeed' ),
72 85 // Only the token auth branch (and only while CF is enabled, via
73 86 // the transitive gate on auth_method → enabled).
74 87 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'token' ),
75 88 ),
@@ -75,31 +88,31 @@
75 88 ),
76 89 'email' => array(
77 90 'type' => 'string',
78 91 'default' => '',
79 - 'label' => 'Account Email',
80 - 'description' => 'Only used when Authentication is set to Global API Key.',
92 + 'label' => __( 'Account Email', 'xspeed' ),
93 + 'description' => __( 'Only used when Authentication is set to Global API Key.', 'xspeed' ),
81 94 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'key' ),
82 95 ),
83 96 'api_key' => array(
84 97 'type' => 'secret',
85 98 'default' => '',
86 - 'label' => 'Global API Key',
87 - 'description' => 'Found at dash.cloudflare.com → My Profile → API Tokens → Global API Key.',
99 + 'label' => __( 'Global API Key', 'xspeed' ),
100 + 'description' => __( 'Found at dash.cloudflare.com → My Profile → API Tokens → Global API Key.', 'xspeed' ),
88 101 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'key' ),
89 102 ),
90 103 'zone_id' => array(
91 104 'type' => 'string',
92 105 'default' => '',
93 - 'label' => 'Zone ID',
94 - 'description' => 'The 32-character hex Zone ID from your domain overview page.',
106 + 'label' => __( 'Zone ID', 'xspeed' ),
107 + 'description' => __( 'The 32-character hex Zone ID from your domain overview page.', 'xspeed' ),
95 108 'dependsOn' => array( 'field' => 'enabled' ),
96 109 ),
97 110 'auto_purge_on_update' => array(
98 111 'type' => 'bool',
99 112 'default' => true,
100 - 'label' => 'Auto-purge Cloudflare on xSpeed purge',
101 - 'description' => 'When xSpeed clears its own cache (post save, settings change, manual purge), trigger a Cloudflare purge too.',
113 + 'label' => __( 'Auto-purge Cloudflare on xSpeed purge', 'xspeed' ),
114 + 'description' => __( 'When xSpeed clears its own cache (post save, settings change, manual purge), trigger a Cloudflare purge too.', 'xspeed' ),
102 115 'dependsOn' => array( 'field' => 'enabled' ),
103 116 ),
104 117 );
105 118 }
@@ -158,8 +171,27 @@
158 171 );
159 172 }
160 173
161 174 public function boot(): void {
175 + /*
176 + * Deferred to `init`. This module reads its own settings to decide
177 + * what to hook, and reading settings builds settings_schema(), whose
178 + * labels are declared through __(). boot() runs on `plugins_loaded`,
179 + * before `after_setup_theme` — the point WordPress 6.7+ treats as the
180 + * earliest safe moment to translate — so doing that here fires
181 + * _load_textdomain_just_in_time on every request AND resolves the
182 + * labels against a domain that is not loaded yet.
183 + *
184 + * Everything below hooks actions that fire after `init`, so running
185 + * one hook later is equivalent.
186 + */
187 + add_action( 'init', array( $this, 'boot_on_init' ) );
188 + }
189 +
190 + /**
191 + * The real boot body — see boot() for why it runs on `init`.
192 + */
193 + public function boot_on_init(): void {
162 194 $opts = $this->get_settings();
163 195 if ( empty( $opts['enabled'] ) ) {
164 196 return;
165 197 }
@@ -171,14 +203,64 @@
171 203 }
172 204 }
173 205
174 206 public function on_xspeed_purge(): void {
207 + /*
208 + * `wp xspeed purge` purges the edge itself, as its own reported line
209 + * item, and the page step it runs first fires this action. Without
210 + * this guard the zone is purged twice per command, and the SECOND
211 + * call's outcome — the one nobody reported — is what lands in the
212 + * health record the panel reads.
213 + *
214 + * Gated on covers(), not merely is_running(): on `--type=page` the
215 + * action still fires but no edge target runs, so standing down there
216 + * would leave the zone stale with nothing in the report to say so.
217 + * That run is exactly the one this listener exists for.
218 + */
219 + if ( class_exists( '\\XSpeed\\Purge_Runner' ) && \XSpeed\Purge_Runner::covers( 'cloudflare' ) ) {
220 + return;
221 + }
222 + if ( true !== $this->can_purge_edge() ) {
223 + return;
224 + }
225 + $this->purge_edge( 'auto-purge' );
226 + }
227 +
228 + /**
229 + * Whether this site can purge its Cloudflare zone right now.
230 + *
231 + * @return true|string True, or the reason it cannot — for the skip line
232 + * in `wp xspeed purge`, which has to explain itself
233 + * rather than silently do nothing.
234 + */
235 + public function can_purge_edge() {
175 236 $opts = $this->get_settings();
176 - if ( empty( $opts['enabled'] ) || empty( $opts['zone_id'] ) ) {
177 - return;
237 + if ( empty( $opts['enabled'] ) ) {
238 + return __( 'the Cloudflare integration is switched off', 'xspeed' );
178 239 }
179 - $result = Cloudflare::purge_all( $opts );
240 + if ( ! $this->has_credentials( $opts ) ) {
241 + return __( 'no zone ID or API credentials are configured', 'xspeed' );
242 + }
243 +
244 + return true;
245 + }
246 +
247 + /**
248 + * Purge the whole zone and record the outcome.
249 + *
250 + * The one edge-purge path: the auto-purge listener, `wp xspeed cf purge`
251 + * and `wp xspeed purge` all land here, so the health record and the
252 + * activity log say the same thing whichever one ran.
253 + *
254 + * @param string $cause Who asked.
255 + * @return array{ok:bool,reason:string,status:int,body:mixed} The engine
256 + * result plus a normalised `reason`, so the `cf` command can
257 + * still print the raw body it always has.
258 + */
259 + public function purge_edge( string $cause = 'manual' ): array {
260 + $result = Cloudflare::purge_all( $this->get_settings() );
180 261 $ok = ! empty( $result['ok'] );
262 + $reason = $ok ? '' : $this->message_of( $result );
181 263
182 264 // A GET /zones verify can pass with a token that still lacks the
183 265 // "Zone → Cache Purge" permission, so the real purge is the only
184 266 // authoritative signal for purge capability. Record it either way so
@@ -183,17 +265,41 @@
183 265 // "Zone → Cache Purge" permission, so the real purge is the only
184 266 // authoritative signal for purge capability. Record it either way so
185 267 // a silent auth failure becomes a visible, unresolved warning on the
186 268 // module rather than an entry buried in the activity log. (#119)
187 - $this->record_health( $ok, 'purge', $ok ? '' : $this->message_of( $result ) );
269 + $this->record_health( $ok, 'purge', $reason );
188 270
189 - if ( ! $ok && class_exists( '\\XSpeed\\Activity_Log' ) ) {
190 - \XSpeed\Activity_Log::record(
191 - 'cloudflare_purge_failed',
192 - 'Cloudflare auto-purge failed: ' . ( $result['body']['message'] ?? 'unknown error' ),
193 - \XSpeed\Activity_Log::WARN
194 - );
271 + if ( class_exists( '\\XSpeed\\Activity_Log' ) ) {
272 + if ( $ok ) {
273 + \XSpeed\Activity_Log::record(
274 + 'cache_purged',
275 + sprintf(
276 + /* translators: %s: what asked for the purge. */
277 + __( 'Purged the Cloudflare edge cache (%s)', 'xspeed' ),
278 + $cause
279 + ),
280 + \XSpeed\Activity_Log::INFO
281 + );
282 + } else {
283 + \XSpeed\Activity_Log::record(
284 + 'cloudflare_purge_failed',
285 + sprintf(
286 + /* translators: 1: what asked for the purge, 2: failure reason. */
287 + __( 'Cloudflare purge failed (%1$s): %2$s', 'xspeed' ),
288 + $cause,
289 + $reason ? $reason : __( 'unknown error', 'xspeed' )
290 + ),
291 + \XSpeed\Activity_Log::WARN
292 + );
293 + }
195 294 }
295 +
296 + return array(
297 + 'ok' => $ok,
298 + 'reason' => $reason,
299 + 'status' => (int) ( $result['status'] ?? 0 ),
300 + 'body' => $result['body'] ?? array(),
301 + );
196 302 }
197 303
198 304 /**
199 305 * Persist any settings sent with the save, then verify the credentials
@@ -347,8 +453,9 @@
347 453 array(
348 454 'name' => 'xspeed cf',
349 455 'callback' => array( $this, 'cli_handler' ),
350 456 'shortdesc' => 'Cloudflare verify / purge / dev-mode helpers.',
457 + 'ai_hint' => 'Cloudflare operations: verify the API credentials work, purge the edge cache, or toggle development mode. Use when a change is live on the origin but visitors still see the old version — that is usually the edge, not the local cache.',
351 458 'synopsis' => array(
352 459 array(
353 460 'type' => 'positional',
354 461 'name' => 'action',
@@ -367,9 +474,13 @@
367 474 case 'verify':
368 475 $res = Cloudflare::verify( $opts );
369 476 break;
370 477 case 'purge':
371 - $res = Cloudflare::purge_all( $opts );
478 + // Through purge_edge() so a CLI purge records the same health
479 + // and activity-log entries as an auto-purge or `wp xspeed
480 + // purge`. Calling the engine directly left the panel's health
481 + // record showing whatever the last NON-CLI call found.
482 + $res = $this->purge_edge( 'CLI' );
372 483 break;
373 484 case 'dev-on':
374 485 $res = Cloudflare::set_dev_mode( $opts, true );
375 486 break;