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;
}
}