| @@ -77,10 +77,42 @@ | ||
| 77 | 77 | 'permission_callback' => array( $this, 'permissions' ), |
| 78 | 78 | ) |
| 79 | 79 | ); |
| 80 | 80 | |
| 81 | + // Force a fresh static-rewrite probe. The result is otherwise cached | |
| 82 | + // for five minutes with nothing to invalidate it, so a user who just | |
| 83 | + // fixed their nginx config had no way to confirm it. (FBS-84012) | |
| 81 | 84 | register_rest_route( |
| 82 | 85 | self::NAMESPACE_V1, |
| 86 | + '/cache/recheck-rewrite', | |
| 87 | + array( | |
| 88 | + 'methods' => 'POST', | |
| 89 | + 'callback' => array( $this, 'recheck_rewrite' ), | |
| 90 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 91 | + ) | |
| 92 | + ); | |
| 93 | + | |
| 94 | + // The server-side mirror of "I pasted the block". Only meaningful on a | |
| 95 | + // host where the probe cannot read the rules back — see | |
| 96 | + // nginx_copied_hash(). | |
| 97 | + register_rest_route( | |
| 98 | + self::NAMESPACE_V1, | |
| 99 | + '/cache/nginx-copied-hash', | |
| 100 | + array( | |
| 101 | + 'methods' => 'POST', | |
| 102 | + 'callback' => array( $this, 'nginx_copied_hash' ), | |
| 103 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 104 | + 'args' => array( | |
| 105 | + 'hash' => array( | |
| 106 | + 'type' => 'string', | |
| 107 | + 'required' => true, | |
| 108 | + ), | |
| 109 | + ), | |
| 110 | + ) | |
| 111 | + ); | |
| 112 | + | |
| 113 | + register_rest_route( | |
| 114 | + self::NAMESPACE_V1, | |
| 83 | 115 | '/cache/benchmark', |
| 84 | 116 | array( |
| 85 | 117 | 'methods' => 'GET', |
| 86 | 118 | 'callback' => array( $this, 'benchmark' ), |
| @@ -89,8 +121,110 @@ | ||
| 89 | 121 | ); |
| 90 | 122 | |
| 91 | 123 | register_rest_route( |
| 92 | 124 | self::NAMESPACE_V1, |
| 125 | + '/cache/benchmark/history', | |
| 126 | + array( | |
| 127 | + 'methods' => 'GET', | |
| 128 | + 'callback' => array( $this, 'benchmark_history' ), | |
| 129 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 130 | + ) | |
| 131 | + ); | |
| 132 | + | |
| 133 | + // Drill-downs behind the four dashboard stat cards. Each is a plain | |
| 134 | + // GET so the same data reaches the CLI and MCP through | |
| 135 | + // `wp xspeed cache inventory|size|purge-log`. | |
| 136 | + register_rest_route( | |
| 137 | + self::NAMESPACE_V1, | |
| 138 | + '/cache/inventory', | |
| 139 | + array( | |
| 140 | + 'methods' => 'GET', | |
| 141 | + 'callback' => array( $this, 'cache_inventory' ), | |
| 142 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 143 | + 'args' => array( | |
| 144 | + 'limit' => array( | |
| 145 | + 'type' => 'integer', | |
| 146 | + 'default' => 50, | |
| 147 | + ), | |
| 148 | + 'offset' => array( | |
| 149 | + 'type' => 'integer', | |
| 150 | + 'default' => 0, | |
| 151 | + ), | |
| 152 | + 'fresh' => array( | |
| 153 | + 'type' => 'boolean', | |
| 154 | + 'default' => false, | |
| 155 | + ), | |
| 156 | + ), | |
| 157 | + ) | |
| 158 | + ); | |
| 159 | + | |
| 160 | + register_rest_route( | |
| 161 | + self::NAMESPACE_V1, | |
| 162 | + '/cache/size', | |
| 163 | + array( | |
| 164 | + 'methods' => 'GET', | |
| 165 | + 'callback' => array( $this, 'cache_size' ), | |
| 166 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 167 | + ) | |
| 168 | + ); | |
| 169 | + | |
| 170 | + register_rest_route( | |
| 171 | + self::NAMESPACE_V1, | |
| 172 | + '/cache/purge-log', | |
| 173 | + array( | |
| 174 | + 'methods' => 'GET', | |
| 175 | + 'callback' => array( $this, 'cache_purge_log' ), | |
| 176 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 177 | + 'args' => array( | |
| 178 | + 'limit' => array( | |
| 179 | + 'type' => 'integer', | |
| 180 | + 'default' => 25, | |
| 181 | + ), | |
| 182 | + ), | |
| 183 | + ) | |
| 184 | + ); | |
| 185 | + | |
| 186 | + register_rest_route( | |
| 187 | + self::NAMESPACE_V1, | |
| 188 | + '/stats/hit-daily', | |
| 189 | + array( | |
| 190 | + 'methods' => 'GET', | |
| 191 | + 'callback' => array( $this, 'hit_daily' ), | |
| 192 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 193 | + ) | |
| 194 | + ); | |
| 195 | + | |
| 196 | + register_rest_route( | |
| 197 | + self::NAMESPACE_V1, | |
| 198 | + '/recommendations', | |
| 199 | + array( | |
| 200 | + 'methods' => 'GET', | |
| 201 | + 'callback' => array( $this, 'recommendations' ), | |
| 202 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 203 | + 'args' => array( | |
| 204 | + // `contributed` adds the entries other plugins hand in | |
| 205 | + // through `xspeed_recommendations`. Only the Overview | |
| 206 | + // card asks for them; see all_with_contributed(). | |
| 207 | + 'include' => array( | |
| 208 | + 'type' => 'string', | |
| 209 | + 'enum' => array( '', 'contributed' ), | |
| 210 | + ), | |
| 211 | + ), | |
| 212 | + ) | |
| 213 | + ); | |
| 214 | + | |
| 215 | + register_rest_route( | |
| 216 | + self::NAMESPACE_V1, | |
| 217 | + '/recommendations/apply', | |
| 218 | + array( | |
| 219 | + 'methods' => 'POST', | |
| 220 | + 'callback' => array( $this, 'recommendations_apply' ), | |
| 221 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 222 | + ) | |
| 223 | + ); | |
| 224 | + | |
| 225 | + register_rest_route( | |
| 226 | + self::NAMESPACE_V1, | |
| 93 | 227 | '/audit/pro', |
| 94 | 228 | array( |
| 95 | 229 | 'methods' => 'GET', |
| 96 | 230 | 'callback' => array( $this, 'pro_audit' ), |
| @@ -99,8 +233,18 @@ | ||
| 99 | 233 | ); |
| 100 | 234 | |
| 101 | 235 | register_rest_route( |
| 102 | 236 | self::NAMESPACE_V1, |
| 237 | + '/activity', | |
| 238 | + array( | |
| 239 | + 'methods' => 'GET', | |
| 240 | + 'callback' => array( $this, 'activity' ), | |
| 241 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 242 | + ) | |
| 243 | + ); | |
| 244 | + | |
| 245 | + register_rest_route( | |
| 246 | + self::NAMESPACE_V1, | |
| 103 | 247 | '/modules', |
| 104 | 248 | array( |
| 105 | 249 | 'methods' => 'GET', |
| 106 | 250 | 'callback' => array( $this, 'get_modules' ), |
| @@ -106,11 +250,53 @@ | ||
| 106 | 250 | 'callback' => array( $this, 'get_modules' ), |
| 107 | 251 | 'permission_callback' => array( $this, 'permissions' ), |
| 108 | 252 | ) |
| 109 | 253 | ); |
| 254 | + | |
| 255 | + // On-demand desktop-vs-mobile HTML equality probe (FBS-83145). POST so | |
| 256 | + // it's never triggered by a prefetch/GET; runs only from the dashboard | |
| 257 | + // "Check now" button behind manage_options. | |
| 258 | + register_rest_route( | |
| 259 | + self::NAMESPACE_V1, | |
| 260 | + '/cache/mobile-probe', | |
| 261 | + array( | |
| 262 | + 'methods' => 'POST', | |
| 263 | + 'callback' => array( $this, 'mobile_probe' ), | |
| 264 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 265 | + ) | |
| 266 | + ); | |
| 267 | + | |
| 268 | + // Dismiss the Separate-Mobile-Cache review prompt (FBS-83145). | |
| 269 | + register_rest_route( | |
| 270 | + self::NAMESPACE_V1, | |
| 271 | + '/cache/mobile-review-dismiss', | |
| 272 | + array( | |
| 273 | + 'methods' => 'POST', | |
| 274 | + 'callback' => array( $this, 'mobile_review_dismiss' ), | |
| 275 | + 'permission_callback' => array( $this, 'permissions' ), | |
| 276 | + ) | |
| 277 | + ); | |
| 110 | 278 | } |
| 111 | 279 | |
| 112 | 280 | /** |
| 281 | + * Run the on-demand mobile-equality probe and return the fresh /status | |
| 282 | + * mobile_separate block so the dashboard can update the callout in place. | |
| 283 | + */ | |
| 284 | + public function mobile_probe( $request ) { | |
| 285 | + unset( $request ); | |
| 286 | + return rest_ensure_response( Cache::probe_mobile_equality() ); | |
| 287 | + } | |
| 288 | + | |
| 289 | + /** | |
| 290 | + * Clear the migration review flag so the callout stops nagging. | |
| 291 | + */ | |
| 292 | + public function mobile_review_dismiss( $request ) { | |
| 293 | + unset( $request ); | |
| 294 | + Cache::clear_mobile_separate_review(); | |
| 295 | + return rest_ensure_response( array( 'dismissed' => true ) ); | |
| 296 | + } | |
| 297 | + | |
| 298 | + /** | |
| 113 | 299 | * The registered-module descriptors — same payload baked into the |
| 114 | 300 | * admin bootstrap (Admin::modules_payload), re-evaluated live. The |
| 115 | 301 | * dashboard re-fetches this after a license activate/deactivate so a |
| 116 | 302 | * Pro module's custom_panel flips between its real surface and |
| @@ -131,8 +317,20 @@ | ||
| 131 | 317 | return rest_ensure_response( array( 'suggestions' => Pro_Audit::run() ) ); |
| 132 | 318 | } |
| 133 | 319 | |
| 134 | 320 | /** |
| 321 | + * Recent activity-log entries for the Overview activity strip — | |
| 322 | + * newest-first (settings changes, purges, cache toggles, …). | |
| 323 | + * | |
| 324 | + * @param \WP_REST_Request $request Unused. | |
| 325 | + * @return \WP_REST_Response | |
| 326 | + */ | |
| 327 | + public function activity( $request ) { | |
| 328 | + unset( $request ); | |
| 329 | + return rest_ensure_response( array( 'activity' => Activity_Log::entries() ) ); | |
| 330 | + } | |
| 331 | + | |
| 332 | + /** | |
| 135 | 333 | * Cache before/after benchmark — fetches home_url() twice (with + |
| 136 | 334 | * without the bypass header) and returns side-by-side timings for |
| 137 | 335 | * the dashboard widget. |
| 138 | 336 | */ |
| @@ -140,8 +338,72 @@ | ||
| 140 | 338 | unset( $request ); |
| 141 | 339 | return rest_ensure_response( Cache_Benchmark::run() ); |
| 142 | 340 | } |
| 143 | 341 | |
| 342 | + /** | |
| 343 | + * Stored benchmark runs (oldest→newest) + the settings-change events | |
| 344 | + * the trend chart overlays as annotations. | |
| 345 | + */ | |
| 346 | + public function benchmark_history( $request ) { | |
| 347 | + $limit = min( 100, max( 1, (int) ( $request['limit'] ?? 100 ) ) ); | |
| 348 | + return rest_ensure_response( | |
| 349 | + array( | |
| 350 | + 'runs' => Cache_Benchmark::history( $limit ), | |
| 351 | + 'changes' => self::settings_change_events(), | |
| 352 | + ) | |
| 353 | + ); | |
| 354 | + } | |
| 355 | + | |
| 356 | + /** | |
| 357 | + * Daily hit/miss aggregates for the 7/30-day trend, plus change events | |
| 358 | + * for annotation markers. | |
| 359 | + */ | |
| 360 | + public function hit_daily( $request ) { | |
| 361 | + $days = min( Hit_Counter::DAILY_MAX_DAYS, max( 1, (int) ( $request['days'] ?? 30 ) ) ); | |
| 362 | + return rest_ensure_response( | |
| 363 | + array( | |
| 364 | + 'days' => Hit_Counter::daily_series( $days ), | |
| 365 | + 'changes' => self::settings_change_events(), | |
| 366 | + ) | |
| 367 | + ); | |
| 368 | + } | |
| 369 | + | |
| 370 | + /** Ranked "next best action" recommendations (issue #48). */ | |
| 371 | + public function recommendations( $request ) { | |
| 372 | + $recs = 'contributed' === (string) $request->get_param( 'include' ) | |
| 373 | + ? Recommendations::all_with_contributed() | |
| 374 | + : Recommendations::all(); | |
| 375 | + return rest_ensure_response( array( 'recommendations' => $recs ) ); | |
| 376 | + } | |
| 377 | + | |
| 378 | + /** One-click apply of a recommendation's settings fix. */ | |
| 379 | + public function recommendations_apply( $request ) { | |
| 380 | + $id = sanitize_key( (string) ( $request['id'] ?? '' ) ); | |
| 381 | + if ( '' === $id ) { | |
| 382 | + return new \WP_Error( 'xspeed_rec_missing_id', __( 'The id argument is required.', 'xspeed' ), array( 'status' => 400 ) ); | |
| 383 | + } | |
| 384 | + $result = Recommendations::apply( $id ); | |
| 385 | + return is_wp_error( $result ) ? $result : rest_ensure_response( $result ); | |
| 386 | + } | |
| 387 | + | |
| 388 | + /** | |
| 389 | + * Recent settings_changed activity entries (the chart annotations). | |
| 390 | + * | |
| 391 | + * @return array<int,array{ts:int,message:string}> | |
| 392 | + */ | |
| 393 | + private static function settings_change_events(): array { | |
| 394 | + $out = array(); | |
| 395 | + foreach ( Activity_Log::entries() as $entry ) { | |
| 396 | + if ( 'settings_changed' === ( $entry['type'] ?? '' ) ) { | |
| 397 | + $out[] = array( | |
| 398 | + 'ts' => (int) $entry['ts'], | |
| 399 | + 'message' => (string) $entry['message'], | |
| 400 | + ); | |
| 401 | + } | |
| 402 | + } | |
| 403 | + return $out; | |
| 404 | + } | |
| 405 | + | |
| 144 | 406 | public function permissions() { |
| 145 | 407 | return current_user_can( 'manage_options' ); |
| 146 | 408 | } |
| 147 | 409 | |
| @@ -167,12 +429,25 @@ | ||
| 167 | 429 | if ( $opts['cache_enabled'] && $rewrite_capable ) { |
| 168 | 430 | $probe = Cache::probe_static_rewrite(); |
| 169 | 431 | $rewrite_probe = array( |
| 170 | 432 | 'active' => (bool) ( $probe['active'] ?? false ), |
| 433 | + // Both flags were previously dropped here, so the dashboard | |
| 434 | + // could not tell "proven inactive" from "no result yet" or | |
| 435 | + // "probe failed" — and rendered the configure-your-server | |
| 436 | + // banner for all three. (FBS-84012) | |
| 437 | + 'pending' => (bool) ( $probe['pending'] ?? false ), | |
| 438 | + 'inconclusive' => (bool) ( $probe['inconclusive'] ?? false ), | |
| 439 | + 'reason' => (string) ( $probe['reason'] ?? '' ), | |
| 171 | 440 | 'server_type' => $server_type, |
| 172 | 441 | 'snippet' => Cache::nginx_snippet(), |
| 173 | 442 | 'topology' => Server::rewrite_topology(), |
| 174 | 443 | 'behind_proxy' => Server::is_behind_proxy(), |
| 444 | + // Whether the rules the web server is running are the rules | |
| 445 | + // these settings generate — `current`, `stale`, `absent` or | |
| 446 | + // `unknown`, with the hashes both sides compared. Nothing | |
| 447 | + // else can answer that: the nginx block lives in a server | |
| 448 | + // config WordPress cannot read. See Cache::rules_state(). | |
| 449 | + 'rules' => Cache::rules_state( $probe ), | |
| 175 | 450 | ); |
| 176 | 451 | } |
| 177 | 452 | |
| 178 | 453 | return rest_ensure_response( |
| @@ -186,8 +461,31 @@ | ||
| 186 | 461 | 'nginx_snippet' => Gzip::nginx_snippet(), |
| 187 | 462 | ), |
| 188 | 463 | 'rewrite_probe' => $rewrite_probe, |
| 189 | 464 | 'nginx_server_block' => Cache::full_nginx_server_block(), |
| 465 | + // What enabling the page cache would do to | |
| 466 | + // wp-content/advanced-cache.php. The dashboard discloses the | |
| 467 | + // replacement BEFORE the write when a leftover drop-in is | |
| 468 | + // already there; see Page_Cache_Detector::dropin_disclosure(). | |
| 469 | + 'dropin' => Page_Cache_Detector::dropin_disclosure(), | |
| 470 | + // Separate Mobile Cache visibility (FBS-83145). `blocking` is | |
| 471 | + // true when mobile_separate is what's keeping the device-blind | |
| 472 | + // static fast path from installing on a rewrite-capable server; | |
| 473 | + // `needs_review` is true when a migration turned it on for us and | |
| 474 | + // the user hasn't confirmed they actually need it. The dashboard | |
| 475 | + // renders a callout (+ "Check now" equality probe) from these. | |
| 476 | + 'mobile_separate' => array( | |
| 477 | + 'enabled' => ! empty( Settings::get()['cache_enabled'] ) ? (bool) ( Settings_Manager::get( 'cache' )['mobile_separate'] ?? false ) : false, | |
| 478 | + // Gated to servers that HAVE a static fast path — see the | |
| 479 | + // matching comment in Admin::bootstrap_payload(): on IIS / | |
| 480 | + // unknown, block_reason still falls through to | |
| 481 | + // mobile_separate and reporting it as "blocking" would nag | |
| 482 | + // about a rewrite that does not exist there (#108). | |
| 483 | + // LiteSpeed joined the capable set with the opt-in (#509). | |
| 484 | + 'blocking' => ( $rewrite_capable || Server::LITESPEED === $server_type ) | |
| 485 | + && 'mobile_separate' === Cache::static_rewrite_block_reason(), | |
| 486 | + 'needs_review' => Cache::mobile_separate_needs_review(), | |
| 487 | + ), | |
| 190 | 488 | ) |
| 191 | 489 | ); |
| 192 | 490 | } |
| 193 | 491 | |
| @@ -209,13 +507,47 @@ | ||
| 209 | 507 | return rest_ensure_response( $updated ); |
| 210 | 508 | } |
| 211 | 509 | |
| 212 | 510 | public function purge() { |
| 213 | - Cache::purge_all(); | |
| 214 | - return rest_ensure_response( array( 'stats' => Cache::get_stats() ) ); | |
| 511 | + // The same core function `wp xspeed purge` runs, so the dashboard | |
| 512 | + // button and the CLI cannot clear different sets of stores — and the | |
| 513 | + // per-store report is available here for the UI to surface a store | |
| 514 | + // that was skipped or refused rather than flashing "cache cleared". | |
| 515 | + $report = Purge_Runner::run( array( 'all' ), __( 'dashboard', 'xspeed' ) ); | |
| 516 | + return rest_ensure_response( | |
| 517 | + array( | |
| 518 | + 'stats' => Cache::get_stats(), | |
| 519 | + 'report' => $report, | |
| 520 | + ) | |
| 521 | + ); | |
| 215 | 522 | } |
| 216 | 523 | |
| 217 | 524 | /** |
| 525 | + * The "Cached Pages" drill-down: which pages are cached and how old they | |
| 526 | + * are. Paginated because a busy site's cache is thousands of entries and | |
| 527 | + * the answer to "is my cache working" doesn't need all of them at once. | |
| 528 | + */ | |
| 529 | + public function cache_inventory( \WP_REST_Request $request ) { | |
| 530 | + return rest_ensure_response( | |
| 531 | + Cache_Inventory::entries( | |
| 532 | + (int) $request->get_param( 'limit' ), | |
| 533 | + (int) $request->get_param( 'offset' ), | |
| 534 | + (bool) $request->get_param( 'fresh' ) | |
| 535 | + ) | |
| 536 | + ); | |
| 537 | + } | |
| 538 | + | |
| 539 | + /** The "Cache Size" drill-down: where the bytes actually go. */ | |
| 540 | + public function cache_size() { | |
| 541 | + return rest_ensure_response( Cache_Inventory::size_breakdown() ); | |
| 542 | + } | |
| 543 | + | |
| 544 | + /** The "Last Purge" drill-down: what cleared the cache, when, and why. */ | |
| 545 | + public function cache_purge_log( \WP_REST_Request $request ) { | |
| 546 | + return rest_ensure_response( Cache_Inventory::purge_log( (int) $request->get_param( 'limit' ) ) ); | |
| 547 | + } | |
| 548 | + | |
| 549 | + /** | |
| 218 | 550 | * Resolved branding ({name, footer_credit, hide_help_links, logo_svg}). |
| 219 | 551 | * Runs the `xspeed_branding` filter so Pro's white-label override is |
| 220 | 552 | * reflected. Consumed by the dashboard's post-save branding refresh. |
| 221 | 553 | */ |
| @@ -222,8 +554,77 @@ | ||
| 222 | 554 | public function get_branding() { |
| 223 | 555 | return rest_ensure_response( Admin::branding() ); |
| 224 | 556 | } |
| 225 | 557 | |
| 558 | + /** | |
| 559 | + * Re-run the static-rewrite probe, bypassing the cached result. | |
| 560 | + * | |
| 561 | + * Returns the same shape the dashboard bootstrap uses, so the caller can | |
| 562 | + * swap it straight into state without a second round trip. (FBS-84012) | |
| 563 | + */ | |
| 564 | + public function recheck_rewrite() { | |
| 565 | + $raw = Cache::recheck_static_rewrite(); | |
| 566 | + $server_type = Server::detect(); | |
| 567 | + | |
| 568 | + // Qualify the raw probe against known config refusals. The probe | |
| 569 | + // fetches its own file from the static tree, which succeeds even when | |
| 570 | + // no real page is served that way — so an unqualified `active` told | |
| 571 | + // clients the static path was engaged on sites where it demonstrably | |
| 572 | + // wasn't. `block_reason` is exposed so a client can act on the | |
| 573 | + // specific cause rather than re-deriving it. See | |
| 574 | + // Cache::qualify_rewrite_probe(). | |
| 575 | + $probe = Cache::qualify_rewrite_probe( $raw ); | |
| 576 | + | |
| 577 | + return rest_ensure_response( | |
| 578 | + array( | |
| 579 | + 'active' => $probe['active'], | |
| 580 | + 'pending' => (bool) ( $raw['pending'] ?? false ), | |
| 581 | + 'inconclusive' => $probe['inconclusive'], | |
| 582 | + 'reason' => $probe['reason'], | |
| 583 | + 'block_reason' => $probe['block_reason'], | |
| 584 | + 'server_type' => $server_type, | |
| 585 | + 'snippet' => Cache::nginx_snippet(), | |
| 586 | + 'topology' => Server::rewrite_topology(), | |
| 587 | + 'behind_proxy' => Server::is_behind_proxy(), | |
| 588 | + // The whole reason to re-run the probe is usually that the | |
| 589 | + // user just pasted the block, so this is where they most need | |
| 590 | + // to be told whether the installed rules are the current ones. | |
| 591 | + // It was only ever on /status before, which the CLI and MCP | |
| 592 | + // recheck paths never call. See Cache::rules_state(). | |
| 593 | + 'rules' => $probe['rules'], | |
| 594 | + ) | |
| 595 | + ); | |
| 596 | + } | |
| 597 | + | |
| 598 | + /** | |
| 599 | + * Remember that this admin copied the current rules block. | |
| 600 | + * | |
| 601 | + * The mirror of the panel's own localStorage note, for the one case that | |
| 602 | + * note cannot cover: a host where the probe returns `unknown` — blocked | |
| 603 | + * loopback, or a CDN answering it — and a second admin, or the same admin | |
| 604 | + * on another machine, is otherwise told to paste a block that is already | |
| 605 | + * installed. Stored per user because it is a claim a person made. | |
| 606 | + * | |
| 607 | + * The hash is the rules marker, and a value that is not one is refused | |
| 608 | + * rather than stored: the mirror is only useful while it holds something | |
| 609 | + * rules_marker_expected() could also produce. | |
| 610 | + */ | |
| 611 | + public function nginx_copied_hash( \WP_REST_Request $request ) { | |
| 612 | + $params = (array) $request->get_json_params(); | |
| 613 | + $hash = isset( $params['hash'] ) ? (string) $params['hash'] : (string) $request->get_param( 'hash' ); | |
| 614 | + | |
| 615 | + $stored = Cache::remember_rules_copied( $hash ); | |
| 616 | + if ( null === $stored ) { | |
| 617 | + return new \WP_Error( | |
| 618 | + 'xspeed_invalid_rules_hash', | |
| 619 | + __( 'That is not a rules marker this site could have generated.', 'xspeed' ), | |
| 620 | + array( 'status' => 400 ) | |
| 621 | + ); | |
| 622 | + } | |
| 623 | + | |
| 624 | + return rest_ensure_response( array( 'copied' => $stored ) ); | |
| 625 | + } | |
| 626 | + | |
| 226 | 627 | public function toggle_cache( \WP_REST_Request $request ) { |
| 227 | 628 | $params = $request->get_json_params(); |
| 228 | 629 | $enabled = isset( $params['enabled'] ) ? (bool) $params['enabled'] : false; |
| 229 | 630 | |
| @@ -231,9 +632,9 @@ | ||
| 231 | 632 | // permission_callback above already enforced current_user_can( |
| 232 | 633 | // 'manage_options' ); the REST nonce is verified by core via the |
| 233 | 634 | // X-WP-Nonce header. |
| 234 | 635 | $state = Cache::toggle( $enabled ); |
| 235 | - $updated = Settings::update( array( 'cache_enabled' => $state['enabled'] ) ); | |
| 636 | + $updated = Settings::get(); | |
| 236 | 637 | |
| 237 | 638 | // Recompute the unified nginx block AFTER cache_enabled is persisted. |
| 238 | 639 | // Cache::toggle() computes it inline, but cache_enabled isn't written |
| 239 | 640 | // until the Settings::update() above — so the block inside $state |
| @@ -243,11 +644,17 @@ | ||
| 243 | 644 | $state['nginx_server_block'] = Cache::full_nginx_server_block(); |
| 244 | 645 | |
| 245 | 646 | return rest_ensure_response( |
| 246 | 647 | array( |
| 247 | - 'enabled' => $updated['cache_enabled'], | |
| 248 | - 'stats' => Cache::get_stats(), | |
| 249 | - 'install_state' => $state, | |
| 648 | + 'enabled' => $updated['cache_enabled'], | |
| 649 | + // Surfaced at the top level so the dashboard can explain a | |
| 650 | + // refusal rather than silently snapping the toggle back: | |
| 651 | + // Cache::toggle() writes nothing when another plugin owns the | |
| 652 | + // drop-in or WP_CACHE is in a shape we must not rewrite. | |
| 653 | + 'blocked' => ! empty( $state['blocked'] ), | |
| 654 | + 'blocked_reason' => $state['blocked_reason'] ?? null, | |
| 655 | + 'stats' => Cache::get_stats(), | |
| 656 | + 'install_state' => $state, | |
| 250 | 657 | ) |
| 251 | 658 | ); |
| 252 | 659 | } |
| 253 | 660 | } |