prefix . 'vigilante_activity_log', $wpdb->prefix . 'vigilante_login_attempts', $wpdb->prefix . 'vigilante_file_integrity', $wpdb->prefix . 'vigilante_2fa_codes', $wpdb->prefix . 'vigilante_2fa_trusted_devices', $wpdb->prefix . 'vigilante_2fa_notifications', $wpdb->prefix . 'vigilante_2fa_totp', ); foreach ( $tables as $table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "DROP TABLE IF EXISTS {$table}" ); } // Delete options $options_to_delete = array( 'vigilante_options', 'vigilante_db_version', 'vigilante_backup_timestamp', 'vigilante_last_integrity_scan', 'vigilante_last_integrity_results', 'vigilante_ignored_files', 'vigilante_dismissed_notices', 'vigilante_under_attack_mode', 'vigilante_active_preset', 'vigilante_firewall_blocks', 'vigilante_critical_files_baseline', 'vigilante_activated_time', 'vigilante_analyzer_last_scan', 'vigilante_analyzer_history', 'vigilante_legacy_backups_cleaned', 'vigilante_css_exclusion_migrated', 'vigilante_checksum_cache_flushed_290', 'vigilante_server_software', 'vigilante_server_files_version', 'vigilante_server_files_pending', 'vigilante_server_files_retry_after', // Safety copies taken before writing to the site's configuration files. // The wp-config.php one holds the database credentials and the // authentication salts, so leaving it behind would keep them readable in // the options table long after the plugin is gone. 'vigilante_htaccess_backup', 'vigilante_wpconfig_backup', 'vigilante_plugin_status_state', 'vigilante_plugin_status_last_check', 'vigilante_ignored_closed_plugins', ); foreach ( $options_to_delete as $option ) { delete_option( $option ); } // Per-backup records are named after their timestamp // (vigilante_backup_info_), so a fixed list cannot reach them. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'vigilante_backup_info_%'" ); // Delete all transients // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_vigilante_%' OR option_name LIKE '_transient_timeout_vigilante_%'" ); // Remove backup directory $backup_dir = WP_CONTENT_DIR . '/vigilante-backups/'; if ( is_dir( $backup_dir ) ) { vigilante_recursive_rmdir( $backup_dir ); } // Delete all user meta with vigilante_ prefix // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE 'vigilante\_%'" ); /* * The plugin is still loaded in the request that runs this file, so * whatever it does later, on shutdown or on a late hook, writes its data * back after the cleanup above has finished. Measured on 22 aug 2026: an * uninstall left 112 rows of plugin status transients and its last check * timestamp behind, all of them written after this file had run. So the * sweep is repeated at the very end of the request. */ add_action( 'shutdown', 'vigilante_uninstall_final_sweep', PHP_INT_MAX ); } /** * Second pass at the end of the request, for anything written after the first one * * Deliberately not a blunt "vigilante%" wildcard: other plugins live under that * name too, the network sync companion among them, and deleting their options * from here would be a fine way to break somebody else's site. * * @since 2.9.9 */ function vigilante_uninstall_final_sweep() { global $wpdb; // The scheduled events go too: they are rescheduled by the plugin that is // still loaded in this request, which is how vigilante_plugin_status_check // survived an uninstall until 2.9.9. $hooks = array( 'vigilante_daily_maintenance', 'vigilante_hourly_check', 'vigilante_hourly_checks', 'vigilante_file_integrity_scan', 'vigilante_cleanup_logs', 'vigilante_password_expiry_reminder', 'vigilante_analyzer_weekly_scan', 'vigilante_under_attack_post_scan', 'vigilante_plugin_status_check', 'vigilante_fi_postupdate_verify', ); foreach ( $hooks as $hook ) { wp_unschedule_hook( $hook ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_vigilante\_%' OR option_name LIKE '_transient_timeout_vigilante\_%' OR option_name LIKE 'vigilante_plugin_status\_%' OR option_name LIKE 'vigilante_backup_info\_%' OR option_name IN ( 'vigilante_options', 'vigilante_db_version', 'vigilante_ignored_closed_plugins', 'vigilante_ignored_files', 'vigilante_dismissed_notices', 'vigilante_under_attack_mode', 'vigilante_active_preset', 'vigilante_server_software', 'vigilante_server_files_version', 'vigilante_server_files_pending', 'vigilante_server_files_retry_after' )" ); } /** * Recursively remove directory * * @param string $dir Directory path. * @return bool */ function vigilante_recursive_rmdir( $dir ) { if ( ! is_dir( $dir ) ) { return false; } // Initialize WP_Filesystem global $wp_filesystem; if ( ! function_exists( 'WP_Filesystem' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } WP_Filesystem(); if ( ! $wp_filesystem ) { return false; } // Use WP_Filesystem delete with recursive flag return $wp_filesystem->delete( $dir, true ); } // Run uninstall vigilante_uninstall();