| @@ -24,26 +24,47 @@ | ||
| 24 | 24 | final class CloudflareModule extends Module { |
| 25 | 25 | |
| 26 | 26 | public const SLUG = 'cloudflare'; |
| 27 | 27 | public const TIER = self::TIER_FREE; |
| 28 | - public const VERSION = '1.0.0'; | |
| 28 | + public const VERSION = '1.1.0'; | |
| 29 | 29 | |
| 30 | + /** | |
| 31 | + * Where the last connection-health result is cached: the outcome of the | |
| 32 | + * most recent verify (token + zone reachable) or purge (Cache-Purge | |
| 33 | + * permission actually works). Read by ui_notices() to show a persistent | |
| 34 | + * warning when Cloudflare is silently failing. (#119) | |
| 35 | + */ | |
| 36 | + private const HEALTH_OPTION = 'xspeed_cloudflare_health'; | |
| 37 | + | |
| 30 | 38 | public function ui_metadata(): array { |
| 31 | 39 | return array( |
| 32 | - 'label' => 'Cloudflare', | |
| 40 | + 'label' => __( 'Cloudflare', 'xspeed' ), | |
| 33 | 41 | 'icon' => 'Cloud', |
| 34 | - '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' ), | |
| 35 | 43 | 'custom_panel' => 'CloudflarePanel', |
| 36 | 44 | ); |
| 37 | 45 | } |
| 38 | 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 | + | |
| 39 | 60 | public function settings_schema(): array { |
| 40 | 61 | return array( |
| 41 | 62 | 'enabled' => array( |
| 42 | 63 | 'type' => 'bool', |
| 43 | 64 | 'default' => false, |
| 44 | - 'label' => 'Enable Cloudflare integration', | |
| 45 | - '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' ), | |
| 46 | 67 | ), |
| 47 | 68 | 'auth_method' => array( |
| 48 | 69 | 'type' => 'enum', |
| 49 | 70 | 'default' => 'token', |
| @@ -51,17 +72,17 @@ | ||
| 51 | 72 | 'option_labels' => array( |
| 52 | 73 | 'token' => 'API Token', |
| 53 | 74 | 'key' => 'Global API Key', |
| 54 | 75 | ), |
| 55 | - 'label' => 'Authentication', | |
| 56 | - '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' ), | |
| 57 | 78 | 'dependsOn' => array( 'field' => 'enabled' ), |
| 58 | 79 | ), |
| 59 | 80 | 'api_token' => array( |
| 60 | - 'type' => 'string', | |
| 81 | + 'type' => 'secret', | |
| 61 | 82 | 'default' => '', |
| 62 | - 'label' => 'API Token', | |
| 63 | - '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' ), | |
| 64 | 85 | // Only the token auth branch (and only while CF is enabled, via |
| 65 | 86 | // the transitive gate on auth_method → enabled). |
| 66 | 87 | 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'token' ), |
| 67 | 88 | ), |
| @@ -67,36 +88,55 @@ | ||
| 67 | 88 | ), |
| 68 | 89 | 'email' => array( |
| 69 | 90 | 'type' => 'string', |
| 70 | 91 | 'default' => '', |
| 71 | - 'label' => 'Account Email', | |
| 72 | - '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' ), | |
| 73 | 94 | 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'key' ), |
| 74 | 95 | ), |
| 75 | 96 | 'api_key' => array( |
| 76 | - 'type' => 'string', | |
| 97 | + 'type' => 'secret', | |
| 77 | 98 | 'default' => '', |
| 78 | - 'label' => 'Global API Key', | |
| 79 | - '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' ), | |
| 80 | 101 | 'dependsOn' => array( 'field' => 'auth_method', 'value' => 'key' ), |
| 81 | 102 | ), |
| 82 | 103 | 'zone_id' => array( |
| 83 | 104 | 'type' => 'string', |
| 84 | 105 | 'default' => '', |
| 85 | - 'label' => 'Zone ID', | |
| 86 | - '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' ), | |
| 87 | 108 | 'dependsOn' => array( 'field' => 'enabled' ), |
| 88 | 109 | ), |
| 89 | 110 | 'auto_purge_on_update' => array( |
| 90 | 111 | 'type' => 'bool', |
| 91 | 112 | 'default' => true, |
| 92 | - 'label' => 'Auto-purge Cloudflare on xSpeed purge', | |
| 93 | - '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' ), | |
| 94 | 115 | 'dependsOn' => array( 'field' => 'enabled' ), |
| 95 | 116 | ), |
| 96 | 117 | ); |
| 97 | 118 | } |
| 98 | 119 | |
| 120 | + /** | |
| 121 | + * Encrypt the pre-1.1.0 plaintext credentials on upgrade. api_token / | |
| 122 | + * api_key became `secret`-typed fields (encrypted at rest); this converts | |
| 123 | + * any already-stored plaintext in one pass. Idempotent — encrypt_for_storage | |
| 124 | + * skips a value that already carries the cipher marker. (#115) | |
| 125 | + */ | |
| 126 | + public function migrations(): array { | |
| 127 | + return array( | |
| 128 | + '1.1.0' => static function ( array $opts ): array { | |
| 129 | + foreach ( array( 'api_token', 'api_key' ) as $key ) { | |
| 130 | + if ( isset( $opts[ $key ] ) && is_string( $opts[ $key ] ) && '' !== $opts[ $key ] ) { | |
| 131 | + $opts[ $key ] = \XSpeed\Settings_Manager::encrypt_for_storage( $opts[ $key ] ); | |
| 132 | + } | |
| 133 | + } | |
| 134 | + return $opts; | |
| 135 | + }, | |
| 136 | + ); | |
| 137 | + } | |
| 138 | + | |
| 99 | 139 | public function rest_routes(): array { |
| 100 | 140 | $default = parent::rest_routes(); |
| 101 | 141 | return array_merge( |
| 102 | 142 | $default, |
| @@ -131,8 +171,27 @@ | ||
| 131 | 171 | ); |
| 132 | 172 | } |
| 133 | 173 | |
| 134 | 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 { | |
| 135 | 194 | $opts = $this->get_settings(); |
| 136 | 195 | if ( empty( $opts['enabled'] ) ) { |
| 137 | 196 | return; |
| 138 | 197 | } |
| @@ -144,24 +203,125 @@ | ||
| 144 | 203 | } |
| 145 | 204 | } |
| 146 | 205 | |
| 147 | 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() { | |
| 148 | 236 | $opts = $this->get_settings(); |
| 149 | - if ( empty( $opts['enabled'] ) || empty( $opts['zone_id'] ) ) { | |
| 150 | - return; | |
| 237 | + if ( empty( $opts['enabled'] ) ) { | |
| 238 | + return __( 'the Cloudflare integration is switched off', 'xspeed' ); | |
| 151 | 239 | } |
| 152 | - $result = Cloudflare::purge_all( $opts ); | |
| 153 | - if ( ! $result['ok'] && class_exists( '\\XSpeed\\Activity_Log' ) ) { | |
| 154 | - \XSpeed\Activity_Log::record( | |
| 155 | - 'cloudflare_purge_failed', | |
| 156 | - 'Cloudflare auto-purge failed: ' . ( $result['body']['message'] ?? 'unknown error' ), | |
| 157 | - \XSpeed\Activity_Log::WARN | |
| 158 | - ); | |
| 240 | + if ( ! $this->has_credentials( $opts ) ) { | |
| 241 | + return __( 'no zone ID or API credentials are configured', 'xspeed' ); | |
| 159 | 242 | } |
| 243 | + | |
| 244 | + return true; | |
| 160 | 245 | } |
| 161 | 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() ); | |
| 261 | + $ok = ! empty( $result['ok'] ); | |
| 262 | + $reason = $ok ? '' : $this->message_of( $result ); | |
| 263 | + | |
| 264 | + // A GET /zones verify can pass with a token that still lacks the | |
| 265 | + // "Zone → Cache Purge" permission, so the real purge is the only | |
| 266 | + // authoritative signal for purge capability. Record it either way so | |
| 267 | + // a silent auth failure becomes a visible, unresolved warning on the | |
| 268 | + // module rather than an entry buried in the activity log. (#119) | |
| 269 | + $this->record_health( $ok, 'purge', $reason ); | |
| 270 | + | |
| 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 | + } | |
| 294 | + } | |
| 295 | + | |
| 296 | + return array( | |
| 297 | + 'ok' => $ok, | |
| 298 | + 'reason' => $reason, | |
| 299 | + 'status' => (int) ( $result['status'] ?? 0 ), | |
| 300 | + 'body' => $result['body'] ?? array(), | |
| 301 | + ); | |
| 302 | + } | |
| 303 | + | |
| 304 | + /** | |
| 305 | + * Persist any settings sent with the save, then verify the credentials | |
| 306 | + * immediately so an invalid or newly-changed token surfaces on the panel | |
| 307 | + * instead of failing silently the next time xSpeed purges. Response shape | |
| 308 | + * is unchanged (flat settings) so the autosave client is unaffected. (#119) | |
| 309 | + */ | |
| 310 | + public function rest_update_settings( \WP_REST_Request $request ) { | |
| 311 | + $params = $request->get_json_params(); | |
| 312 | + if ( ! is_array( $params ) ) { | |
| 313 | + $params = $request->get_params(); | |
| 314 | + } | |
| 315 | + $settings = $this->update_settings( is_array( $params ) ? $params : array() ); | |
| 316 | + $this->verify_and_record(); | |
| 317 | + return rest_ensure_response( $settings ); | |
| 318 | + } | |
| 319 | + | |
| 162 | 320 | public function rest_verify( \WP_REST_Request $request ) { |
| 163 | - return rest_ensure_response( Cloudflare::verify( $this->get_settings() ) ); | |
| 321 | + $res = Cloudflare::verify( $this->get_settings() ); | |
| 322 | + $this->record_health( ! empty( $res['ok'] ), 'verify', $this->message_of( $res ) ); | |
| 323 | + return rest_ensure_response( $res ); | |
| 164 | 324 | } |
| 165 | 325 | |
| 166 | 326 | public function rest_purge( \WP_REST_Request $request ) { |
| 167 | 327 | $params = $request->get_json_params(); |
| @@ -180,8 +340,115 @@ | ||
| 180 | 340 | $on = ! empty( $params['on'] ); |
| 181 | 341 | return rest_ensure_response( Cloudflare::set_dev_mode( $this->get_settings(), $on ) ); |
| 182 | 342 | } |
| 183 | 343 | |
| 344 | + /** | |
| 345 | + * Persistent callouts on the Cloudflare panel: a hard warning when the | |
| 346 | + * connection is enabled but silently failing (bad token, or a purge that | |
| 347 | + * was rejected for lack of the Cache-Purge permission), and a soft warning | |
| 348 | + * when it's enabled but not fully configured yet. (#119) | |
| 349 | + */ | |
| 350 | + public function ui_notices(): array { | |
| 351 | + $opts = $this->get_settings(); | |
| 352 | + if ( empty( $opts['enabled'] ) ) { | |
| 353 | + return array(); | |
| 354 | + } | |
| 355 | + if ( ! $this->has_credentials( $opts ) ) { | |
| 356 | + return array( | |
| 357 | + array( | |
| 358 | + 'tone' => 'warn', | |
| 359 | + 'title' => __( 'Cloudflare is not fully configured.', 'xspeed' ), | |
| 360 | + 'body' => __( 'Add your API token (or Global API Key + account email) and the Zone ID, then press Verify. Until then auto-purge does nothing.', 'xspeed' ), | |
| 361 | + ), | |
| 362 | + ); | |
| 363 | + } | |
| 364 | + $health = get_option( self::HEALTH_OPTION, null ); | |
| 365 | + if ( is_array( $health ) && array_key_exists( 'ok', $health ) && false === $health['ok'] ) { | |
| 366 | + $context = isset( $health['context'] ) ? (string) $health['context'] : 'verify'; | |
| 367 | + $message = isset( $health['message'] ) ? (string) $health['message'] : ''; | |
| 368 | + $suffix = '' !== $message ? ': ' . $message : ''; | |
| 369 | + if ( 'purge' === $context ) { | |
| 370 | + return array( | |
| 371 | + array( | |
| 372 | + 'tone' => 'danger', | |
| 373 | + 'title' => __( 'Cloudflare purge is failing.', 'xspeed' ), | |
| 374 | + 'body' => sprintf( | |
| 375 | + /* translators: %s: the Cloudflare API error message, or empty. */ | |
| 376 | + __( 'The last edge purge was rejected by Cloudflare%s. Confirm the API token includes the "Zone → Cache Purge" permission for this zone — a token that can read the zone can still lack purge rights.', 'xspeed' ), | |
| 377 | + $suffix | |
| 378 | + ), | |
| 379 | + ), | |
| 380 | + ); | |
| 381 | + } | |
| 382 | + return array( | |
| 383 | + array( | |
| 384 | + 'tone' => 'danger', | |
| 385 | + 'title' => __( 'Cloudflare credentials were rejected.', 'xspeed' ), | |
| 386 | + 'body' => sprintf( | |
| 387 | + /* translators: %s: the Cloudflare API error message, or empty. */ | |
| 388 | + __( 'The saved credentials could not verify this zone%s. Auto-purge will not work until this is fixed.', 'xspeed' ), | |
| 389 | + $suffix | |
| 390 | + ), | |
| 391 | + ), | |
| 392 | + ); | |
| 393 | + } | |
| 394 | + return array(); | |
| 395 | + } | |
| 396 | + | |
| 397 | + /** Verify the current credentials and cache the outcome (save-time hook). */ | |
| 398 | + private function verify_and_record(): void { | |
| 399 | + $opts = $this->get_settings(); | |
| 400 | + if ( empty( $opts['enabled'] ) || ! $this->has_credentials( $opts ) ) { | |
| 401 | + // Nothing to verify — drop any stale health so an old failure notice | |
| 402 | + // doesn't linger after the user disables or clears the integration. | |
| 403 | + delete_option( self::HEALTH_OPTION ); | |
| 404 | + return; | |
| 405 | + } | |
| 406 | + $res = Cloudflare::verify( $opts ); | |
| 407 | + $this->record_health( ! empty( $res['ok'] ), 'verify', $this->message_of( $res ) ); | |
| 408 | + } | |
| 409 | + | |
| 410 | + /** Cache the last verify/purge outcome for ui_notices(). */ | |
| 411 | + private function record_health( bool $ok, string $context, string $message ): void { | |
| 412 | + update_option( | |
| 413 | + self::HEALTH_OPTION, | |
| 414 | + array( | |
| 415 | + 'ok' => $ok, | |
| 416 | + 'context' => $context, | |
| 417 | + 'message' => $message, | |
| 418 | + 'checked_at' => time(), | |
| 419 | + ), | |
| 420 | + false | |
| 421 | + ); | |
| 422 | + } | |
| 423 | + | |
| 424 | + /** Whether the current auth branch has all the fields it needs. */ | |
| 425 | + private function has_credentials( array $opts ): bool { | |
| 426 | + if ( empty( $opts['zone_id'] ) ) { | |
| 427 | + return false; | |
| 428 | + } | |
| 429 | + $method = isset( $opts['auth_method'] ) ? (string) $opts['auth_method'] : 'token'; | |
| 430 | + if ( 'key' === $method ) { | |
| 431 | + return ! empty( $opts['api_key'] ) && ! empty( $opts['email'] ); | |
| 432 | + } | |
| 433 | + return ! empty( $opts['api_token'] ); | |
| 434 | + } | |
| 435 | + | |
| 436 | + /** Human-readable failure reason from a Cloudflare engine result. */ | |
| 437 | + private function message_of( array $res ): string { | |
| 438 | + if ( ! empty( $res['ok'] ) ) { | |
| 439 | + return ''; | |
| 440 | + } | |
| 441 | + $body = isset( $res['body'] ) && is_array( $res['body'] ) ? $res['body'] : array(); | |
| 442 | + if ( ! empty( $body['message'] ) ) { | |
| 443 | + return (string) $body['message']; | |
| 444 | + } | |
| 445 | + if ( ! empty( $body['errors'][0]['message'] ) ) { | |
| 446 | + return (string) $body['errors'][0]['message']; | |
| 447 | + } | |
| 448 | + return 'HTTP ' . ( isset( $res['status'] ) ? (string) $res['status'] : '0' ); | |
| 449 | + } | |
| 450 | + | |
| 184 | 451 | public function cli_commands(): array { |
| 185 | 452 | return array( |
| 186 | 453 | array( |
| 187 | 454 | 'name' => 'xspeed cf', |
| @@ -186,8 +453,9 @@ | ||
| 186 | 453 | array( |
| 187 | 454 | 'name' => 'xspeed cf', |
| 188 | 455 | 'callback' => array( $this, 'cli_handler' ), |
| 189 | 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.', | |
| 190 | 458 | 'synopsis' => array( |
| 191 | 459 | array( |
| 192 | 460 | 'type' => 'positional', |
| 193 | 461 | 'name' => 'action', |
| @@ -206,9 +474,13 @@ | ||
| 206 | 474 | case 'verify': |
| 207 | 475 | $res = Cloudflare::verify( $opts ); |
| 208 | 476 | break; |
| 209 | 477 | case 'purge': |
| 210 | - $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' ); | |
| 211 | 483 | break; |
| 212 | 484 | case 'dev-on': |
| 213 | 485 | $res = Cloudflare::set_dev_mode( $opts, true ); |
| 214 | 486 | break; |