'GET', 'callback' => array( $this, 'get_status' ), 'permission_callback' => array( $this, 'permissions' ), ) ); register_rest_route( self::NAMESPACE_V1, '/settings', array( array( 'methods' => 'GET', 'callback' => array( $this, 'get_settings' ), 'permission_callback' => array( $this, 'permissions' ), ), array( 'methods' => 'POST', 'callback' => array( $this, 'update_settings' ), 'permission_callback' => array( $this, 'permissions' ), ), ) ); // Resolved white-label branding. The dashboard refetches this after // a white-label save so the chrome (sidebar name/logo, footer) // updates live without a reload. (FBS white-label-onboarding) register_rest_route( self::NAMESPACE_V1, '/branding', array( 'methods' => 'GET', 'callback' => array( $this, 'get_branding' ), 'permission_callback' => array( $this, 'permissions' ), ) ); register_rest_route( self::NAMESPACE_V1, '/cache/purge', array( 'methods' => 'POST', 'callback' => array( $this, 'purge' ), 'permission_callback' => array( $this, 'permissions' ), ) ); register_rest_route( self::NAMESPACE_V1, '/cache/toggle', array( 'methods' => 'POST', 'callback' => array( $this, 'toggle_cache' ), 'permission_callback' => array( $this, 'permissions' ), ) ); register_rest_route( self::NAMESPACE_V1, '/cache/benchmark', array( 'methods' => 'GET', 'callback' => array( $this, 'benchmark' ), 'permission_callback' => array( $this, 'permissions' ), ) ); register_rest_route( self::NAMESPACE_V1, '/audit/pro', array( 'methods' => 'GET', 'callback' => array( $this, 'pro_audit' ), 'permission_callback' => array( $this, 'permissions' ), ) ); register_rest_route( self::NAMESPACE_V1, '/modules', array( 'methods' => 'GET', 'callback' => array( $this, 'get_modules' ), 'permission_callback' => array( $this, 'permissions' ), ) ); // On-demand desktop-vs-mobile HTML equality probe (FBS-83145). POST so // it's never triggered by a prefetch/GET; runs only from the dashboard // "Check now" button behind manage_options. register_rest_route( self::NAMESPACE_V1, '/cache/mobile-probe', array( 'methods' => 'POST', 'callback' => array( $this, 'mobile_probe' ), 'permission_callback' => array( $this, 'permissions' ), ) ); // Dismiss the Separate-Mobile-Cache review prompt (FBS-83145). register_rest_route( self::NAMESPACE_V1, '/cache/mobile-review-dismiss', array( 'methods' => 'POST', 'callback' => array( $this, 'mobile_review_dismiss' ), 'permission_callback' => array( $this, 'permissions' ), ) ); } /** * Run the on-demand mobile-equality probe and return the fresh /status * mobile_separate block so the dashboard can update the callout in place. */ public function mobile_probe( $request ) { unset( $request ); return rest_ensure_response( Cache::probe_mobile_equality() ); } /** * Clear the migration review flag so the callout stops nagging. */ public function mobile_review_dismiss( $request ) { unset( $request ); Cache::clear_mobile_separate_review(); return rest_ensure_response( array( 'dismissed' => true ) ); } /** * The registered-module descriptors — same payload baked into the * admin bootstrap (Admin::modules_payload), re-evaluated live. The * dashboard re-fetches this after a license activate/deactivate so a * Pro module's custom_panel flips between its real surface and * LicenseLockedPanel (decided server-side via the * xspeed_module_descriptor filter) WITHOUT a full page reload. */ public function get_modules() { return rest_ensure_response( Admin::modules_payload() ); } /** * Run the Pro audit — scans current settings + cache stats, * returns a personalized list of Pro features that would help * THIS site. See Pro_Audit::run() for the rule set. */ public function pro_audit( $request ) { unset( $request ); return rest_ensure_response( array( 'suggestions' => Pro_Audit::run() ) ); } /** * Cache before/after benchmark — fetches home_url() twice (with + * without the bypass header) and returns side-by-side timings for * the dashboard widget. */ public function benchmark( $request ) { unset( $request ); return rest_ensure_response( Cache_Benchmark::run() ); } public function permissions() { return current_user_can( 'manage_options' ); } public function get_status() { $opts = Settings::get(); $stats = Cache::get_stats(); // rewrite_probe + nginx_server_block mirror the admin bootstrap // payload (Admin::bootstrap_data). The dashboard re-fetches /status // after every module save to refresh the consolidated nginx // server-block snippet without a full page reload — if these were // omitted here, the snippet would only ever update on reload (the // QA bug: "Server config snippet requires full page reload to // reflect toggle changes"). Keep this in sync with Admin. $server_type = Server::type(); // LiteSpeed deliberately serves hits via the PHP drop-in (its // .htaccess can't add the HIT header or log a static hit), so the // static-rewrite probe is N/A there — surfacing it would pop the // "PHP fallback" nag for a setup that's working as designed. Only // nginx + Apache use a server-level rewrite worth probing. $rewrite_capable = ( $server_type === Server::NGINX || $server_type === Server::APACHE ); $rewrite_probe = null; if ( $opts['cache_enabled'] && $rewrite_capable ) { $probe = Cache::probe_static_rewrite(); $rewrite_probe = array( 'active' => (bool) ( $probe['active'] ?? false ), 'server_type' => $server_type, 'snippet' => Cache::nginx_snippet(), 'topology' => Server::rewrite_topology(), 'behind_proxy' => Server::is_behind_proxy(), ); } return rest_ensure_response( array( 'enabled' => (bool) $opts['cache_enabled'], 'stats' => $stats, 'server' => array( 'type' => $server_type, 'gzip_mode' => Server::gzip_mode(), 'gzip_active' => Gzip::probe_active(), 'nginx_snippet' => Gzip::nginx_snippet(), ), 'rewrite_probe' => $rewrite_probe, 'nginx_server_block' => Cache::full_nginx_server_block(), // Separate Mobile Cache visibility (FBS-83145). `blocking` is // true when mobile_separate is what's keeping the device-blind // static fast path from installing on a rewrite-capable server; // `needs_review` is true when a migration turned it on for us and // the user hasn't confirmed they actually need it. The dashboard // renders a callout (+ "Check now" equality probe) from these. 'mobile_separate' => array( 'enabled' => ! empty( Settings::get()['cache_enabled'] ) ? (bool) ( Settings_Manager::get( 'cache' )['mobile_separate'] ?? false ) : false, 'blocking' => $rewrite_capable && 'mobile_separate' === Cache::static_rewrite_block_reason(), 'needs_review' => Cache::mobile_separate_needs_review(), ), ) ); } public function get_settings() { return rest_ensure_response( Settings::get() ); } public function update_settings( \WP_REST_Request $request ) { $params = $request->get_json_params(); if ( ! is_array( $params ) ) { $params = $request->get_params(); } // `cache_enabled` is the trigger for drop-in install / wp-config.php // edit and must only flow through the dedicated /cache/toggle // endpoint. Strip it here so generic settings updates can never // implicitly write a drop-in or modify wp-config.php. unset( $params['cache_enabled'] ); $updated = Settings::update( $params ); return rest_ensure_response( $updated ); } public function purge() { Cache::purge_all(); return rest_ensure_response( array( 'stats' => Cache::get_stats() ) ); } /** * Resolved branding ({name, footer_credit, hide_help_links, logo_svg}). * Runs the `xspeed_branding` filter so Pro's white-label override is * reflected. Consumed by the dashboard's post-save branding refresh. */ public function get_branding() { return rest_ensure_response( Admin::branding() ); } public function toggle_cache( \WP_REST_Request $request ) { $params = $request->get_json_params(); $enabled = isset( $params['enabled'] ) ? (bool) $params['enabled'] : false; // User-explicit drop-in install / wp-config.php edit happens here. // permission_callback above already enforced current_user_can( // 'manage_options' ); the REST nonce is verified by core via the // X-WP-Nonce header. $state = Cache::toggle( $enabled ); $updated = Settings::update( array( 'cache_enabled' => $state['enabled'] ) ); // Recompute the unified nginx block AFTER cache_enabled is persisted. // Cache::toggle() computes it inline, but cache_enabled isn't written // until the Settings::update() above — so the block inside $state // reflects the PRE-toggle state (CacheModule::nginx_directives() gates // on cache_enabled). Regenerate here so the dashboard's optimistic // update shows the snippet for the state the user just selected. $state['nginx_server_block'] = Cache::full_nginx_server_block(); return rest_ensure_response( array( 'enabled' => $updated['cache_enabled'], 'stats' => Cache::get_stats(), 'install_state' => $state, ) ); } }