do_init(); } catch ( \Throwable $e ) { // Prevent a single module from taking down the entire site. // Surface a soft admin notice when possible. if ( function_exists( 'is_admin' ) && is_admin() && function_exists( 'add_action' ) ) { add_action( 'admin_notices', function () use ( $e ) { if ( ! current_user_can( 'manage_options' ) ) { return; } echo '

QW Optimize failed to fully initialize: ' . esc_html( $e->getMessage() ) . ' (see debug.log).

'; } ); } } } /** * Actual bootstrap (separated so a Throwable can be caught). */ private function do_init() { $this->maybe_migrate(); $this->load_settings(); $this->load_textdomain(); add_filter( 'cron_schedules', array( $this, 'cron_schedules' ) ); // Per-post exclusions must also be registered on public requests, not // just within wp-admin. Query-dependent filters evaluate after `wp`. new Admin\PostMetabox(); // Free settings are never removed or zeroed based on add-on status. // Add-ons may register their own engines through the public hooks. // Runtime settings with any third-party filters applied. $s = apply_filters( 'qwsc_runtime_settings', $this->settings ); $cache_on = ! empty( $s['cache_enabled'] ) || ! empty( $s['page_cache_enabled'] ); // --- Always: cache core when any cache feature is relevant --- if ( $cache_on ) { new Cache\Engine(); new Cache\Stats(); new Cache\Debug(); new Cache\EdgeDetect(); new Cache\Headers(); new Cache\Purge(); new Cache\AlwaysPurge(); new Cache\AdvancedCache(); new Cache\StaleCleanup(); } else { // Purge still useful if user disables cache later / admin actions. new Cache\Purge(); new Cache\AdvancedCache(); } new Settings\Profiles(); // --- Assets / optimization (front-end cost) --- $assets_on = ! empty( $s['minify_css'] ) || ! empty( $s['minify_js'] ) || ! empty( $s['minify_html'] ) || ! empty( $s['combine_css'] ) || ! empty( $s['combine_js'] ) || ! empty( $s['defer_js'] ) || ! empty( $s['google_fonts_optimize'] ) || ! empty( $s['font_display_swap'] ) || ! empty( $s['asset_unload_enabled'] ) || ! empty( $s['optimize_enabled'] ); if ( $assets_on || is_admin() ) { new Assets\Manager(); new Assets\ScriptManager(); new Assets\HardcodedAssets(); new Assets\ResourceAttributes(); new Assets\Overview(); if ( is_admin() ) { new Assets\AdminManager(); } new Optimization\Css(); new Optimization\Minify\Html(); new Optimization\Minify\Rollback(); new Optimization\Js(); new Optimization\GoogleFonts(); new Optimization\FontDisplay(); } // --- Media --- $media_on = ! empty( $s['lazyload_images'] ) || ! empty( $s['lazyload_iframes'] ) || ! empty( $s['image_dimensions'] ) || ! empty( $s['preload_critical_images'] ) || ! empty( $s['svg_upload_enabled'] ); if ( $media_on || is_admin() ) { // Critical images must register before LazyLoad so the LCP candidate is // protected from lazy-loading and can receive high fetch priority. new Media\CriticalImages(); new Media\LazyLoad(); new Media\ImageDimensions(); new Media\Cleanup(); new Media\BrokenImages(); } if ( ! empty( $s['svg_upload_enabled'] ) ) { if ( class_exists( '\QWSpeedCache\Media\SvgUpload' ) ) { new Media\SvgUpload(); } } // Premium SEO and monitoring engines are registered by the Pro add-on. new Stats\Dashboard(); new Stats\Crux(); new Stats\SpeedTest(); // --- CDN --- if ( ! empty( $s['cdn_enabled'] ) || is_admin() ) { new CDN\Cdn(); } new Database\Cleanup(); new Preload\Manager(); // --- Compatibility (keep Woo/builders always if plugin present) --- new Compatibility\WooCommerce(); new Compatibility\Builders(); new Compatibility\PluginFixes(); new Compatibility\Themes(); new Compatibility\Multilingual(); new Compatibility\Ecommerce(); // Plugin Organizer is a Pro-owned module, registered at the extension boundary. if ( ! empty( $s['maintenance_mode'] ) || is_admin() ) { new Compatibility\MaintenanceMode(); } if ( ! empty( $s['local_analytics_enabled'] ) || is_admin() ) { new Compatibility\LocalAnalytics(); } if ( ! empty( $s['indexnow_enabled'] ) || is_admin() ) { new Compatibility\IndexNow(); } new Compatibility\RobotsTxt(); if ( ! empty( $s['local_avatars'] ) || is_admin() ) { new Compatibility\LocalAvatarField(); } if ( ! empty( $s['redirects_enabled'] ) || is_admin() ) { new Compatibility\Redirects(); } new Compatibility\CookieConsent(); new Compatibility\HeadCleanup(); new Compatibility\Bloat(); // Smart third-party script unloading is implemented and registered by Pro only. new Compatibility\Access(); /** * Extension boundary. The Free package never boots premium engines itself. * A separately installed add-on can register its runtime here. * * @param Plugin $plugin Main plugin instance. * @param array $settings Runtime settings supplied to extension engines. */ do_action( 'qwsc_extensions_boot', $this, $s ); new Rest\Controller(); new Safety\SafeMode(); if ( defined( 'WP_CLI' ) && WP_CLI ) { CLI\Commands::register(); } new Tools\ImportExport(); new Tools\SupportExport(); new Tools\OpCache(); new Tools\Debug(); new Tools\ToolsPage(); new Tools\MissedSchedule(); new Tools\ScheduleUnpublish(); if ( is_admin() ) { // Migrate only QW-owned legacy cache guards once after an upgrade. add_action( 'admin_init', array( __CLASS__, 'maybe_upgrade_cache_dir_guards' ), 2 ); new Admin\Settings(); new Admin\Menu(); new Admin\Notices(); new Admin\Wizard(); new Admin\Onboarding(); new Admin\AutoPreset(); new Admin\DomainChange(); new Admin\SetupChecklist(); new Admin\ModuleToggle(); } // Front toolbar may load for admins on front. new Admin\FrontendToolbar(); new Admin\AdminBar(); new Front\Optimizer(); new Front\HoverPrefetch(); new Front\ResourceHints(); /** Fires after QW Optimize Free has completed its runtime bootstrap. */ do_action( 'qwsc_ready', $this, $s ); } private function load_settings() { $defaults = \QWSpeedCache\Settings\Schema::defaults(); $saved = get_option( 'qwsc_settings', array() ); $this->settings = wp_parse_args( $saved, $defaults ); } public function get_setting( $key, $default = null ) { $s = $this->get_all_settings(); return isset( $s[ $key ] ) ? $s[ $key ] : $default; } public function update_settings( $new ) { $this->settings = wp_parse_args( $new, $this->settings ); update_option( 'qwsc_settings', $this->settings ); } public function get_all_settings() { return apply_filters( 'qwsc_runtime_settings', $this->settings ); } /** * Reload settings from the options table into the singleton cache. * Required after direct update_option( 'qwsc_settings', ... ) (e.g. tests). */ public function reload_settings() { $this->load_settings(); return $this->settings; } private function load_textdomain() { } public static function activate() { if ( class_exists( '\QWSpeedCache\Compatibility\LocalAnalytics' ) ) { \QWSpeedCache\Compatibility\LocalAnalytics::schedule_event(); } if ( class_exists( '\QWSpeedCache\Tools\MissedSchedule' ) ) { \QWSpeedCache\Tools\MissedSchedule::schedule_event(); } if ( class_exists( '\QWSpeedCache\Security\VulnAlerts' ) ) { \QWSpeedCache\Security\VulnAlerts::schedule_event(); } // Create and harden cache directory (WP_Filesystem for WordPress.org guidelines). if ( ! file_exists( QWSC_CACHE_DIR ) ) { wp_mkdir_p( QWSC_CACHE_DIR ); } if ( self::write_cache_dir_guards() ) { update_option( 'qwsc_cache_guards_version', '2', false ); } if ( class_exists( '\QWSpeedCache\Admin\AutoPreset' ) ) { \QWSpeedCache\Admin\AutoPreset::on_activate(); } // Default options if ( false === get_option( 'qwsc_settings' ) ) { update_option( 'qwsc_settings', array() ); // Keep initial settings lightweight. Presets are opt-in and can // enable server-level compression / .htaccess changes explicitly. update_option( 'qwsc_settings', array_merge( (array) get_option( 'qwsc_settings', array() ), array( 'cache_enabled' => 1, 'page_cache_enabled' => 1, 'lazyload_images' => 1, 'js_safe_mode' => 1, 'builder_compat' => 1, 'page_cache_static_serve' => 0, 'advanced_cache_enabled' => 0, ) ) ); } // Refresh the post exclusion index before any optional static rules are built. // This also migrates sites that used the older 200-path list. if ( class_exists( '\\QWSpeedCache\Admin\PostMetabox' ) ) { \QWSpeedCache\Admin\PostMetabox::rebuild_exclusions(); } // Install advanced-cache drop-in when enabled. if ( class_exists( '\\QWSpeedCache\\Cache\\AdvancedCache' ) ) { $s = get_option( 'qwsc_settings', array() ); if ( ! empty( $s['advanced_cache_enabled'] ) ) { \QWSpeedCache\Cache\AdvancedCache::install(); } } if ( class_exists( '\\QWSpeedCache\\Cache\\HtaccessRules' ) ) { $active_rules = (array) get_option( 'qwsc_settings', array() ); // Do not write site-root server rules on first activation unless // the administrator has enabled a setting that needs them. if ( '' !== \QWSpeedCache\Cache\HtaccessRules::build_all( $active_rules ) ) { \QWSpeedCache\Cache\HtaccessRules::sync( $active_rules ); } } flush_rewrite_rules(); /** Allow separately installed extensions to perform activation work. */ do_action( 'qwsc_activated' ); } /** * Upgrade the legacy deny-all cache guard without touching custom rules. * Only site administrators initiate the repair, once per plugin installation. */ public static function maybe_upgrade_cache_dir_guards() { if ( ! current_user_can( 'manage_options' ) || '2' === (string) get_option( 'qwsc_cache_guards_version', '' ) ) { return; } if ( self::write_cache_dir_guards() ) { update_option( 'qwsc_cache_guards_version', '2', false ); } } /** * Protect private cache files while allowing generated CSS/JS to be served. * Anonymous cached HTML is served through an Apache rewrite only when * QWSC_STATIC has been set by the owning rewrite rule. Deny-all cache guards * from earlier releases broke static serving and used Apache 2.2 directives * that can cause HTTP 500 on Apache 2.4 without mod_access_compat. * * @return bool True if the new guard is installed, already present, or the * cache directory cannot be created safely. */ public static function write_cache_dir_guards() { if ( ! defined( 'QWSC_CACHE_DIR' ) || ! QWSC_CACHE_DIR ) { return false; } if ( ! is_dir( QWSC_CACHE_DIR ) && ! wp_mkdir_p( QWSC_CACHE_DIR ) ) { return false; } if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } global $wp_filesystem; $fs_ready = WP_Filesystem() && $wp_filesystem; $legacy = "Order Deny,Allow\nDeny from all\n\nRequire all denied\n\n"; $guard = "# QW Optimize cache access rules v2\n" . "\n" . "\nRequire all denied\n\n" . "\n\nRequire env QWSC_STATIC\nRequire env REDIRECT_QWSC_STATIC\n\n\n" . "\n" . "\n" . "\nOrder allow,deny\nDeny from all\n\n" . "\n"; $ht_path = QWSC_CACHE_DIR . '.htaccess'; $idx_path = QWSC_CACHE_DIR . 'index.html'; if ( $fs_ready ) { if ( ! $wp_filesystem->exists( $idx_path ) ) { $wp_filesystem->put_contents( $idx_path, '', FS_CHMOD_FILE ); } $previous = $wp_filesystem->exists( $ht_path ) ? $wp_filesystem->get_contents( $ht_path ) : ''; if ( false === $previous ) { return false; } if ( trim( $previous ) === trim( $guard ) ) { return true; } // Never replace a cache .htaccess that we don't recognize. if ( '' !== trim( $previous ) && trim( $previous ) !== trim( $legacy ) ) { return false; } return (bool) $wp_filesystem->put_contents( $ht_path, $guard, FS_CHMOD_FILE ); } // Some hosts have no WP_Filesystem transport during activation. if ( ! file_exists( $idx_path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- cache guard fallback only. file_put_contents( $idx_path, '' ); } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading QW Optimize's own local cache .htaccess guard before updating it. $previous = is_file( $ht_path ) ? file_get_contents( $ht_path ) : ''; if ( false === $previous || ( '' !== trim( $previous ) && trim( $previous ) !== trim( $legacy ) && trim( $previous ) !== trim( $guard ) ) ) { return false; } if ( trim( $previous ) === trim( $guard ) ) { return true; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- runtime cache folder outside plugin directory. return false !== file_put_contents( $ht_path, $guard, LOCK_EX ); } public function cron_schedules( $schedules ) { $schedules['qwsc_fifteen_min'] = array( 'interval' => 15 * \MINUTE_IN_SECONDS, 'display' => __( 'Every 15 minutes (QW Optimize)', 'qw-optimize' ), ); if ( empty( $schedules['weekly'] ) ) { $schedules['weekly'] = array( 'interval' => 7 * \DAY_IN_SECONDS, 'display' => __( 'Once weekly (QW Optimize)', 'qw-optimize' ), ); } return $schedules; } public static function deactivate() { self::clear_all_cache(); // Remove only QW Optimize-owned server rules/drop-ins. if ( class_exists( '\\QWSpeedCache\\Cache\\HtaccessRules' ) ) { \QWSpeedCache\Cache\HtaccessRules::remove(); } if ( class_exists( '\\QWSpeedCache\\Cache\\AdvancedCache' ) ) { \QWSpeedCache\Cache\AdvancedCache::uninstall(); } $hooks = array( 'qwsc_refresh_dynamic_lists', 'qwsc_cache_stale_cleanup', 'qwsc_preload_batch', 'qwsc_scheduled_db_cleanup', 'qwsc_daily_vitals_check', 'qwsc_mirror_ga_script', 'qwsc_mirror_gtm_script', 'qwsc_weekly_digest', 'qwsc_uptime_ping', 'qwsc_vuln_check', 'qwsc_il_scan_batch', 'qwsc_broken_images_batch', 'qwsc_broken_scan_queue', 'qwsc_missed_schedule_sweep', 'qwsc_unpublish_post', ); foreach ( $hooks as $hook ) { wp_clear_scheduled_hook( $hook ); } flush_rewrite_rules(); } /** * Shared gate for all optimizations (Test Mode aware). */ public function maybe_migrate() { $v = get_option( 'qwsc_db_version', '0' ); if ( version_compare( (string) $v, '2.2.0', '>=' ) ) { return; } $s = get_option( 'qwsc_settings', array() ); $dirty = false; if ( ! is_array( $s ) ) { $s = array(); $dirty = true; } if ( array_key_exists( 'plugin_unload_enabled', $s ) ) { unset( $s['plugin_unload_enabled'] ); $dirty = true; } // Orphaned schema key removed in 1.6.11 (no UI / runtime use). if ( array_key_exists( 'preload_fonts', $s ) ) { unset( $s['preload_fonts'] ); $dirty = true; } if ( $dirty ) { update_option( 'qwsc_settings', $s ); } delete_option( 'qwsc_plugin_unload' ); delete_option( 'qwsc_last_purge_error' ); // Verify before bumping version $s2 = get_option( 'qwsc_settings', array() ); $ok = is_array( $s2 ) && ! array_key_exists( 'plugin_unload_enabled', $s2 ); $ok = $ok && ! array_key_exists( 'preload_fonts', $s2 ); $ok = $ok && false === get_option( 'qwsc_plugin_unload', false ); $ok = $ok && false === get_option( 'qwsc_last_purge_error', false ); if ( $ok ) { update_option( 'qwsc_db_version', '2.2.0', false ); } } public static function should_optimize() { $settings = self::instance()->get_all_settings(); if ( ! empty( $settings['test_mode'] ) && ! current_user_can( 'manage_options' ) ) { return false; } return (bool) apply_filters( 'qwsc_should_optimize', true ); } /** * Delete generated QW Optimize cache files while retaining root guard/config files. * * @return true|\WP_Error Success or a filesystem failure. */ public static function clear_all_cache() { if ( ! defined( 'QWSC_CACHE_DIR' ) || ! is_dir( QWSC_CACHE_DIR ) ) { // A missing disk directory does not mean Redis/Memcached page entries // are absent. Invalidate QW Optimize's generation in every purge path. \QWSpeedCache\Cache\PageStore::flush_memory( self::instance()->get_all_settings() ); return true; } $cache_dir = rtrim( (string) QWSC_CACHE_DIR, '/\\' ) . '/'; $protected = array( '.htaccess', 'index.html', 'dropin-config.json' ); $failed = false; try { $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $cache_dir, \RecursiveDirectoryIterator::SKIP_DOTS ), \RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $files as $file ) { $path = $file->getPathname(); // Do not use getRealPath(): a symlink may point outside the QW cache directory. if ( $file->getPath() === rtrim( $cache_dir, '/' ) && in_array( $file->getFilename(), $protected, true ) ) { continue; } if ( $file->isDir() && ! $file->isLink() ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged -- Removing only generated empty directories inside our own cache root. if ( ! @rmdir( $path ) && is_dir( $path ) ) { $failed = true; } } else { wp_delete_file( $path ); if ( file_exists( $path ) || is_link( $path ) ) { $failed = true; } } } } catch ( \RuntimeException $e ) { // Permission failures from recursive directory traversal are also // RuntimeExceptions; report a purge failure rather than HTTP 500. $failed = true; } if ( $failed ) { return new \WP_Error( 'qwsc_purge_failed', __( 'Some QW Optimize cache files could not be removed. Check cache directory permissions.', 'qw-optimize' ) ); } // All call sites (settings, REST, CLI, toolbar and automation) share // this routine. Invalidate memory-backed page entries here so none of // them can accidentally leave stale pages after clearing disk files. \QWSpeedCache\Cache\PageStore::flush_memory( self::instance()->get_all_settings() ); if ( class_exists( '\QWSpeedCache\Stats\Logger' ) ) { \QWSpeedCache\Stats\Logger::log( 'purge', 'clear_all_cache' ); } return true; } }