<?php
/**
 * Backreload — uninstall cleanup.
 *
 * Runs ONCE when the user clicks "Delete" on the plugin row in
 * Plugins → Installed Plugins (not on plain deactivate). Removes every
 * piece of state the plugin ever wrote to WordPress so the site is
 * returned to its pre-install condition.
 *
 * IMPORTANT: WordPress only loads this file when uninstalling. It does
 * NOT autoload the plugin's classes, so anything we reference here must
 * be plain PHP / WordPress core. No `use Backreload\...` allowed.
 *
 * The WAF .htaccess block is intentionally removed via inline regex
 * rather than calling our own remover, because the class loader isn't
 * running here.
 *
 * What we DON'T delete: the actual backup files in
 * `WP_CONTENT_DIR/backreload-backups/`. Those are USER DATA — the archives the
 * plugin created on the user's behalf. Wiping them on uninstall is too
 * destructive (a user who reinstalls later loses every historical
 * backup). The user can delete that folder by hand if they want a
 * truly clean uninstall.
 *
 * @package Backreload
 */

// Gate: WP_UNINSTALL_PLUGIN is the canonical "this is a real uninstall"
// signal. Without it, bail — never run destructive cleanup as a side
// effect of a misconfigured cron or a direct file request.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
	exit;
}

global $wpdb;

/**
 * Per-site cleanup. Each blog on a Multisite network has its own copy of
 * the plugin's custom tables (`{prefix}backreload_*`), its own options rows in
 * `{prefix}options`, its own cron events, and its own transients. The
 * earlier single-site uninstall would only clean up the blog that was
 * active when the user clicked Delete — leaving every other subsite's
 * plugin state orphaned forever (a real footgun on Multisite). This
 * helper does the per-blog work once, and the caller invokes it inside
 * a `switch_to_blog()` loop on Multisite (or just once on single-site).
 */
$backreload_cleanup_for_blog = static function () use ( $wpdb ) {
	// 1. Drop the custom DB tables. `IF EXISTS` keeps it idempotent —
	// a partial install where one table never got created still
	// uninstalls cleanly.
	$tables = [
		$wpdb->prefix . 'backreload_backups',
		$wpdb->prefix . 'backreload_logs',
		$wpdb->prefix . 'backreload_destinations',
	];
	foreach ( $tables as $t ) {
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin reads/writes its own custom tables; results are transient backup state and must not be cached.
		$wpdb->query( "DROP TABLE IF EXISTS `{$t}`" );
	}

	// 2. Delete every plugin-owned option. Built-in keys plus a
	// wildcard sweep for auto-generated keys (tick locks, per-backup
	// settings snapshots, etc.). Both lists are kept so a future audit
	// can see exactly what the plugin used.
	$options = [
		'backreload_settings',          // main settings (compression, exclusions, etc.)
		'backreload_db_version',        // schema version stamp
		'backreload_schedule',          // schedule definitions
		'backreload_notifiers',         // configured notification channels
		'backreload_maintenance',       // maintenance-mode state + customisation
		'backreload_auth_material_v2',  // legacy v2 frozen auth material
		'backreload_trust_forwarded_ip',// maintenance trust-proxy flag
		'backreload_schema_repaired_at',// one-time repair marker
	];
	foreach ( $options as $opt ) {
		delete_option( $opt );
	}

	// Catch anything the plugin added that the explicit list misses.
	// Both `backreload_*` and `backreload_*` prefixes are used in the codebase.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin reads/writes its own custom tables; results are transient backup state and must not be cached.
	$wpdb->query( $wpdb->prepare(
		"DELETE FROM `{$wpdb->options}` WHERE option_name LIKE %s OR option_name LIKE %s",
		$wpdb->esc_like( 'backreload_' ) . '%',
		$wpdb->esc_like( 'backreload_' ) . '%'
	) );

	// 3. Clear scheduled cron events for this blog. Cron events on
	// Multisite are per-site, so the sweep MUST run inside each
	// switch_to_blog() context.
	$cron_hooks = [
		'backreload_run_schedule',     // current per-schedule run hook
		'backreload_execute',          // tick-engine resume hook
		'backreload_daily_maintenance',// daily retention/maintenance hook
		'backreload_stale_sweep',      // hourly orphan-record sweep
		'backreload_migration_tick',   // migration push engine ticks
		'backreload_restore_async',    // async restore (used by migration auto-restore)
		'backreload_run',              // legacy single-event hook (pre-multi-schedule)
	];
	foreach ( $cron_hooks as $hook ) {
		wp_clear_scheduled_hook( $hook );
	}

	// 4. Delete plugin transients. Wildcards aren't supported by
	// `delete_transient`, so sweep the options table directly. Covers
	// both `_transient_<key>` and `_transient_timeout_<key>` rows.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin reads/writes its own custom tables; results are transient backup state and must not be cached.
	$wpdb->query( $wpdb->prepare(
		"DELETE FROM `{$wpdb->options}` WHERE option_name LIKE %s OR option_name LIKE %s",
		'_transient_backreload_%',
		'_transient_timeout_backreload_%'
	) );
};

if ( is_multisite() ) {
	// Multisite path: iterate every site on the network. `get_sites()` with
	// a high `number` returns all sites (default cap is 100; bump it so
	// large networks aren't truncated to the first page). The
	// switch_to_blog/restore_current_blog pair flips $wpdb->prefix so the
	// helper's table/option references resolve to the current blog.
	$backreload_sites = function_exists( 'get_sites' )
		? get_sites( [ 'number' => 0, 'fields' => 'ids' ] )
		: [];
	foreach ( $backreload_sites as $backreload_site_id ) {
		switch_to_blog( (int) $backreload_site_id );
		$backreload_cleanup_for_blog();
		restore_current_blog();
	}
} else {
	$backreload_cleanup_for_blog();
}

// ─── 5. Remove the WAF exemption from .htaccess ─────────────────────
// On servers that use .htaccess (Apache / LiteSpeed) the plugin writes
// a marker block to exempt chunked imports from mod_security 406s.
// Strip that block — leave the rest of the user's .htaccess alone.
$backreload_htaccess = ABSPATH . '.htaccess';
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- Read-only writability probe of a target path.
if ( is_file( $backreload_htaccess ) && is_writable( $backreload_htaccess ) ) {
	$backreload_contents = (string) @file_get_contents( $backreload_htaccess );
	if ( '' !== $backreload_contents && false !== strpos( $backreload_contents, '# BEGIN Backreload' ) ) {
		// Match our marker block and any whitespace immediately after it
		// so we don't leave an orphan blank line behind.
		$backreload_cleaned = preg_replace(
			'/# BEGIN Backreload.*?# END Backreload\s*/s',
			'',
			$backreload_contents
		);
		if ( null !== $backreload_cleaned && $backreload_cleaned !== $backreload_contents ) {
			// phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected, WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- The site's .htaccess can only exist at the web root (ABSPATH); it is not user data. On uninstall we strip only the plugin's own "# BEGIN/END Backreload" marker block and write the remainder back. wp_upload_dir() does not apply to a root server-config file.
			@file_put_contents( $backreload_htaccess, $backreload_cleaned );
		}
	}
}

// Done. Backup files in WP_CONTENT_DIR/backreload-backups/ are intentionally
// preserved so a reinstall can rediscover them.
