| @@ -36,11 +36,11 @@ | ||
| 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 | |
| @@ -61,10 +61,10 @@ | ||
| 61 | 61 | return array( |
| 62 | 62 | 'enabled' => array( |
| 63 | 63 | 'type' => 'bool', |
| 64 | 64 | 'default' => false, |
| 65 | - 'label' => 'Enable Cloudflare integration', | |
| 66 | - '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' ), | |
| 67 | 67 | ), |
| 68 | 68 | 'auth_method' => array( |
| 69 | 69 | 'type' => 'enum', |
| 70 | 70 | 'default' => 'token', |
| @@ -72,17 +72,17 @@ | ||
| 72 | 72 | 'option_labels' => array( |
| 73 | 73 | 'token' => 'API Token', |
| 74 | 74 | 'key' => 'Global API Key', |
| 75 | 75 | ), |
| 76 | - 'label' => 'Authentication', | |
| 77 | - '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' ), | |
| 78 | 78 | 'dependsOn' => array( 'field' => 'enabled' ), |
| 79 | 79 | ), |
| 80 | 80 | 'api_token' => array( |
| 81 | 81 | 'type' => 'secret', |
| 82 | 82 | 'default' => '', |
| 83 | - 'label' => 'API Token', | |
| 84 | - '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' ), | |
| 85 | 85 | // Only the token auth branch (and only while CF is enabled, via |
| 86 | 86 | // the transitive gate on auth_method → enabled). |
| 87 | 87 | 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'token' ), |
| 88 | 88 | ), |
| @@ -88,31 +88,31 @@ | ||
| 88 | 88 | ), |
| 89 | 89 | 'email' => array( |
| 90 | 90 | 'type' => 'string', |
| 91 | 91 | 'default' => '', |
| 92 | - 'label' => 'Account Email', | |
| 93 | - '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' ), | |
| 94 | 94 | 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'key' ), |
| 95 | 95 | ), |
| 96 | 96 | 'api_key' => array( |
| 97 | 97 | 'type' => 'secret', |
| 98 | 98 | 'default' => '', |
| 99 | - 'label' => 'Global API Key', | |
| 100 | - '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' ), | |
| 101 | 101 | 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'key' ), |
| 102 | 102 | ), |
| 103 | 103 | 'zone_id' => array( |
| 104 | 104 | 'type' => 'string', |
| 105 | 105 | 'default' => '', |
| 106 | - 'label' => 'Zone ID', | |
| 107 | - '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' ), | |
| 108 | 108 | 'dependsOn' => array( 'field' => 'enabled' ), |
| 109 | 109 | ), |
| 110 | 110 | 'auto_purge_on_update' => array( |
| 111 | 111 | 'type' => 'bool', |
| 112 | 112 | 'default' => true, |
| 113 | - 'label' => 'Auto-purge Cloudflare on xSpeed purge', | |
| 114 | - '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' ), | |
| 115 | 115 | 'dependsOn' => array( 'field' => 'enabled' ), |
| 116 | 116 | ), |
| 117 | 117 | ); |
| 118 | 118 | } |
| @@ -171,8 +171,27 @@ | ||
| 171 | 171 | ); |
| 172 | 172 | } |
| 173 | 173 | |
| 174 | 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 { | |
| 175 | 194 | $opts = $this->get_settings(); |
| 176 | 195 | if ( empty( $opts['enabled'] ) ) { |
| 177 | 196 | return; |
| 178 | 197 | } |
| @@ -184,14 +203,64 @@ | ||
| 184 | 203 | } |
| 185 | 204 | } |
| 186 | 205 | |
| 187 | 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() { | |
| 188 | 236 | $opts = $this->get_settings(); |
| 189 | - if ( empty( $opts['enabled'] ) || empty( $opts['zone_id'] ) ) { | |
| 190 | - return; | |
| 237 | + if ( empty( $opts['enabled'] ) ) { | |
| 238 | + return __( 'the Cloudflare integration is switched off', 'xspeed' ); | |
| 191 | 239 | } |
| 192 | - $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() ); | |
| 193 | 261 | $ok = ! empty( $result['ok'] ); |
| 262 | + $reason = $ok ? '' : $this->message_of( $result ); | |
| 194 | 263 | |
| 195 | 264 | // A GET /zones verify can pass with a token that still lacks the |
| 196 | 265 | // "Zone → Cache Purge" permission, so the real purge is the only |
| 197 | 266 | // authoritative signal for purge capability. Record it either way so |
| @@ -196,17 +265,41 @@ | ||
| 196 | 265 | // "Zone → Cache Purge" permission, so the real purge is the only |
| 197 | 266 | // authoritative signal for purge capability. Record it either way so |
| 198 | 267 | // a silent auth failure becomes a visible, unresolved warning on the |
| 199 | 268 | // module rather than an entry buried in the activity log. (#119) |
| 200 | - $this->record_health( $ok, 'purge', $ok ? '' : $this->message_of( $result ) ); | |
| 269 | + $this->record_health( $ok, 'purge', $reason ); | |
| 201 | 270 | |
| 202 | - if ( ! $ok && class_exists( '\\XSpeed\\Activity_Log' ) ) { | |
| 203 | - \XSpeed\Activity_Log::record( | |
| 204 | - 'cloudflare_purge_failed', | |
| 205 | - 'Cloudflare auto-purge failed: ' . ( $result['body']['message'] ?? 'unknown error' ), | |
| 206 | - \XSpeed\Activity_Log::WARN | |
| 207 | - ); | |
| 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 | + } | |
| 208 | 294 | } |
| 295 | + | |
| 296 | + return array( | |
| 297 | + 'ok' => $ok, | |
| 298 | + 'reason' => $reason, | |
| 299 | + 'status' => (int) ( $result['status'] ?? 0 ), | |
| 300 | + 'body' => $result['body'] ?? array(), | |
| 301 | + ); | |
| 209 | 302 | } |
| 210 | 303 | |
| 211 | 304 | /** |
| 212 | 305 | * Persist any settings sent with the save, then verify the credentials |
| @@ -381,9 +474,13 @@ | ||
| 381 | 474 | case 'verify': |
| 382 | 475 | $res = Cloudflare::verify( $opts ); |
| 383 | 476 | break; |
| 384 | 477 | case 'purge': |
| 385 | - $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' ); | |
| 386 | 483 | break; |
| 387 | 484 | case 'dev-on': |
| 388 | 485 | $res = Cloudflare::set_dev_mode( $opts, true ); |
| 389 | 486 | break; |