From 15096f064f6e85cc59014ca9ea3175f78a2292f4 Mon Sep 17 00:00:00 2001 From: wpdev Date: Mon, 28 Sep 2026 18:25:01 +0800 Subject: [PATCH] fix(2meet-data-optimizer): deactivate and uninstall clear every scheduled event and tmdo_* option - One CRON_HOOKS list for deactivate() and uninstall; wp_unschedule_hook() also removes events scheduled with args (tmdo_drift_resync, backfill batches). - Fix the wrong wpdo_user_stress_test_batch name; add migration tick, entity backfill, auto-promote and all stress-test batch hooks. - Uninstall sweeps tmdo_* options and _transient_(timeout_)tmdo_* rows too. - Verify: InstallerCleanupTest (3 new), unit 711, integration 611, phpcs 0 - PR: PR-138 (manual F-4, F-5) --- includes/class-tmdo-installer.php | 100 ++++++++++----------- tests/bootstrap.php | 1 + tests/integration/DriftCheckTest.php | 3 +- tests/integration/InstallerCleanupTest.php | 43 +++++++++ tests/integration/bootstrap.php | 1 + 5 files changed, 96 insertions(+), 52 deletions(-) diff --git a/includes/class-tmdo-installer.php b/includes/class-tmdo-installer.php index 4485e4c..2b7df7d 100644 --- a/includes/class-tmdo-installer.php +++ b/includes/class-tmdo-installer.php @@ -780,31 +780,46 @@ class TMDO_Installer { return $status; } + /** + * Every cron hook the plugin schedules. Cleared with wp_unschedule_hook(), + * which also removes events scheduled with args (tmdo_drift_resync( 'post' ), + * backfill batches) that wp_clear_scheduled_hook( $hook ) leaves behind + * (2.4.2 PR-138). + */ + private const CRON_HOOKS = array( + 'wpdo_warm_cleanup', + 'wpdo_archive_sweep', + 'wpdo_errors_gc', + 'wpdo_flush_views', + 'wpdo_remove_uae_plugin_dir', + 'wpdo_daily_health_check', + 'wpdo_snapshot_prune_daily', + 'wpdo_fsm_automator_run', + 'wpdo_collect_site_metrics', + 'wpdo_health_snapshot_monthly', + 'wpdo_post_shadow_verify', + 'wpdo_term_comment_shadow_verify', + 'tmdo_drift_resync', + 'tmdo_ensure_group_indexes', + 'wpdo_entity_backfill_batch', + 'wpdo_migration_tick', + 'wpdo_auto_promote_check', + 'wpdo_post_stress_test_batch', + 'wpdo_stress_test_batch', + 'wpdo_term_stress_test_batch', + 'wpdo_comment_stress_test_batch', + ); + /** * Called on plugin deactivation. Non-destructive. */ public static function deactivate(): void { - wp_clear_scheduled_hook( 'wpdo_warm_cleanup' ); - wp_clear_scheduled_hook( 'wpdo_archive_sweep' ); - // v2.14.0: clear all plugin cron hooks (was incomplete pre-v2.14.0). - wp_clear_scheduled_hook( 'wpdo_errors_gc' ); - wp_clear_scheduled_hook( 'wpdo_flush_views' ); - wp_clear_scheduled_hook( 'wpdo_remove_uae_plugin_dir' ); - wp_clear_scheduled_hook( 'wpdo_daily_health_check' ); - wp_clear_scheduled_hook( 'wpdo_snapshot_prune_daily' ); - wp_clear_scheduled_hook( 'wpdo_fsm_automator_run' ); - wp_clear_scheduled_hook( 'wpdo_collect_site_metrics' ); - wp_clear_scheduled_hook( 'wpdo_health_snapshot_monthly' ); - wp_clear_scheduled_hook( 'wpdo_post_shadow_verify' ); - wp_clear_scheduled_hook( 'wpdo_term_comment_shadow_verify' ); - wp_clear_scheduled_hook( 'tmdo_drift_resync' ); - wp_clear_scheduled_hook( 'tmdo_ensure_group_indexes' ); - wp_clear_scheduled_hook( 'wpdo_post_stress_test_batch' ); - wp_clear_scheduled_hook( 'wpdo_user_stress_test_batch' ); - wp_clear_scheduled_hook( 'wpdo_term_stress_test_batch' ); - wp_clear_scheduled_hook( 'wpdo_comment_stress_test_batch' ); + foreach ( self::CRON_HOOKS as $hook ) { + wp_unschedule_hook( $hook ); + } } + // ── v2.14.0: Multisite-aware site cleanup ───────────────────────────────── /** @@ -976,42 +991,25 @@ class TMDO_Installer { // Sweep any remaining wpdo_* options (stragglers introduced post-v2.14.0 // or by 3rd-party hooks). Validates name pattern before delete to avoid // accidental option removal. - $residual = $wpdb->get_col( - $wpdb->prepare( - "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", - $wpdb->esc_like( 'wpdo_' ) . '%' - ) - ); - foreach ( (array) $residual as $opt ) { - if ( preg_match( '/^wpdo_[a-zA-Z0-9_]+$/', $opt ) && delete_option( $opt ) ) { - ++$options_deleted; + // 1.4.2: tmdo_* options and their transients too (tmdo_schema_deferred, + // tmdo_entity_indexes, tmdo_cov_* …) — a reinstall saw stale ones. + foreach ( array( 'wpdo_', 'tmdo_', '_transient_tmdo_', '_transient_timeout_tmdo_' ) as $prefix ) { + $residual = $wpdb->get_col( + $wpdb->prepare( + "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", + $wpdb->esc_like( $prefix ) . '%' + ) + ); + foreach ( (array) $residual as $opt ) { + if ( preg_match( '/^(_transient_(timeout_)?)?(wpdo|tmdo)_[a-zA-Z0-9_]+$/', $opt ) && delete_option( $opt ) ) { + ++$options_deleted; + } } } // ── 5) Clear cron events ────────────────────────────────────────────── - $crons = array( - 'wpdo_warm_cleanup', - 'wpdo_archive_sweep', - 'wpdo_errors_gc', - 'wpdo_flush_views', - 'wpdo_remove_uae_plugin_dir', - 'wpdo_daily_health_check', - 'wpdo_snapshot_prune_daily', - 'wpdo_fsm_automator_run', - 'wpdo_collect_site_metrics', - 'wpdo_health_snapshot_monthly', - 'wpdo_post_shadow_verify', - 'wpdo_term_comment_shadow_verify', - 'tmdo_drift_resync', - 'tmdo_ensure_group_indexes', - 'wpdo_post_stress_test_batch', - 'wpdo_user_stress_test_batch', - 'wpdo_term_stress_test_batch', - 'wpdo_comment_stress_test_batch', - ); - foreach ( $crons as $hook ) { - if ( false !== wp_next_scheduled( $hook ) ) { - wp_clear_scheduled_hook( $hook ); + foreach ( self::CRON_HOOKS as $hook ) { + if ( wp_unschedule_hook( $hook ) ) { ++$crons_cleared; } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9e64583..447b53f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -188,6 +188,7 @@ if ( ! function_exists( 'wp_schedule_event' ) ) { if ( ! function_exists( 'wp_schedule_single_event' ) ) { function wp_schedule_single_event( int $ts, string $hook ): bool { return true; } } +if ( ! function_exists( 'wp_unschedule_hook' ) ) { function wp_unschedule_hook( string $hook ): int|false { $GLOBALS['_wp_unscheduled'][] = $hook; return 0; } } if ( ! function_exists( 'wp_clear_scheduled_hook' ) ) { function wp_clear_scheduled_hook( string $hook ): int|false { return 0; } } diff --git a/tests/integration/DriftCheckTest.php b/tests/integration/DriftCheckTest.php index c646b4c..1f416d1 100644 --- a/tests/integration/DriftCheckTest.php +++ b/tests/integration/DriftCheckTest.php @@ -303,7 +303,8 @@ class DriftCheckTest extends TestCase { } // Literal: uninstall.php does not load TMDO_Drift_Check. $installer = (string) file_get_contents( WPDO_PLUGIN_DIR . 'includes/class-tmdo-installer.php' ); - $this->assertSame( 2, substr_count( $installer, "'" . TMDO_Drift_Check::RESYNC_HOOK . "'" ), 'cleared on deactivate and uninstall' ); + // One CRON_HOOKS list serves deactivate and uninstall (InstallerCleanupTest covers both). + $this->assertSame( 1, substr_count( $installer, "'" . TMDO_Drift_Check::RESYNC_HOOK . "'" ), 'listed in CRON_HOOKS' ); } public function test_the_daily_tick_runs_on_the_daily_health_check_hook(): void { diff --git a/tests/integration/InstallerCleanupTest.php b/tests/integration/InstallerCleanupTest.php index b95224b..a057cd8 100644 --- a/tests/integration/InstallerCleanupTest.php +++ b/tests/integration/InstallerCleanupTest.php @@ -68,6 +68,49 @@ class InstallerCleanupTest extends TestCase { $GLOBALS['_wp_options'] = array(); } + /** + * Events scheduled with args (tmdo_drift_resync( 'post' ), backfill + * batches) are not cleared by wp_clear_scheduled_hook( $hook ); the user + * stress hook was misnamed and three hooks were missing (2.4.2 PR-138, F-4). + */ + private const ARG_AND_MISSED_HOOKS = array( 'tmdo_drift_resync', 'wpdo_entity_backfill_batch', 'wpdo_stress_test_batch', 'wpdo_migration_tick', 'wpdo_auto_promote_check' ); + + public function test_deactivate_unschedules_every_hook_whatever_its_args(): void { + $GLOBALS['_wp_unscheduled'] = array(); + + WPDO_Installer::deactivate(); + + foreach ( self::ARG_AND_MISSED_HOOKS as $hook ) { + $this->assertContains( $hook, $GLOBALS['_wp_unscheduled'], $hook ); + } + } + + public function test_uninstall_unschedules_every_hook_whatever_its_args(): void { + $GLOBALS['_wp_unscheduled'] = array(); + + WPDO_Installer::drop_all_tables_for_current_blog(); + + foreach ( self::ARG_AND_MISSED_HOOKS as $hook ) { + $this->assertContains( $hook, $GLOBALS['_wp_unscheduled'], $hook ); + } + } + + /** tmdo_* options and transients were left behind; a reinstall then saw stale deferrals and indexes (F-5). */ + public function test_uninstall_removes_tmdo_options_and_transients(): void { + global $wpdb; + foreach ( array( 'tmdo_schema_deferred', 'tmdo_entity_indexes', '_transient_tmdo_cov_x', '_transient_timeout_tmdo_cov_x', 'hivepress_settings' ) as $name ) { + $wpdb->insert( $wpdb->options, array( 'option_name' => $name, 'option_value' => '1' ) ); + $GLOBALS['_wp_options'][ $name ] = '1'; + } + + WPDO_Installer::drop_all_tables_for_current_blog(); + + foreach ( array( 'tmdo_schema_deferred', 'tmdo_entity_indexes', '_transient_tmdo_cov_x', '_transient_timeout_tmdo_cov_x' ) as $name ) { + $this->assertArrayNotHasKey( $name, $GLOBALS['_wp_options'], $name ); + } + $this->assertArrayHasKey( 'hivepress_settings', $GLOBALS['_wp_options'], 'other plugins untouched' ); + } + public function test_drops_static_tables(): void { global $wpdb; diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index 76fcb96..e7d2d4d 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -315,6 +315,7 @@ if ( ! function_exists( 'get_metadata' ) ) { if ( ! function_exists( 'wp_next_scheduled' ) ) { function wp_next_scheduled( string $hook ): int|false { return false; } } if ( ! function_exists( 'wp_schedule_event' ) ) { function wp_schedule_event( int $t, string $r, string $h ): bool { return true; } } if ( ! function_exists( 'wp_schedule_single_event' ) ) { function wp_schedule_single_event( int $ts, string $hook ): bool { $GLOBALS['_wp_scheduled_single'][] = $hook; return true; } } +if ( ! function_exists( 'wp_unschedule_hook' ) ) { function wp_unschedule_hook( string $hook ): int|false { $GLOBALS['_wp_unscheduled'][] = $hook; return 0; } } if ( ! function_exists( 'wp_clear_scheduled_hook' ) ) { function wp_clear_scheduled_hook( string $hook ): int|false { return 0; } } if ( ! function_exists( 'sanitize_text_field' ) ) { function sanitize_text_field( string $s ): string { return trim( strip_tags( $s ) ); } } if ( ! function_exists( '__' ) ) { function __( string $text, string $domain = 'default' ): string { return $text; } }