| @@ -25,9 +25,9 @@ | ||
| 25 | 25 | */ |
| 26 | 26 | public const LOG_FILE_NAME = 'network-cron-manager'; |
| 27 | 27 | |
| 28 | 28 | /** |
| 29 | - * Array of registered callbacks. | |
| 29 | + * Array of registered hook configurations. | |
| 30 | 30 | * |
| 31 | 31 | * @var array |
| 32 | 32 | */ |
| 33 | 33 | private $callbacks = array(); |
| @@ -64,10 +64,12 @@ | ||
| 64 | 64 | |
| 65 | 65 | /** |
| 66 | 66 | * Load callbacks from network options. |
| 67 | 67 | */ |
| 68 | - private function load_callbacks() { | |
| 69 | - $this->callbacks = get_network_option( get_main_network_id(), $this->callbacks_option, array() ); | |
| 68 | + public function load_callbacks() { | |
| 69 | + if ( array() === $this->callbacks ) { | |
| 70 | + $this->callbacks = get_network_option( get_main_network_id(), $this->callbacks_option, array() ); | |
| 71 | + } | |
| 70 | 72 | } |
| 71 | 73 | |
| 72 | 74 | /** |
| 73 | 75 | * Save callbacks to network options. |
| @@ -92,9 +94,9 @@ | ||
| 92 | 94 | * @return bool|void False on validation failure, void on success. |
| 93 | 95 | */ |
| 94 | 96 | public function register_callback( string $hook_name, callable $callback, int $interval_seconds, $start_time = null, array $args = array() ) { |
| 95 | 97 | $hook_name = sanitize_key( $hook_name ); |
| 96 | - if ( empty( $hook_name ) || ! is_string( $hook_name ) ) { | |
| 98 | + if ( '' === $hook_name ) { | |
| 97 | 99 | $this->log( 'Task registration failed: Invalid task name provided', self::LOG_FILE_NAME ); |
| 98 | 100 | return false; |
| 99 | 101 | } |
| 100 | 102 | if ( ! is_callable( $callback ) ) { |
| @@ -111,23 +113,25 @@ | ||
| 111 | 113 | wp_clear_scheduled_hook( $hook_name ); |
| 112 | 114 | $this->log( "Cleared existing WordPress cron event for '{$hook_name}' to prevent conflicts", self::LOG_FILE_NAME ); |
| 113 | 115 | } |
| 114 | 116 | |
| 115 | - $this->callbacks[ $hook_name ] = array( | |
| 116 | - 'callback' => $callback, | |
| 117 | - 'interval' => $interval_seconds, | |
| 118 | - 'start_time' => $start_time, | |
| 119 | - 'args' => $args, | |
| 120 | - ); | |
| 121 | - $this->save_callbacks(); | |
| 122 | - } else { | |
| 123 | - if ( ! wp_next_scheduled( $hook_name ) ) { | |
| 117 | + // Only save if this is a new callback or something changed. | |
| 118 | + if ( ! isset( $this->callbacks[ $hook_name ] ) || $this->callbacks[ $hook_name ]['interval'] !== $interval_seconds || $this->callbacks[ $hook_name ]['start_time'] !== $start_time ) { | |
| 119 | + $this->callbacks[ $hook_name ] = array( | |
| 120 | + 'interval' => $interval_seconds, | |
| 121 | + 'start_time' => $start_time, | |
| 122 | + 'args' => $args, | |
| 123 | + ); | |
| 124 | + $this->save_callbacks(); | |
| 125 | + } | |
| 126 | + } elseif ( ! wp_next_scheduled( $hook_name ) ) { | |
| 124 | 127 | $start_timestamp = $this->calculate_start_time( $start_time ); |
| 125 | 128 | $schedule = $this->get_schedule_name( $interval_seconds ); |
| 126 | 129 | wp_schedule_event( $start_timestamp, $schedule, $hook_name ); |
| 127 | - } | |
| 128 | - add_action( $hook_name, $callback ); | |
| 129 | 130 | } |
| 131 | + | |
| 132 | + // Add the action to the hook (applies to both multisite and single-site). | |
| 133 | + add_action( $hook_name, $callback ); | |
| 130 | 134 | } |
| 131 | 135 | |
| 132 | 136 | /** |
| 133 | 137 | * Calculate the start time for a cron event. |
| @@ -186,12 +190,30 @@ | ||
| 186 | 190 | /** |
| 187 | 191 | * Check and execute registered callbacks. |
| 188 | 192 | */ |
| 189 | 193 | public function check_and_execute_callbacks() { |
| 190 | - $this->load_callbacks(); | |
| 191 | - if ( empty( $this->callbacks ) ) { | |
| 194 | + // Early exit during plugin deletion/uninstall. | |
| 195 | + if ( defined( 'WP_UNINSTALL_PLUGIN' ) ) { | |
| 192 | 196 | return; |
| 193 | 197 | } |
| 198 | + | |
| 199 | + // Skip if page post ID exists, or during AJAX/cron execution to prevent concurrent runs. | |
| 200 | + $post_data = defender_get_data_from_request( null, 'p' ); | |
| 201 | + if ( | |
| 202 | + ( is_array( $post_data ) && array() !== $post_data ) | |
| 203 | + || defined( 'DOING_AJAX' ) | |
| 204 | + || defined( 'DOING_CRON' ) | |
| 205 | + ) { | |
| 206 | + return; | |
| 207 | + } | |
| 208 | + | |
| 209 | + if ( did_action( 'after_setup_theme' ) > 0 ) { | |
| 210 | + $this->load_callbacks(); | |
| 211 | + } | |
| 212 | + | |
| 213 | + if ( array() === $this->callbacks ) { | |
| 214 | + return; | |
| 215 | + } | |
| 194 | 216 | foreach ( $this->callbacks as $hook_name => $config ) { |
| 195 | 217 | $this->execute_callback( $hook_name, $config ); |
| 196 | 218 | } |
| 197 | 219 | } |
| @@ -208,14 +230,13 @@ | ||
| 208 | 230 | } |
| 209 | 231 | if ( ! $this->acquire_lock( $hook_name ) ) { |
| 210 | 232 | return; |
| 211 | 233 | } |
| 212 | - $start_time = defender_get_current_time(); | |
| 234 | + | |
| 213 | 235 | try { |
| 214 | - if ( is_callable( $config['callback'] ) ) { | |
| 215 | - call_user_func_array( $config['callback'], $config['args'] ); | |
| 216 | - $this->update_last_run( $hook_name ); | |
| 217 | - } | |
| 236 | + // Execute the hook with arguments. | |
| 237 | + do_action( $hook_name, ...$config['args'] ); | |
| 238 | + $this->update_last_run( $hook_name ); | |
| 218 | 239 | } catch ( \Exception $exception ) { |
| 219 | 240 | $this->log( "Task '{$hook_name}' failed to run: " . $exception->getMessage(), self::LOG_FILE_NAME ); |
| 220 | 241 | } finally { |
| 221 | 242 | $this->release_lock( $hook_name ); |
| @@ -238,9 +259,10 @@ | ||
| 238 | 259 | * |
| 239 | 260 | * @param int $interval The interval in seconds. |
| 240 | 261 | * @param string $hook_name The hook name being executed. |
| 241 | 262 | */ |
| 242 | - $interval = (int) apply_filters( 'wpdef_network_cron_interval', $interval, $hook_name ); | |
| 263 | + $interval = apply_filters( 'wpdef_network_cron_interval', $interval, $hook_name ); | |
| 264 | + $interval = ! is_int( $interval ) ? (int) $interval : $interval; | |
| 243 | 265 | $last_run = $this->get_last_run( $hook_name ); |
| 244 | 266 | $current_time = defender_get_current_time(); |
| 245 | 267 | |
| 246 | 268 | if ( ! $last_run && isset( $config['start_time'] ) && null !== $config['start_time'] ) { |
| @@ -272,12 +294,13 @@ | ||
| 272 | 294 | * |
| 273 | 295 | * @param int $timeout The timeout in seconds. |
| 274 | 296 | * @param string $hook_name The hook name being locked. |
| 275 | 297 | */ |
| 276 | - $lock_timeout = (int) apply_filters( 'wpdef_network_cron_lock_timeout', 300, $hook_name ); | |
| 298 | + $lock_timeout = apply_filters( 'wpdef_network_cron_lock_timeout', 300, $hook_name ); | |
| 299 | + $lock_timeout = ! is_int( $lock_timeout ) ? (int) $lock_timeout : $lock_timeout; | |
| 277 | 300 | $lock_key = $this->lock_prefix . $hook_name; |
| 278 | 301 | $existing_lock = get_network_option( get_main_network_id(), $lock_key ); |
| 279 | - if ( $existing_lock && ( defender_get_current_time() - $existing_lock ) < $lock_timeout ) { | |
| 302 | + if ( 0 < $existing_lock && ( defender_get_current_time() - $existing_lock ) < $lock_timeout ) { | |
| 280 | 303 | return false; |
| 281 | 304 | } |
| 282 | 305 | $lock_value = defender_get_current_time(); |
| 283 | 306 | return update_network_option( get_main_network_id(), $lock_key, $lock_value ); |
| @@ -343,6 +366,51 @@ | ||
| 343 | 366 | $this->lock_prefix . '%', |
| 344 | 367 | $this->lastrun_prefix . '%' |
| 345 | 368 | ) |
| 346 | 369 | ); |
| 370 | + } | |
| 371 | + | |
| 372 | + /** | |
| 373 | + * Remove a specific callback. | |
| 374 | + * | |
| 375 | + * - For multisite: Remove the callback from the network option and release the lock. | |
| 376 | + * - For single-site: Unschedule the task. | |
| 377 | + * | |
| 378 | + * @param string $hook_name The hook name. | |
| 379 | + * @return bool Whether the callback was removed. | |
| 380 | + */ | |
| 381 | + public function remove_callback( string $hook_name ) { | |
| 382 | + $hook_name = sanitize_key( $hook_name ); | |
| 383 | + | |
| 384 | + if ( '' === $hook_name ) { | |
| 385 | + return false; | |
| 386 | + } | |
| 387 | + | |
| 388 | + if ( is_multisite() ) { | |
| 389 | + // Ensure callbacks are loaded. | |
| 390 | + $this->load_callbacks(); | |
| 391 | + | |
| 392 | + if ( isset( $this->callbacks[ $hook_name ] ) ) { | |
| 393 | + unset( $this->callbacks[ $hook_name ] ); | |
| 394 | + $this->save_callbacks(); | |
| 395 | + $this->log( "Task '{$hook_name}' removed.", self::LOG_FILE_NAME ); | |
| 396 | + } | |
| 397 | + | |
| 398 | + // Cleanup hook history. | |
| 399 | + $last_run_key = $this->lastrun_prefix . $hook_name; | |
| 400 | + delete_network_option( get_main_network_id(), $last_run_key ); | |
| 401 | + | |
| 402 | + // Release the lock. | |
| 403 | + $this->release_lock( $hook_name ); | |
| 404 | + } else { | |
| 405 | + // Unschedule the task. | |
| 406 | + $timestamp = wp_next_scheduled( $hook_name ); | |
| 407 | + | |
| 408 | + if ( $timestamp ) { | |
| 409 | + wp_unschedule_event( $timestamp, $hook_name ); | |
| 410 | + $this->log( "Task '{$hook_name}' unscheduled.", self::LOG_FILE_NAME ); | |
| 411 | + } | |
| 412 | + } | |
| 413 | + | |
| 414 | + return true; | |
| 347 | 415 | } |
| 348 | 416 | } |