diff --git a/includes/class-activator.php b/includes/class-activator.php index eccb13e..3b67c7b 100644 --- a/includes/class-activator.php +++ b/includes/class-activator.php @@ -13,7 +13,7 @@ final class Activator self::guard_requirements(); self::install_table(); self::seed_silent_drop_options(); - self::schedule_events(); + self::ensure_scheduled(); } public static function deactivate(): void @@ -22,6 +22,63 @@ final class Activator wp_clear_scheduled_hook('cf7stg_cleanup'); } + /** + * Self-heal cron events. Idempotent — safe to call from Plugin::boot() + * on every request. If wp_next_scheduled() shows the hook isn't queued, + * re-schedule it. This recovers from any cause of cron loss (manual + * wp_clear_scheduled_hook, plugin upgrade that bypassed activation, + * truncated wp_options.cron value) without requiring deactivate/activate + * by the user. + * + * The cron_schedules filter must be in place inside this same request so + * 'cf7stg_every_minute' is a known recurrence at wp_schedule_event time. + * Dispatcher::register_hooks() also registers it on plugins_loaded, but + * we add it here too in case ensure_scheduled() runs from a context where + * Dispatcher's registration hasn't fired yet (e.g. during an upgrade hook). + */ + public static function ensure_scheduled(): void + { + add_filter('cron_schedules', ['\\Cf7stg\\Dispatcher', 'register_schedule']); + + if (!wp_next_scheduled('cf7stg_dispatch')) { + wp_schedule_event(time() + 60, 'cf7stg_every_minute', 'cf7stg_dispatch'); + } + if (!wp_next_scheduled('cf7stg_cleanup')) { + wp_schedule_event(time() + 3600, 'daily', 'cf7stg_cleanup'); + } + } + + /** + * upgrader_process_complete callback. WordPress fires this after a + * plugin / theme / core update completes. Plugin updates do NOT call + * register_activation_hook, so any cron events the user's old install + * relied on must be re-installed explicitly here. We also re-run the + * table installer (idempotent dbDelta) and re-seed silent-drop options + * (uses add_option, so user values are preserved). + * + * @param mixed $upgrader WP_Upgrader instance (unused). + * @param array $hook_extra ['action'=>'update','type'=>'plugin','plugins'=>[...]] + */ + public static function on_upgrade_complete($upgrader, $hook_extra): void + { + if (!is_array($hook_extra)) return; + if (($hook_extra['action'] ?? '') !== 'update') return; + if (($hook_extra['type'] ?? '') !== 'plugin') return; + + $plugins = (array)($hook_extra['plugins'] ?? []); + $self = plugin_basename(CF7STG_PLUGIN_FILE); + if (!in_array($self, $plugins, true)) { + return; + } + + // Skip guard_requirements() — already running on a live install whose + // PHP/WP versions are clearly compatible. wp_die here would interrupt + // the upgrader and leave admin in a broken state. + self::install_table(); + self::seed_silent_drop_options(); + self::ensure_scheduled(); + } + private static function guard_requirements(): void { if (version_compare(PHP_VERSION, '7.4', '<')) { @@ -86,20 +143,4 @@ final class Activator ]); } - private static function schedule_events(): void - { - // Register the custom 60-second schedule here — Dispatcher::register_hooks() - // runs on plugins_loaded, which is AFTER activation. Without this filter - // being in place during activate(), wp_schedule_event with an unknown - // recurrence ('cf7stg_every_minute') silently fails and the event never - // fires. - add_filter('cron_schedules', ['\\Cf7stg\\Dispatcher', 'register_schedule']); - - if (!wp_next_scheduled('cf7stg_dispatch')) { - wp_schedule_event(time() + 60, 'cf7stg_every_minute', 'cf7stg_dispatch'); - } - if (!wp_next_scheduled('cf7stg_cleanup')) { - wp_schedule_event(time() + 3600, 'daily', 'cf7stg_cleanup'); - } - } } diff --git a/includes/class-plugin.php b/includes/class-plugin.php index 12138d7..d79fec0 100644 --- a/includes/class-plugin.php +++ b/includes/class-plugin.php @@ -50,6 +50,15 @@ final class Plugin Dispatcher::register_hooks(); (new CF7Source())->register(); + // Self-heal cron events on every boot. Recovers from PUC self-update + // (which doesn't run register_activation_hook) and any other cause of + // cron event loss. Idempotent — wp_next_scheduled() short-circuits + // when events are already queued. + Activator::ensure_scheduled(); + + // Re-run install steps after a plugin upgrade (PUC or wp-admin upload). + add_action('upgrader_process_complete', ['\\Cf7stg\\Activator', 'on_upgrade_complete'], 10, 2); + self::register_update_checker(); // Admin diff --git a/readme.txt b/readme.txt index c6ef471..f89a4a0 100644 --- a/readme.txt +++ b/readme.txt @@ -24,6 +24,10 @@ License: GPLv2 or later == Changelog == += 1.2.1 = +* Fix: Cron-события `cf7stg_dispatch` / `cf7stg_cleanup` теперь самовосстанавливаются. `Plugin::boot()` на каждой загрузке вызывает `Activator::ensure_scheduled()` — если событие пропало (PUC self-update без `register_activation_hook`, ручной `wp_clear_scheduled_hook`, обрезанный wp_options.cron), оно немедленно пересоздаётся. До фикса очередь могла молча застревать на дни без признаков ошибки (sent-24h=0, failed=0). +* Fix: Подписан хук `upgrader_process_complete` — после plugin update WordPress повторно вызывает `Activator::install_table()`, `seed_silent_drop_options()` и `ensure_scheduled()`. Без guard_requirements (чтобы не убить admin при апгрейде). + = 1.2.0 = * New: Self-hosted plugin updates from Gitea via Plugin Update Checker v5.6. WordPress admin shows updates automatically (checked hourly). Source: