<?php
/**
 * Plugin Name:       CacheLoop – Simple Page Cache & Speed Optimization
 * Plugin URI:        https://dev.intouchdesign.it/plugin/cacheloop/
 * Description:       Set-it-and-forget-it cache & speed optimization. Activate and go fast.
 * Version:           1.0.18
 * Requires at least: 6.2
 * Requires PHP:      7.4
 * Author:            InTouchDesign
 * Author URI:        https://dev.intouchdesign.it/
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       cacheloop-simple-page-cache-speed-optimization
 * Domain Path:       /languages
 */

// Security: Prevent direct file access.
defined( 'ABSPATH' ) || exit;

// ---------------------------------------------------------------------------
// WP_Filesystem Helper
// ---------------------------------------------------------------------------
/**
 * Safe wrapper to load and get the WordPress Filesystem object.
 *
 * @return WP_Filesystem_Base|false
 */
function cacheloop_get_filesystem() {
	global $wp_filesystem;
	if ( empty( $wp_filesystem ) ) {
		// WP_Filesystem() (and everything it needs, including
		// request_filesystem_credentials()) is defined here — no other core
		// file needs to be loaded directly.
		require_once ABSPATH . 'wp-admin/includes/file.php';
		// WP_Filesystem() can populate $wp_filesystem with an unconnected
		// object (e.g. ftpext with no stored credentials) and still return
		// false — checking only emptiness of the global isn't enough, every
		// caller would then try to use a filesystem that was never connected.
		if ( ! WP_Filesystem() ) {
			return false;
		}
	}
	return $wp_filesystem;
}

// ---------------------------------------------------------------------------
// Freemius SDK Initialization
// ---------------------------------------------------------------------------
if ( ! class_exists( 'CacheLoop_Fs_Stub' ) ) {
	/**
	 * Stub used when the Freemius SDK is unavailable or fails to init.
	 * Real methods (not closures on stdClass) so cacheloop_fs()->method() calls work.
	 */
	class CacheLoop_Fs_Stub {
		public function add_action( ...$args ) {}
		public function add_filter( ...$args ) {}
		public function is_premium() {
			return false;
		}
		public function is_registered() {
			return false;
		}
		public function can_use_premium_code() {
			return false;
		}
		public function is_live() {
			return false;
		}
		public function get_upgrade_url() {
			return '';
		}
		public function get_account_url() {
			return '';
		}
		public function get_reconnect_url() {
			return '';
		}
		public function set_text( ...$args ) {}
		public function init_dynamic_text( ...$args ) {}
		public function has_any_popups() {
			return false;
		}
		public function is_plugin_page() {
			return false;
		}
		public function apply_inline_patch( ...$args ) {}
	}
}

if ( ! function_exists( 'cacheloop_fs' ) ) {

	/**
	 * Creates or returns the Freemius SDK singleton for CacheLoop.
	 *
	 * @return Freemius
	 */
	function cacheloop_fs() {
		global $cacheloop_fs;

		if ( ! isset( $cacheloop_fs ) ) {
			$sdk_path = __DIR__ . '/vendor/freemius/wordpress-sdk/start.php';

			// Safety check: verify the SDK file exists before requiring.
			if ( ! file_exists( $sdk_path ) ) {
				// Note: SDK file is missing. The plugin will run in limited mode.
				// Return a minimal stub so the rest of the plugin doesn't crash.
				$cacheloop_fs = new CacheLoop_Fs_Stub();
				return $cacheloop_fs;
			}

			require_once $sdk_path;

			try {
				$cacheloop_fs = fs_dynamic_init(
					array(
						'id'                  => '33999',
						'slug'                => 'cacheloop',
						'type'                => 'plugin',
						'public_key'          => 'pk_4ab8cbc31d7969793ee6956bb8bbd',
						// Detected at runtime rather than hardcoded: the Premium ZIP
						// built for Freemius (see .github/workflows/deploy.yml) is the
						// same cacheloop.php as the WordPress.org free build — only the
						// `*__premium_only.php` files differ between the two ZIPs, so
						// their presence is what actually distinguishes the codebases.
						// A hardcoded `false` here previously made Freemius treat the
						// Premium install as free code too, which hid the "Activate
						// License" trigger on the Account page (templates/account.php
						// gates it on `$is_premium`) — users saw only a "Sync License"
						// submit button that reloads the page and does nothing.
						'is_premium'          => file_exists( __DIR__ . '/includes/class-cl-db-optimizer__premium_only.php' ),
						'premium_suffix'      => 'Pro',
						'has_premium_version' => true,
						'has_addons'          => false,
						'has_paid_plans'      => true,
						'is_org_compliant'    => true,
						// A `menu` array without a `slug` key reads to the SDK as "no
						// custom menu exists at all," which hides every Freemius
						// auto-attached submenu item (Account, Contact, Affiliation,
						// Pricing, Add-ons), not just ones deliberately turned off —
						// verified via reflection against wordpress-sdk 2.13.2 (see
						// CLAUDE.md). 'cacheloop' matches CACHELOOP_Admin::PAGE_SLUG,
						// the slug add_menu_page() registers in cl_register_menu().
						'menu'                => array( 'slug' => 'cacheloop' ),
					)
				);
			} catch ( Exception $e ) {
				// SDK initialization failed — fall back to stub.
				$cacheloop_fs = new CacheLoop_Fs_Stub();
			}
		}

		return $cacheloop_fs;
	}

	// Initialize Freemius SDK.
	cacheloop_fs();

	// Signal that SDK has been initiated so other plugins/extensions can hook in.
	do_action( 'cacheloop_fs_loaded' );

	// Auto-enable Pro features when license is activated.
	cacheloop_fs()->add_action( 'license_activated', 'cacheloop_auto_enable_pro_features', 10, 2 );
	cacheloop_fs()->add_action( 'account_change', 'cacheloop_auto_enable_pro_features', 10, 2 );

	// The Premium ZIP ships the exact same "Plugin Name:" header as the free
	// WordPress.org build (see the `is_premium` detection above), so the two
	// are indistinguishable in the Plugins list. Append the same "Pro" suffix
	// already configured as `premium_suffix` in the Freemius init above.
	// Gated on can_use_premium_code() (valid license or active trial), not
	// is_premium() (which ZIP is installed) — matches the $is_pro convention
	// already used throughout admin/class-cl-admin.php and the settings
	// view, so a Premium install with a lapsed license doesn't say "Pro" in
	// the menu while every Pro feature toggle stays locked in the UI.
	add_filter( 'all_plugins', 'cacheloop_append_pro_suffix_to_plugin_name' );

	/**
	 * Appends " Pro" to the plugin's displayed name in wp-admin/plugins.php
	 * when the current install can use Pro features (valid license or
	 * active trial), so it's distinguishable from the free version.
	 *
	 * @param array $plugins Plugin data keyed by plugin basename.
	 * @return array
	 */
	function cacheloop_append_pro_suffix_to_plugin_name( $plugins ) {
		if ( ! cacheloop_fs()->can_use_premium_code() ) {
			return $plugins;
		}

		$basename = plugin_basename( __FILE__ );

		if ( isset( $plugins[ $basename ]['Name'] ) ) {
			$plugins[ $basename ]['Name'] .= ' Pro';
		}

		return $plugins;
	}

	/**
	 * Auto-enables all Pro feature toggles when a valid license is activated.
	 *
	 * @param int    $license_id Freemius license ID.
	 * @param object $license    License object.
	 * @return void
	 */
	function cacheloop_auto_enable_pro_features( $license_id, $license ) {
		$settings = get_option( 'cacheloop_settings', array() );

		// Enable Pro features by default when license is active.
		$pro_features = array(
			'minify',
			'delay_js',
			'db_optimizer',
			'preloader',
			'lazy_load',
			'disable_animations',
			'heartbeat',
		);

		foreach ( $pro_features as $feature ) {
			if ( ! isset( $settings[ $feature ] ) ) {
				$settings[ $feature ] = 1;
			}
		}

		update_option( 'cacheloop_settings', $settings );
	}

	// ---------------------------------------------------------------------------
	// Freemius Uninstall Hook
	// ---------------------------------------------------------------------------
	// Freemius requires uninstall logic to be registered via their hook,
	// NOT via a standalone uninstall.php file.
	cacheloop_fs()->add_action(
		'after_uninstall',
		function () {
			// 1. Remove plugin settings from the database.
			delete_option( 'cacheloop_settings' );

			$fs = cacheloop_get_filesystem();

			// 2. Remove .htaccess rules added by CacheLoop.
			$htaccess = ABSPATH . '.htaccess';
			if ( $fs && $fs->exists( $htaccess ) && $fs->is_writable( $htaccess ) ) {
				$content = $fs->get_contents( $htaccess );
				$content = preg_replace( '/# BEGIN CacheLoop.*?# END CacheLoop\n?/s', '', $content );
				$fs->put_contents( $htaccess, $content );
			}

			// 3. Delete all cached HTML files recursively.
			$cache_dir = WP_CONTENT_DIR . '/cache/cacheloop/';
			cacheloop_delete_dir_recursive( $cache_dir );

			// 4. Remove advanced-cache.php drop-in
			$dropin_dest = WP_CONTENT_DIR . '/advanced-cache.php';
			if ( $fs && $fs->exists( $dropin_dest ) ) {
				$fs->delete( $dropin_dest );
			}

			// 4b. Remove object-cache.php drop-in, but only if it's ours —
			// never touch a drop-in installed by another caching plugin.
			// Inlined (no require on a __premium_only class, which may not
			// even exist on disk in the free build) so uninstall is safe on
			// both tiers.
			$oc_dropin = WP_CONTENT_DIR . '/object-cache.php';
			if ( $fs && $fs->exists( $oc_dropin ) ) {
				$oc_head = $fs->get_contents( $oc_dropin );
				if ( is_string( $oc_head ) && false !== strpos( substr( $oc_head, 0, 4096 ), 'CacheLoop Object Cache Drop-in' ) ) {
					$fs->delete( $oc_dropin );
				}
			}

			// 5. Disable WP_CACHE in wp-config.php
			$wp_config_path = ABSPATH . 'wp-config.php';
			if ( $fs && $fs->exists( $wp_config_path ) && $fs->is_writable( $wp_config_path ) ) {
				$config_content = $fs->get_contents( $wp_config_path );
				$config_content = str_replace( "// Added by CacheLoop\ndefine('WP_CACHE', true);\n", '', $config_content );
				$config_content = preg_replace( "/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*true\s*\)\s*;\s*/i", '', $config_content );
				$fs->put_contents( $wp_config_path, $config_content );
			}

			// 6. Delete every generated .webp sibling before the tracking
			// table (the only record of which attachments have one) is
			// dropped — otherwise they're orphaned on disk forever with no
			// way to identify them afterwards.
			require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-image-optimize-db.php';
			foreach ( CACHELOOP_Image_Optimize_DB::find_tracked_attachment_ids() as $tracked_id ) {
				$source = get_attached_file( $tracked_id );
				if ( ! $source || ! $fs || ! $fs->exists( $source ) ) {
					continue;
				}

				$size_paths = array( $source );
				$metadata   = wp_get_attachment_metadata( $tracked_id );
				if ( ! empty( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
					$dir = trailingslashit( dirname( $source ) );
					foreach ( $metadata['sizes'] as $size ) {
						if ( ! empty( $size['file'] ) ) {
							$size_paths[] = $dir . $size['file'];
						}
					}
				}

				foreach ( array_unique( $size_paths ) as $path ) {
					$webp = preg_replace( '/\.(jpe?g|png)$/i', '.webp', $path );
					if ( $webp !== $path && $fs->exists( $webp ) ) {
						$fs->delete( $webp );
					}
				}
			}

			// 7. Drop the Image Optimization tracking table.
			CACHELOOP_Image_Optimize_DB::drop_table();
			delete_option( 'cacheloop_image_optimize_db_migrated' );
		}
	);
}

/**
 * Recursively deletes a directory and all its contents.
 * Used by the Freemius after_uninstall hook.
 *
 * @param  string $dir Absolute path to the directory to delete.
 * @return void
 */
function cacheloop_delete_dir_recursive( $dir ) {
	$fs = cacheloop_get_filesystem();
	if ( $fs && $fs->is_dir( $dir ) ) {
		$fs->delete( $dir, true );
	}
}

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
if ( ! defined( 'CACHELOOP_VERSION' ) ) {
	// Read the version straight from this file's own header instead of a
	// second hardcoded copy — the header comment was bumped release after
	// release while this constant silently stayed at '1.0.3', which broke
	// the ?ver= cache-busting on every enqueued script/style: browsers and
	// CDNs kept serving JS from before whatever version actually shipped,
	// since the query string URL never changed across updates.
	$cacheloop_version_data = get_file_data( __FILE__, array( 'Version' => 'Version' ) );
	define( 'CACHELOOP_VERSION', ! empty( $cacheloop_version_data['Version'] ) ? $cacheloop_version_data['Version'] : '1.0.5' );
}
define( 'CACHELOOP_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'CACHELOOP_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
define( 'CACHELOOP_CACHE_DIR', WP_CONTENT_DIR . '/cache/cacheloop/' );

/**
 * Ensures wp-content/cache/cacheloop/ is not directly web-reachable.
 *
 * The directory holds cache data (HTML, REST JSON, and the runtime-config.json
 * read pre-bootstrap by the drop-ins, which includes the object-cache password
 * when Object Cache Pro is configured) that must never be fetched directly
 * over HTTP. "Require all denied" blocks every direct request to the
 * directory on Apache/LiteSpeed (which also means no index.php placeholder
 * is needed — nothing in it is ever served). No executable PHP is written
 * here, only this static access-control file.
 *
 * @return void
 */
function cacheloop_protect_cache_dir() {
	if ( ! is_dir( CACHELOOP_CACHE_DIR ) ) {
		wp_mkdir_p( CACHELOOP_CACHE_DIR );
	}

	$htaccess = CACHELOOP_CACHE_DIR . '.htaccess';
	if ( ! file_exists( $htaccess ) ) {
		$rules = "# Added by CacheLoop — deny direct access to cache files.\n"
			. "<IfModule mod_authz_core.c>\n\tRequire all denied\n</IfModule>\n"
			. "<IfModule !mod_authz_core.c>\n\tOrder allow,deny\n\tDeny from all\n</IfModule>\n"
			. "Options -Indexes\n";
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- access-control file for the plugin's own wp-content/cache/ data directory, not executable code.
		@file_put_contents( $htaccess, $rules );
	}
}

// ---------------------------------------------------------------------------
// Activation Hook
// ---------------------------------------------------------------------------
/**
 * Runs on plugin activation.
 * Writes .htaccess rules and seeds default settings.
 */
function cacheloop_plugin_activate() {
	require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-browser-cache.php';
	CACHELOOP_Browser_Cache::write_htaccess_rules();
	cacheloop_protect_cache_dir();

	// Image Optimization tracking table — created on every activation
	// (dbDelta() is idempotent) so it's ready even before a Pro license is
	// entered, and re-created if it was somehow dropped.
	require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-image-optimize-db.php';
	CACHELOOP_Image_Optimize_DB::create_table();

	$fs = cacheloop_get_filesystem();
	if ( $fs ) {
		// Install advanced-cache.php drop-in
		$dropin_src  = CACHELOOP_PLUGIN_DIR . 'drop-ins/advanced-cache.php';
		$dropin_dest = WP_CONTENT_DIR . '/advanced-cache.php';
		if ( $fs->exists( $dropin_src ) ) {
			$fs->copy( $dropin_src, $dropin_dest, true );
		}

		// Enable WP_CACHE in wp-config.php
		$wp_config_path = ABSPATH . 'wp-config.php';
		if ( $fs->exists( $wp_config_path ) && $fs->is_writable( $wp_config_path ) ) {
			$config_content = $fs->get_contents( $wp_config_path );
			if ( false === strpos( $config_content, "'WP_CACHE'" ) && false === strpos( $config_content, '"WP_CACHE"' ) ) {
				// Insert it after the opening PHP tag
				$config_content = preg_replace( '/<\?php/', "<?php\n// Added by CacheLoop\ndefine('WP_CACHE', true);\n", $config_content, 1 );
				$fs->put_contents( $wp_config_path, $config_content );
			}
		}
	}

	// Seed default settings (all features on by default — "set and forget").
	if ( false === get_option( 'cacheloop_settings' ) ) {
		add_option(
			'cacheloop_settings',
			array(
				'page_cache'              => 1,
				'browser_cache'           => 1,
				'gzip'                    => 1,
				'lazy_load'               => 1,
				'cdn_enabled'             => 0,
				'cdn_domain'              => '',
				'bypass_cookies'          => '',
				'lcp_image_count'         => 1,
				'defer_css'               => 0,
				'minify'                  => 0,
				'delay_js'                => 0,
				'cache_ttl'               => 86400,
				'cache_device_split'      => 0,
				'cache_whitelist'         => '',
				'cache_whitelist_enabled' => 0,
				'cache_blacklist'         => '',
				'delay_js_exclusions'     => "jquery.min.js\njquery-migrate.min.js\nbootstrap.min.js\nmodernizr.custom.js\njs_composer_front.min.js\nwaypoints.min.js\nultimate.min.js\nvhparallax.min.js\nrbtools.min.js\nrs6.min.js\nslick.min.js",
				'disable_animations'      => 0,
				'lcp_preload_urls'        => '',
				'db_optimizer'            => 0,
				'preloader'               => 0,
				'heartbeat'               => 0,
				'disable_emojis'          => 0,
				'local_google_fonts'      => 0,
				'instant_page'            => 0,
				'preconnect_domains'      => '',
				'preload_lcp_images'      => 0,
				'object_cache_enabled'    => 0,
				'object_cache_driver'     => 'redis',
				'object_cache_host'       => '127.0.0.1',
				'object_cache_port'       => '6379',
				'object_cache_password'   => '',
				'object_cache_ttl'        => 3600,
				'image_optimize'          => 0,
				'image_optimize_on_upload' => 0,
				'image_optimize_quality'  => 82,
				'image_optimize_method'   => 'auto',
				'bloat_disable_xmlrpc'       => 0,
				'bloat_disable_dashicons'    => 0,
				'bloat_disable_jquery_migrate' => 0,
				'bloat_disable_block_css'    => 0,
				'bloat_disable_cart_fragment' => 0,
				'bloat_disable_woo_assets'   => 0,
				'bloat_disable_rss'          => 0,
				'bloat_disable_oembeds'      => 0,
				'bloat_disable_gutenberg'    => 0,
				'bloat_limit_revisions'      => '',
				'combine_js_footer'          => 0,
				'critical_css_enabled'       => 0,
			)
		);
	}

	// Trigger initial background scan for assets.
	if ( class_exists( 'CACHELOOP_Asset_Scanner' ) === false ) {
		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-asset-scanner.php';
	}
	CACHELOOP_Asset_Scanner::trigger_background_scan();

	// Write runtime config for the drop-in (pre-bootstrap TTL).
	$cache_dir = WP_CONTENT_DIR . '/cache/cacheloop/';
	if ( ! is_dir( $cache_dir ) ) {
		wp_mkdir_p( $cache_dir );
	}

	// Use hardcoded defaults (these variables don't exist in activation context).
	$device_split = 0;
	$cdn_enabled  = 0;
	$cdn_domain   = '';
	$home_url     = function_exists( 'home_url' ) ? home_url() : 'http://example.com';

	// Data file (not executable PHP) read by the drop-ins pre-bootstrap via
	// json_decode(). Kept in sync with CACHELOOP_Admin::cl_write_runtime_config(),
	// which regenerates it (with real settings) on every settings save.
	$config_path = $cache_dir . 'runtime-config.json';
	$config      = array(
		'version'        => CACHELOOP_VERSION,
		'ttl'            => DAY_IN_SECONDS,
		'device_split'   => $device_split,
		'cdn_enabled'    => $cdn_enabled,
		'cdn_domain'     => $cdn_domain,
		'home_url'       => $home_url,
		'bypass_cookies' => '',
	);
	// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- writes a non-executable data file to wp-content/cache/, not the plugin's own directory.
	file_put_contents( $config_path, wp_json_encode( $config ) );
}
register_activation_hook( __FILE__, 'cacheloop_plugin_activate' );

// ---------------------------------------------------------------------------
// Deactivation Hook
// ---------------------------------------------------------------------------
/**
 * Runs on plugin deactivation.
 * Removes .htaccess rules and flushes cache.
 */
function cacheloop_plugin_deactivate() {
	require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-browser-cache.php';
	CACHELOOP_Browser_Cache::remove_htaccess_rules();

	require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-page-cache.php';
	CACHELOOP_Page_Cache::flush_all();

	$fs = cacheloop_get_filesystem();
	if ( $fs ) {
		// Disable WP_CACHE in wp-config.php first, and only remove the
		// advanced-cache.php drop-in once that's confirmed to have taken
		// effect. If wp-config.php can't be edited (not writable, FTP-only
		// filesystem method, etc.), leave the drop-in in place too — deleting
		// only the drop-in while WP_CACHE stays true left WordPress trying to
		// include a now-missing file on every request ("failed to open
		// stream" + cascading "headers already sent" warnings).
		$wp_cache_cleared = true;
		$wp_config_path   = ABSPATH . 'wp-config.php';
		if ( $fs->exists( $wp_config_path ) ) {
			if ( $fs->is_writable( $wp_config_path ) ) {
				$config_content = $fs->get_contents( $wp_config_path );
				$new_content    = str_replace( "// Added by CacheLoop\ndefine('WP_CACHE', true);\n", '', $config_content );
				$new_content    = preg_replace( "/define\s*\(\s*['\"]WP_CACHE['\"]\s*,\s*true\s*\)\s*;\s*/i", '', $new_content );
				$wp_cache_cleared = ( $new_content === $config_content ) || $fs->put_contents( $wp_config_path, $new_content );
			} else {
				$wp_cache_cleared = false;
			}
		}

		// Remove advanced-cache.php drop-in.
		$dropin_dest = WP_CONTENT_DIR . '/advanced-cache.php';
		if ( $wp_cache_cleared && $fs->exists( $dropin_dest ) ) {
			$fs->delete( $dropin_dest );
		}

		// Remove object-cache.php drop-in, but only if it's ours.
		$oc_dropin = WP_CONTENT_DIR . '/object-cache.php';
		if ( $fs->exists( $oc_dropin ) ) {
			$oc_head = $fs->get_contents( $oc_dropin );
			if ( is_string( $oc_head ) && false !== strpos( substr( $oc_head, 0, 4096 ), 'CacheLoop Object Cache Drop-in' ) ) {
				$fs->delete( $oc_dropin );
			}
		}
	}

	// Remove scheduled cron events (Pro).
	wp_clear_scheduled_hook( 'cacheloop_db_optimizer_event' );
	wp_clear_scheduled_hook( 'cacheloop_preload_cron' );
	wp_clear_scheduled_hook( 'cacheloop_image_optimize_cron' );
	wp_clear_scheduled_hook( 'cacheloop_optimize_image' );
	wp_clear_scheduled_hook( 'cacheloop_image_revert_cron' );
	delete_option( 'cacheloop_image_optimize_queue' );
	delete_option( 'cacheloop_image_optimize_total' );
	delete_option( 'cacheloop_image_revert_queue' );
	delete_option( 'cacheloop_image_revert_total' );
	wp_clear_scheduled_hook( 'cacheloop_image_migrate_cron' );
	delete_option( 'cacheloop_image_migrate_queue' );
	delete_option( 'cacheloop_image_migrate_total' );

	// Remove runtime config file.
	$runtime_config = WP_CONTENT_DIR . '/cache/cacheloop/runtime-config.json';
	if ( $fs && $fs->exists( $runtime_config ) ) {
		$fs->delete( $runtime_config );
	}
}
register_deactivation_hook( __FILE__, 'cacheloop_plugin_deactivate' );

// ---------------------------------------------------------------------------
// Bootstrap
// ---------------------------------------------------------------------------
/**
 * Loads the CacheLoop core class and starts the plugin.
 */
function cacheloop_run_plugin() {
	// Invalidate cache when plugin version changes. Doesn't touch the
	// filesystem API, so it must not be gated behind $fs being available.
	$prev_version = get_option( 'cacheloop_version' );
	if ( $prev_version && $prev_version !== CACHELOOP_VERSION ) {
		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-page-cache.php';
		CACHELOOP_Page_Cache::flush_all();

		// Self-heal: sites already active before the dedicated Image
		// Optimization table existed need it created on upgrade too,
		// not just on a fresh activation.
		require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-image-optimize-db.php';
		CACHELOOP_Image_Optimize_DB::create_table();
	}
	update_option( 'cacheloop_version', CACHELOOP_VERSION );

	// Drop-in self-heal and wp-config.php editing are admin/maintenance
	// concerns only — running WP_Filesystem() on every front-end request
	// is needless overhead, and on hosts where it resolves to a non-direct
	// method with no stored credentials it has nothing useful to do here.
	if ( is_admin() ) {
		// Self-healing: sites already active before the cache directory got
		// .htaccess protection need it added on upgrade too, not just on a
		// fresh activation.
		cacheloop_protect_cache_dir();

		$fs = cacheloop_get_filesystem();
		if ( $fs ) {
			// Self-healing: Ensure advanced-cache.php is installed and matches
			// this version's copy. Comparing existence alone missed the more
			// common case — an in-place update (auto-update, ZIP override,
			// SVN/WordPress.org upgrade) that never deactivates/reactivates
			// the plugin leaves the OLD drop-in on disk indefinitely, since
			// it never goes missing. That silently keeps serving requests
			// with a prior version's drop-in logic (e.g. missing stats
			// tracking added in 1.0.16) with no visible error.
			$dropin_src  = CACHELOOP_PLUGIN_DIR . 'drop-ins/advanced-cache.php';
			$dropin_dest = WP_CONTENT_DIR . '/advanced-cache.php';
			if ( $fs->exists( $dropin_src ) ) {
				$dropin_stale = ! $fs->exists( $dropin_dest )
					|| md5( (string) $fs->get_contents( $dropin_src ) ) !== md5( (string) $fs->get_contents( $dropin_dest ) );
				if ( $dropin_stale ) {
					$fs->copy( $dropin_src, $dropin_dest, true );
				}
			}

			$wp_config_path = ABSPATH . 'wp-config.php';
			if ( $fs->exists( $wp_config_path ) && $fs->is_writable( $wp_config_path ) ) {
				$config_content = $fs->get_contents( $wp_config_path );
				if ( false === strpos( $config_content, "'WP_CACHE'" ) && false === strpos( $config_content, '"WP_CACHE"' ) ) {
					$config_content = preg_replace( '/<\?php/', "<?php\n// Added by CacheLoop\ndefine('WP_CACHE', true);\n", $config_content, 1 );
					$fs->put_contents( $wp_config_path, $config_content );
				}
			}
		}
	}

	require_once CACHELOOP_PLUGIN_DIR . 'includes/class-cl-core.php';
	CACHELOOP_Core::instance();
}
cacheloop_run_plugin();
