'Cache', 'icon' => 'Database', 'description' => 'Page caching for non-logged-in visitors.', ); } public function settings_schema(): array { return array( 'cache_expiry' => array( 'type' => 'int', 'default' => 24, 'min' => 1, 'max' => 720, 'label' => 'Cache Expiry (hours)', 'description' => 'How long cached pages live before regenerating. 1 to 720 hours (30 days).', ), 'excluded_urls' => array( 'type' => 'list', // Comprehensive LiteSpeed / WP Rocket-parity default URL // exclusions (FBS-82181). Plain text = "contains", glob via // * ? [ ], or a `~` prefix for raw regex (e.g. ~wp-.*\.php). 'default' => array( '/wp-admin/', '/wp-json/', '/xmlrpc.php', '~wp-.*\.php', '/feed/', 'index.php', '~sitemap(_index)?\.xml', // Bare (no trailing slash) so "contains" matches both // /cart and /cart/items — WooCommerce serves both forms. '/cart', '/checkout', '/my-account', 'ao_noptirocket', 'ao_speedup_cachebuster', 'removed_item', '/wc-api', '/edd-api', '/wp-login', ), 'item_type' => 'string', 'label' => 'Excluded URLs', 'description' => 'One pattern per line. Plain text matches anywhere in the URL (e.g. /cart). Use glob for anchored matches (/cart/* matches /cart/items but not /foo/cart/bar; *.pdf matches PDFs). Prefix with ~ for a raw regex (e.g. ~wp-.*\.php).', ), 'excluded_cookies' => array( 'type' => 'list', // Cookies that signal a logged-in / transactional visitor // whose response must not be served from a shared cache. // `~` prefix = raw regex (e.g. ~wordpress_[a-f0-9]+). (FBS-82181) 'default' => array( 'comment_author', '~wordpress_[a-f0-9]+', 'wp-postpass', 'wordpress_no_cache', 'wordpress_logged_in', 'edd_items_in_cart', 'woocommerce_items_in_cart', 'fct_cart_hash', 'comment_', 'woocommerce_', 'wordpress', 'xf_', 'edd_', 'jetpack', 'yith_wcwl_session_', 'yith_wrvp_', 'wpsc_', 'ecwid', 'ec_', 'bookly', ), 'item_type' => 'string', 'label' => 'Excluded Cookies', 'description' => 'Skip cache for any visitor whose request carries a cookie whose NAME matches one of these patterns. Plain text = "contains"; glob (woocommerce_*) and ~regex (~wordpress_[a-f0-9]+) supported. One per line.', ), 'bypass_user_agents' => array( 'type' => 'list', 'default' => array(), 'item_type' => 'string', 'label' => 'Bypass User Agents', 'description' => 'Substring match against the visitor User-Agent. Matched UAs bypass cache (useful for screenshot bots, internal previews, monitoring). Glob + ~regex supported. One per line.', ), 'ignored_query_params' => array( 'type' => 'list', // Analytics / ad / session query keys stripped before the // cache key is computed, so /post?utm_source=x and /post // share one entry. `~` prefix = raw regex. (FBS-82181) 'default' => array( '__s', '_ga', '_ke', '~[a-zA-Z0-9_-]+_sid', 'adgroupid', 'age-verified', 'ao_noptimize', 'campaignid', 'ck_subscriber_id', 'cn-reloaded', 'dclid', 'epik', 'fb_action_ids', 'fb_action_types', 'fb_source', 'fbclid', 'gclid', 'jobid', 'mc_cid', 'mc_eid', 'mkt_tok', 'msclkid', 'ref', '~session_[a-zA-Z0-9_-]+_alive', 'sseid', 'sslid', 'usqp', '~utm_[a-zA-Z0-9_-]+', ), 'item_type' => 'string', 'label' => 'Ignored Query Parameters', 'description' => 'Query keys removed from the URL before computing the cache key, so /post?utm_source=x and /post share a cache entry. Defaults cover the common analytics + ad + session params. Glob + ~regex supported. One per line.', ), 'mobile_separate' => array( 'type' => 'bool', 'default' => false, 'label' => 'Separate Mobile Cache', 'description' => 'Keep mobile and desktop responses in separate cache buckets. Turn on for AMP, mobile-specific themes (WPtouch / Jetpack mobile theme), or any setup that serves different HTML by device.', ), ); } /** * Seed per-module option from the legacy xspeed_options blob if we * haven't done so yet. Idempotent — once xspeed_module_cache exists * or the legacy keys are gone, this is a no-op. Runs on both boot * and activate so installs on every code path are covered. */ public function boot(): void { $this->seed_from_legacy_if_needed(); } public function activate(): void { $this->seed_from_legacy_if_needed(); } private function seed_from_legacy_if_needed(): void { if ( null !== get_option( 'xspeed_module_cache', null ) ) { return; } $legacy = get_option( 'xspeed_options', array() ); if ( ! is_array( $legacy ) ) { return; } $seed = array( '_version' => self::VERSION ); $dirty = false; if ( array_key_exists( 'cache_expiry', $legacy ) ) { $seed['cache_expiry'] = max( 1, min( 720, (int) $legacy['cache_expiry'] ) ); unset( $legacy['cache_expiry'] ); $dirty = true; } if ( array_key_exists( 'excluded_urls', $legacy ) ) { $seed['excluded_urls'] = is_array( $legacy['excluded_urls'] ) ? array_values( array_filter( $legacy['excluded_urls'], 'is_string' ) ) : array(); unset( $legacy['excluded_urls'] ); $dirty = true; } if ( $dirty ) { update_option( 'xspeed_module_cache', $seed ); update_option( 'xspeed_options', $legacy ); } } public function cli_commands(): array { return array( array( 'name' => 'xspeed cache', 'callback' => array( $this, 'cli_handler' ), 'shortdesc' => 'Inspect Cache module settings (purge / toggle use the dedicated commands).', 'synopsis' => array( array( 'type' => 'positional', 'name' => 'action', 'options' => array( 'status' ), 'optional' => true, ), ), ), ); } public function cli_handler( array $args, array $assoc ): void { $opts = Settings_Manager::get( self::SLUG ); \WP_CLI::log( 'cache_expiry ' . $opts['cache_expiry'] . 'h' ); \WP_CLI::log( 'excluded_urls ' . count( $opts['excluded_urls'] ) . ' entries' ); foreach ( $opts['excluded_urls'] as $u ) { \WP_CLI::log( ' - ' . $u ); } } /** * Static-rewrite directives for the unified nginx server-block * snippet. Returns null when cache is disabled — there's no rewrite * to install in that state. Delegates to \XSpeed\Cache::nginx_snippet() * which already produces nginx-detection-gated output. */ public function nginx_directives(): ?string { $opts = get_option( 'xspeed_options', array() ); if ( empty( $opts['cache_enabled'] ) ) { return null; } return \XSpeed\Cache::nginx_snippet(); } }