fix(2meet-data-optimizer): refuse uninstall while data lives only in flat tables
- uninstall.php checks every site first and wp_die()s (keeping tables, options and plugin files) on aeav_only modes, misc rows, or zone modules past cutover; a failed bootstrap also refuses - Multisite now checks and cleans every site: network activation is always false at uninstall, so only the main site was being cleaned before - Drop the inline fallback DROP loop, unreachable now that a missing helper refuses instead Verify: UninstallGuardTest (8 tests incl. running uninstall.php single and multisite); unit 602 OK, integration 429 OK PR: PR-008
This commit is contained in:
@@ -297,6 +297,58 @@ class TMDO_Installer {
|
|||||||
update_option( self::MISC_BUCKET_DEFAULT_FLAG, '1', true );
|
update_option( self::MISC_BUCKET_DEFAULT_FLAG, '1', true );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reasons this site's wpdo_* tables cannot be dropped yet; empty means safe.
|
||||||
|
*
|
||||||
|
* Reads the site's own options rather than Mode_Manager / Feature_Flags,
|
||||||
|
* whose per-request caches do not follow switch_to_blog() during uninstall.
|
||||||
|
*
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public static function uninstall_blockers(): array {
|
||||||
|
global $wpdb;
|
||||||
|
$blockers = array();
|
||||||
|
|
||||||
|
$modes = get_option( 'wpdo_bridge_modes', array() );
|
||||||
|
foreach ( is_array( $modes ) ? $modes : array() as $entity => $mode ) {
|
||||||
|
if ( 'aeav_only' === $mode ) {
|
||||||
|
$blockers[] = sprintf( '%1$s 的 bridge 模式是 aeav_only:資料只存在 wpdo_%1$s_* 扁平表。', $entity );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( array( 'wpdo_term_misc', 'wpdo_comment_misc' ) as $name ) {
|
||||||
|
if ( self::table_has_rows( $wpdb->prefix . $name ) ) {
|
||||||
|
$blockers[] = sprintf( '%s 有資料:這些 term / comment meta 只存在 misc 表。', $name );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$features = get_option( 'wpdo_features', array() );
|
||||||
|
foreach ( TMDO_Feature_Flags::ZONE_MODULES as $module ) {
|
||||||
|
$state = is_array( $features ) ? ( $features[ $module ] ?? 'idle' ) : 'idle';
|
||||||
|
if ( in_array( $state, array( 'cleanup', 'complete' ), true ) ) {
|
||||||
|
$blockers[] = sprintf( '區域模組 %1$s 處於 %2$s:讀取已改走 wpdo_* 表。', $module, $state );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $blockers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a table exists and holds at least one row.
|
||||||
|
*
|
||||||
|
* @param string $table Full table name.
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private static function table_has_rows( string $table ): bool {
|
||||||
|
global $wpdb;
|
||||||
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||||
|
if ( $table !== $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return null !== $wpdb->get_var( "SELECT 1 FROM `{$table}` LIMIT 1" );
|
||||||
|
// phpcs:enable
|
||||||
|
}
|
||||||
|
|
||||||
// ── v2.0.0 entity + audit + shadow_diffs schema (PR-2) ─────────────────
|
// ── v2.0.0 entity + audit + shadow_diffs schema (PR-2) ─────────────────
|
||||||
// These tables are installed only when v2.0.0 upgrade runs (PR-7).
|
// These tables are installed only when v2.0.0 upgrade runs (PR-7).
|
||||||
// install_v2_tables() is idempotent; safe to call multiple times.
|
// install_v2_tables() is idempotent; safe to call multiple times.
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration test: uninstall refuses to drop tables while data lives only in them.
|
||||||
|
*
|
||||||
|
* Covers TMDO_Installer::uninstall_blockers() and, end to end, that uninstall.php
|
||||||
|
* stops (wp_die) before touching any table or option.
|
||||||
|
*/
|
||||||
|
class UninstallGuardTest extends TestCase {
|
||||||
|
|
||||||
|
private const TERM_MISC = 'wp_itest_wpdo_term_misc';
|
||||||
|
|
||||||
|
public static function setUpBeforeClass(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query(
|
||||||
|
'CREATE TABLE IF NOT EXISTS `' . self::TERM_MISC . '` (
|
||||||
|
term_id bigint(20) unsigned NOT NULL,
|
||||||
|
meta_key varchar(191) NOT NULL,
|
||||||
|
meta_value longtext,
|
||||||
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (term_id, meta_key)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setUp(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->prefix = 'wp_itest_';
|
||||||
|
$wpdb->options = 'wp_itest_options';
|
||||||
|
$wpdb->query( 'TRUNCATE TABLE `' . self::TERM_MISC . '`' );
|
||||||
|
delete_option( 'wpdo_bridge_modes' );
|
||||||
|
delete_option( 'wpdo_features' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_clean_site_has_no_blockers(): void {
|
||||||
|
$this->assertSame( array(), TMDO_Installer::uninstall_blockers() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_aeav_only_mode_blocks(): void {
|
||||||
|
update_option( 'wpdo_bridge_modes', array( 'post' => 'aeav_only', 'user' => 'dual_write' ) );
|
||||||
|
|
||||||
|
$blockers = TMDO_Installer::uninstall_blockers();
|
||||||
|
|
||||||
|
$this->assertCount( 1, $blockers );
|
||||||
|
$this->assertStringContainsString( 'post', $blockers[0] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_modes_that_still_write_native_do_not_block(): void {
|
||||||
|
update_option( 'wpdo_bridge_modes', array( 'user' => 'dual_write', 'term' => 'shadow_read', 'comment' => 'disabled' ) );
|
||||||
|
|
||||||
|
$this->assertSame( array(), TMDO_Installer::uninstall_blockers() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_misc_rows_block(): void {
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query( 'INSERT INTO `' . self::TERM_MISC . "` (term_id, meta_key, meta_value) VALUES (1, 'k', 'v')" );
|
||||||
|
|
||||||
|
$blockers = TMDO_Installer::uninstall_blockers();
|
||||||
|
|
||||||
|
$this->assertCount( 1, $blockers );
|
||||||
|
$this->assertStringContainsString( 'wpdo_term_misc', $blockers[0] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_zone_module_past_cutover_blocks(): void {
|
||||||
|
update_option( 'wpdo_features', array( 'hot_hp_listing' => 'complete', 'cold_hp_vendor' => 'cleanup', 'warm' => 'cutover' ) );
|
||||||
|
|
||||||
|
$blockers = TMDO_Installer::uninstall_blockers();
|
||||||
|
|
||||||
|
$this->assertCount( 2, $blockers );
|
||||||
|
$this->assertStringContainsString( 'hot_hp_listing', implode( ' ', $blockers ) );
|
||||||
|
$this->assertStringContainsString( 'cold_hp_vendor', implode( ' ', $blockers ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_hpct_module_complete_does_not_block(): void {
|
||||||
|
// HPCT modules live in hpct_* tables, which uninstall never drops.
|
||||||
|
update_option( 'wpdo_features', array( 'reviews' => 'complete' ) );
|
||||||
|
|
||||||
|
$this->assertSame( array(), TMDO_Installer::uninstall_blockers() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_uninstall_script_refuses_and_keeps_data(): void {
|
||||||
|
global $wpdb;
|
||||||
|
update_option( 'wpdo_bridge_modes', array( 'post' => 'aeav_only' ) );
|
||||||
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
|
define( 'WP_UNINSTALL_PLUGIN', '2meet-data-optimizer/2meet-data-optimizer.php' );
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require TMDO_PATH . 'uninstall.php';
|
||||||
|
$this->fail( 'uninstall.php should have stopped with wp_die()' );
|
||||||
|
} catch ( RuntimeException $e ) {
|
||||||
|
$this->assertStringContainsString( 'aeav_only', $e->getMessage() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame( array( 'post' => 'aeav_only' ), get_option( 'wpdo_bridge_modes' ) );
|
||||||
|
$this->assertSame( self::TERM_MISC, $wpdb->get_var( "SHOW TABLES LIKE '" . self::TERM_MISC . "'" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_uninstall_script_checks_every_site_on_multisite(): void {
|
||||||
|
update_option( 'wpdo_bridge_modes', array( 'user' => 'aeav_only' ) );
|
||||||
|
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||||
|
define( 'WP_UNINSTALL_PLUGIN', '2meet-data-optimizer/2meet-data-optimizer.php' );
|
||||||
|
}
|
||||||
|
$GLOBALS['_wp_is_multisite'] = true;
|
||||||
|
$GLOBALS['_wp_sites'] = array( 1, 3 );
|
||||||
|
|
||||||
|
try {
|
||||||
|
require TMDO_PATH . 'uninstall.php';
|
||||||
|
$this->fail( 'uninstall.php should have stopped with wp_die()' );
|
||||||
|
} catch ( RuntimeException $e ) {
|
||||||
|
$this->assertStringContainsString( '站台 #1', $e->getMessage() );
|
||||||
|
$this->assertStringContainsString( '站台 #3', $e->getMessage() );
|
||||||
|
} finally {
|
||||||
|
unset( $GLOBALS['_wp_is_multisite'], $GLOBALS['_wp_sites'] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-171
@@ -5,14 +5,18 @@
|
|||||||
* Runs only when the plugin is deleted via the WordPress admin.
|
* Runs only when the plugin is deleted via the WordPress admin.
|
||||||
* Removes all WPDO tables and options. Does NOT remove HPCT tables.
|
* Removes all WPDO tables and options. Does NOT remove HPCT tables.
|
||||||
*
|
*
|
||||||
|
* Refuses (wp_die, before any snapshot or DROP) while any site still holds data
|
||||||
|
* that exists only in wpdo_* tables — see TMDO_Installer::uninstall_blockers().
|
||||||
|
* wp_die() also stops WordPress from deleting the plugin files.
|
||||||
|
*
|
||||||
* v2.2.0 hardening: take a `pre_uninstall` snapshot BEFORE dropping tables.
|
* v2.2.0 hardening: take a `pre_uninstall` snapshot BEFORE dropping tables.
|
||||||
* The snapshot file under wp-content/uploads/wpdo-backups/ survives plugin
|
* The snapshot file under wp-content/uploads/wpdo-backups/ survives plugin
|
||||||
* deletion (uploads dir is never auto-purged), so the user can re-install +
|
* deletion (uploads dir is never auto-purged), so the user can re-install +
|
||||||
* `wp wpdo snapshot restore <id> --apply` to recover.
|
* `wp wpdo snapshot restore <id> --apply` to recover.
|
||||||
*
|
*
|
||||||
* v2.14.0 multisite hardening: when network-uninstalled, iterate all sites
|
* Multisite: every site is checked and cleaned. The plugin may have been active
|
||||||
* via switch_to_blog so every wp_N_wpdo_* table is dropped (previously only
|
* per site, and it is always inactive by the time it can be deleted, so network
|
||||||
* the main site got cleaned, leaving orphans on every other site).
|
* activation cannot tell which sites hold wpdo_* tables.
|
||||||
*
|
*
|
||||||
* @package WP_Data_Optimizer
|
* @package WP_Data_Optimizer
|
||||||
*/
|
*/
|
||||||
@@ -44,11 +48,61 @@ foreach ( $bootstrap_files as $f ) {
|
|||||||
require_once $f;
|
require_once $f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sites to check and clean (0 = single site, no blog switching) ────────
|
||||||
|
$wpdo_site_ids = is_multisite()
|
||||||
|
? array_map(
|
||||||
|
'intval',
|
||||||
|
get_sites(
|
||||||
|
array(
|
||||||
|
'fields' => 'ids',
|
||||||
|
'number' => 0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: array( 0 );
|
||||||
|
|
||||||
|
$wpdo_for_each_site = static function ( callable $callback ) use ( $wpdo_site_ids ): void {
|
||||||
|
foreach ( $wpdo_site_ids as $wpdo_blog_id ) {
|
||||||
|
if ( $wpdo_blog_id ) {
|
||||||
|
switch_to_blog( $wpdo_blog_id );
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$callback( $wpdo_blog_id );
|
||||||
|
} finally {
|
||||||
|
if ( $wpdo_blog_id ) {
|
||||||
|
restore_current_blog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Refuse while any site holds data that exists only in wpdo_* tables ───
|
||||||
|
if ( ! $bootstrap_ok || ! method_exists( 'TMDO_Installer', 'uninstall_blockers' ) ) {
|
||||||
|
wp_die( esc_html( '2meet Data Optimizer:無法載入資料安全檢查,為避免資料遺失,已停止解除安裝。' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpdo_blocked = array();
|
||||||
|
$wpdo_for_each_site(
|
||||||
|
static function ( int $blog_id ) use ( &$wpdo_blocked ): void {
|
||||||
|
foreach ( TMDO_Installer::uninstall_blockers() as $reason ) {
|
||||||
|
$wpdo_blocked[] = ( $blog_id ? "站台 #{$blog_id}:" : '' ) . $reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $wpdo_blocked ) {
|
||||||
|
wp_die(
|
||||||
|
'<p>' . esc_html( '2meet Data Optimizer 已停止解除安裝:以下資料只存在 wpdo_* 表,刪除外掛會一併刪除這些表。' ) . '</p>'
|
||||||
|
. '<ul><li>' . implode( '</li><li>', array_map( 'esc_html', $wpdo_blocked ) ) . '</li></ul>'
|
||||||
|
. '<p>' . esc_html( '目前沒有把這些資料寫回 WordPress 原生表的自動工具。若只想移除外掛程式,可手動刪除外掛目錄,資料表會保留。' ) . '</p>'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── v2.2.0 M1.5: pre-uninstall snapshot (main-site context only) ─────────
|
// ── 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
|
// 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
|
// wpdo_snapshots rows (which is per-site so per-site snapshots survive the
|
||||||
// site-level table DROP that happens later). Best-effort.
|
// site-level table DROP that happens later). Best-effort.
|
||||||
if ( $bootstrap_ok && class_exists( 'TMDO_Snapshot_Manager' ) ) {
|
if ( class_exists( 'TMDO_Snapshot_Manager' ) ) {
|
||||||
$result = TMDO_Snapshot_Manager::create(
|
$result = TMDO_Snapshot_Manager::create(
|
||||||
'pre_uninstall',
|
'pre_uninstall',
|
||||||
array(),
|
array(),
|
||||||
@@ -75,172 +129,9 @@ if ( $bootstrap_ok && class_exists( 'TMDO_Snapshot_Manager' ) ) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── v2.14.0: shared cleanup helper ───────────────────────────────────────
|
// ── Drop wpdo_* tables and options on every site ─────────────────────────
|
||||||
// Falls back to inline DROP loop if the helper class isn't loadable
|
$wpdo_for_each_site(
|
||||||
// (e.g. partially-broken install where includes/class-tmdo-installer.php
|
static function (): void {
|
||||||
// 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();
|
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();
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user