--apply` to recover. * * v2.14.0 multisite hardening: when network-uninstalled, iterate all sites * via switch_to_blog so every wp_N_wpdo_* table is dropped (previously only * the main site got cleaned, leaving orphans on every other site). * * @package WP_Data_Optimizer */ declare(strict_types=1); if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { exit; } global $wpdb; // ── Load shared cleanup logic + snapshot classes ───────────────────────── $bootstrap_files = array( __DIR__ . '/includes/class-tmdo-logger.php', __DIR__ . '/includes/class-tmdo-feature-flags.php', __DIR__ . '/includes/class-tmdo-installer.php', __DIR__ . '/includes/snapshots/class-tmdo-snapshot-manager.php', __DIR__ . '/includes/snapshots/class-tmdo-snapshot-writer.php', __DIR__ . '/includes/snapshots/class-tmdo-snapshot-reader.php', __DIR__ . '/includes/snapshots/class-tmdo-snapshot-pruner.php', ); $bootstrap_ok = true; foreach ( $bootstrap_files as $f ) { if ( ! is_file( $f ) ) { $bootstrap_ok = false; continue; } require_once $f; } // ── v2.2.0 M1.5: pre-uninstall snapshot (main-site context only) ───────── // Snapshot is taken once on the main site — it captures the catalog of all // wpdo_snapshots rows (which is per-site so per-site snapshots survive the // site-level table DROP that happens later). Best-effort. if ( $bootstrap_ok && class_exists( 'TMDO_Snapshot_Manager' ) ) { $result = TMDO_Snapshot_Manager::create( 'pre_uninstall', array(), array( 'notes' => 'Auto-snapshot taken at plugin uninstall time', 'retention_days' => 365, 'inline_threshold_bytes' => 0, ) ); if ( ! empty( $result['ok'] ) ) { $snapshot_id = $result['snapshot_id']; $dir = TMDO_Snapshot_Manager::backup_dir(); if ( is_dir( $dir ) ) { file_put_contents( trailingslashit( $dir ) . 'README-pre-uninstall.txt', "WPDO pre-uninstall snapshot: {$snapshot_id}\n" . 'Created: ' . gmdate( 'Y-m-d H:i:s' ) . " UTC\n\n" . "To restore after re-installing wp-data-optimizer:\n" . " 1. Re-install the plugin (it will re-create wp_wpdo_snapshots).\n" . " 2. Manually re-insert this snapshot's catalog row, or use wp wpdo snapshot list to confirm.\n" . " 3. Run: wp wpdo snapshot restore {$snapshot_id} --apply\n" ); } } } // ── v2.14.0: shared cleanup helper ─────────────────────────────────────── // Falls back to inline DROP loop if the helper class isn't loadable // (e.g. partially-broken install where includes/class-tmdo-installer.php // is missing). The inline path is the original v2.13.x logic verbatim. $has_helper = class_exists( 'TMDO_Installer' ) && method_exists( 'TMDO_Installer', 'drop_all_tables_for_current_blog' ); /** * Run the cleanup for the current blog (helper-aware). */ $run_cleanup = static function () use ( $has_helper ): void { global $wpdb; if ( $has_helper ) { TMDO_Installer::drop_all_tables_for_current_blog(); return; } // ── Fallback inline path (helper unavailable) ───────────────────────── $tables = array( $wpdb->prefix . 'wpdo_migrations', $wpdb->prefix . 'wpdo_errors', $wpdb->prefix . 'wpdo_benchmarks', $wpdb->prefix . 'wpdo_warm', $wpdb->prefix . 'wpdo_archive', $wpdb->prefix . 'wpdo_audit', $wpdb->prefix . 'wpdo_shadow_diffs', $wpdb->prefix . 'wpdo_site_metrics', $wpdb->prefix . 'wpdo_registry_meta', $wpdb->prefix . 'wpdo_uni_options', $wpdb->prefix . 'wpdo_wc_commissions', $wpdb->prefix . 'wpdo_snapshots', $wpdb->prefix . 'wpdo_migration_status', $wpdb->prefix . 'wpdo_user_points_ledger', $wpdb->prefix . 'wpdo_user_membership', $wpdb->prefix . 'wpdo_user_activity', $wpdb->prefix . 'wpdo_user_profile', $wpdb->prefix . 'wpdo_user_sso', $wpdb->prefix . 'wpdo_user_core_profile', $wpdb->prefix . 'wpdo_user_social', $wpdb->prefix . 'wpdo_user_commerce', $wpdb->prefix . 'wpdo_user_hp_user', $wpdb->prefix . 'wpdo_user_admin_prefs', $wpdb->prefix . 'wpdo_post_wp_core', $wpdb->prefix . 'wpdo_post_attachment', $wpdb->prefix . 'wpdo_post_wc_product', $wpdb->prefix . 'wpdo_post_hp_listing_core', $wpdb->prefix . 'wpdo_post_hp_request_core', $wpdb->prefix . 'wpdo_post_hp_vendor_core', $wpdb->prefix . 'wpdo_post_nav_menu_item', $wpdb->prefix . 'wpdo_hot_hp_listing', $wpdb->prefix . 'wpdo_term_hp_taxonomy', $wpdb->prefix . 'wpdo_comment_hp_review', $wpdb->prefix . 'wpdo_term_misc', $wpdb->prefix . 'wpdo_comment_misc', // v3.0.0: HivePress family integration tables. $wpdb->prefix . 'wpdo_hot_hp_request', $wpdb->prefix . 'wpdo_hot_hp_membership', $wpdb->prefix . 'wpdo_hot_hp_membership_plan', $wpdb->prefix . 'wpdo_hot_hp_booking', $wpdb->prefix . 'wpdo_comment_hp_message', $wpdb->prefix . 'wpdo_comment_hp_favorite', $wpdb->prefix . 'wpdo_comment_hp_offer', $wpdb->prefix . 'wpdo_term_hp_listing_tag', ); $hot_like = $wpdb->esc_like( $wpdb->prefix . 'wpdo_hot_' ) . '%'; $cold_like = $wpdb->esc_like( $wpdb->prefix . 'wpdo_cold_' ) . '%'; $entity_like = $wpdb->esc_like( $wpdb->prefix . 'wpdo_user_' ) . '%'; $post_ent_like = $wpdb->esc_like( $wpdb->prefix . 'wpdo_post_' ) . '%'; $term_ent_like = $wpdb->esc_like( $wpdb->prefix . 'wpdo_term_' ) . '%'; $comment_ent_like = $wpdb->esc_like( $wpdb->prefix . 'wpdo_comment_' ) . '%'; if ( class_exists( 'WP_SQLite_Driver' ) ) { $dynamic = $wpdb->get_col( $wpdb->prepare( "SELECT name FROM sqlite_master WHERE type='table' AND (name LIKE %s OR name LIKE %s OR name LIKE %s OR name LIKE %s OR name LIKE %s OR name LIKE %s)", $hot_like, $cold_like, $entity_like, $post_ent_like, $term_ent_like, $comment_ent_like ) ); } else { $dynamic = $wpdb->get_col( $wpdb->prepare( 'SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND (TABLE_NAME LIKE %s OR TABLE_NAME LIKE %s OR TABLE_NAME LIKE %s OR TABLE_NAME LIKE %s OR TABLE_NAME LIKE %s OR TABLE_NAME LIKE %s)', $hot_like, $cold_like, $entity_like, $post_ent_like, $term_ent_like, $comment_ent_like ) ); } $tables = array_unique( array_merge( $tables, $dynamic ?: array() ) ); $prefix = $wpdb->prefix . 'wpdo_'; foreach ( $tables as $table ) { if ( ! preg_match( '/^[a-zA-Z0-9_]+$/', $table ) || strpos( $table, $prefix ) !== 0 ) { continue; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" ); } delete_option( 'wpdo_db_version' ); delete_option( 'wpdo_features' ); delete_option( 'wpdo_features_shadow' ); delete_option( 'wpdo_hpct_imported' ); delete_option( 'wpdo_hook_bus_enabled' ); delete_option( 'wpdo_v2_features_backup' ); delete_option( 'wpdo_v2_upgrade_status' ); delete_option( 'wpdo_v2_upgrade_error' ); delete_option( 'wpdo_v2_upgraded_at' ); delete_option( 'wpdo_health_alert' ); delete_option( 'wpdo_rl_stats' ); delete_option( 'wpdo_setup_wizard_completed' ); delete_option( 'wpdo_first_run_at' ); wp_clear_scheduled_hook( 'wpdo_warm_cleanup' ); wp_clear_scheduled_hook( 'wpdo_archive_sweep' ); wp_clear_scheduled_hook( 'wpdo_errors_gc' ); wp_clear_scheduled_hook( 'wpdo_health_snapshot_monthly' ); wp_clear_scheduled_hook( 'wpdo_daily_health_check' ); wp_clear_scheduled_hook( 'wpdo_snapshot_prune_daily' ); }; // ── Run cleanup ────────────────────────────────────────────────────────── // v2.14.0: when network-uninstalling on multisite, iterate every site so each // wp_N_wpdo_* table set is dropped. Pre-v2.14.0 only the main site got cleaned. if ( is_multisite() ) { $is_network_active = function_exists( 'is_plugin_active_for_network' ) && is_plugin_active_for_network( plugin_basename( __FILE__ ) ); if ( $is_network_active ) { // Iterate all sites in batches. $offset = 0; $batch = 100; do { $site_ids = get_sites( array( 'number' => $batch, 'offset' => $offset, 'fields' => 'ids', ) ); foreach ( $site_ids as $wpdo_blog_id ) { switch_to_blog( (int) $wpdo_blog_id ); try { $run_cleanup(); } finally { restore_current_blog(); } } $offset += $batch; $batch_size = count( $site_ids ); } while ( $batch_size === $batch ); } else { // Per-site activation: clean only the current site. $run_cleanup(); } } else { // Single-site install: run once. $run_cleanup(); }