false, ); } public static function get() { $saved = get_option( self::OPTION_KEY, array() ); return wp_parse_args( $saved, self::defaults() ); } public static function update( array $input ) { $current = self::get(); $clean = $current; // Every former field is now in per-module storage: // - minify_* → MinifyModule, gzip_enabled → GzipModule, // cache_expiry / excluded_urls → CacheModule. // Only cache_enabled lives on here, owned by Cache::toggle's // drop-in lifecycle. All other writes are silently ignored to // keep duplicate sources from re-forming. if ( isset( $input['cache_enabled'] ) ) { $clean['cache_enabled'] = (bool) $input['cache_enabled']; } update_option( self::OPTION_KEY, $clean ); return $clean; } public static function set_defaults() { if ( false === get_option( self::OPTION_KEY ) ) { add_option( self::OPTION_KEY, self::defaults() ); // Only a genuinely fresh install reaches this branch — the // option survives deactivation, so an upgrade (deactivate → // wipe → install → activate) always finds it present. self::seed_recommended_modules(); } } /** * Settings a fresh install starts with, beyond each module's schema * default. Mirrors the wizard's "Balanced" preset — the profile the * product already labels "Recommended for most sites". * * Why this exists: the wizard is skippable, and a WP-CLI or bulk * activation never shows it at all. Those users fell through to the raw * schema defaults, which are more conservative than what we recommend to * the very same site — so whether a site compressed its responses came * down to whether someone clicked through a wizard. Measured on a real * install: gzip off, browser-cache headers off, no minification, while * lazy-load and resource hints (schema default `true`) were on. * * Deliberately excluded: `minify_js`, `defer_js`, `combine_css`, * `combine_js`, `delay_js`. Each can break a theme, and a default that * breaks the site is worse than a default that is merely slow. They stay * opt-in via the wizard's Aggressive preset or the dashboard. * * @return array> module slug => settings */ private static function recommended_module_settings(): array { return array( // Compression — the single largest byte win, and inert until a // server actually supports it (GzipModule writes .htaccess only // where supports_htaccess() is true, and emits a snippet // otherwise). 'gzip' => array( 'gzip_enabled' => true ), // Far-future caching for static assets. Only ever affects // css/js/images/fonts; HTML keeps its own short TTL. 'browser-cache' => array( 'enabled' => true ), // HTML + CSS minification. Both are whitespace/comment-level // and do not reorder or combine anything, so they carry none of // the cascade risk that combine_css does. 'minify' => array( 'minify_html' => true, 'minify_css' => true, ), ); } /** * Write the recommended defaults for a fresh install, without ever * overwriting a value the user has already chosen. * * Each key is written only when it is absent from stored settings, so * this stays safe if it is ever reached on a site that has some — but * not all — module options saved. */ private static function seed_recommended_modules(): void { foreach ( self::recommended_module_settings() as $slug => $values ) { $key = 'xspeed_module_' . $slug; $stored = get_option( $key, null ); $stored = is_array( $stored ) ? $stored : array(); $next = $stored; foreach ( $values as $setting => $value ) { if ( ! array_key_exists( $setting, $stored ) ) { $next[ $setting ] = $value; } } if ( $next !== $stored ) { update_option( $key, $next, false ); } } } // sanitize_urls() removed — excluded_urls now owned by CacheModule // and validated by Settings_Manager's typed schema (list / item_type). }