PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | src/component/class-network-cron-manager.php +203 -36 5.5.0 → 6.3.0 View file →
@@ -18,10 +18,17 @@
18 18 * Provides centralized cron management with locking and multisite execution control.
19 19 */
20 20 class Network_Cron_Manager extends Component {
21 21 /**
22 - * Array of registered callbacks.
22 + * Log file name constant.
23 23 *
24 + * @var string
25 + */
26 + public const LOG_FILE_NAME = 'network-cron-manager';
27 +
28 + /**
29 + * Array of registered hook configurations.
30 + *
24 31 * @var array
25 32 */
26 33 private $callbacks = array();
27 34
@@ -51,16 +58,18 @@
51 58 *
52 59 * Initializes the cron manager and hooks into WordPress.
53 60 */
54 61 public function __construct() {
55 - add_action( 'init', array( $this, 'check_and_execute_callbacks' ), 20 );
62 + add_action( 'shutdown', array( $this, 'check_and_execute_callbacks' ), PHP_INT_MAX );
56 63 }
57 64
58 65 /**
59 66 * Load callbacks from network options.
60 67 */
61 - private function load_callbacks() {
62 - $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 + }
63 72 }
64 73
65 74 /**
66 75 * Save callbacks to network options.
@@ -71,41 +80,140 @@
71 80
72 81 /**
73 82 * Register a callback for cron execution.
74 83 *
75 - * @param string $hook_name The hook name.
76 - * @param callable $callback The callback function.
77 - * @param int $interval_seconds The interval in seconds.
78 - * @param array $args Arguments for the callback.
84 + * Automatically handles both multisite and single-site setups:
85 + * - For multisite: Uses Network Cron Manager execution with start_time support
86 + * - For single-site: Uses WordPress native cron with proper scheduling
87 + *
88 + * @param string $hook_name The hook name.
89 + * @param callable $callback The callback function.
90 + * @param int $interval_seconds The interval in seconds.
91 + * @param int|string|null $start_time When to start: timestamp, 'next Thursday', etc. Defaults to defender_get_current_time().
92 + * @param array $args Arguments for the callback.
93 + *
79 94 * @return bool|void False on validation failure, void on success.
80 95 */
81 - public function register_callback( $hook_name, $callback, $interval_seconds, $args = array() ) {
96 + public function register_callback( string $hook_name, callable $callback, int $interval_seconds, $start_time = null, array $args = array() ) {
82 97 $hook_name = sanitize_key( $hook_name );
83 - if ( empty( $hook_name ) || ! is_string( $hook_name ) ) {
98 + if ( '' === $hook_name ) {
99 + $this->log( 'Task registration failed: Invalid task name provided', self::LOG_FILE_NAME );
84 100 return false;
85 101 }
86 102 if ( ! is_callable( $callback ) ) {
103 + $this->log( "Task registration failed: Task '{$hook_name}' function cannot be called", self::LOG_FILE_NAME );
87 104 return false;
88 105 }
89 106 if ( ! is_numeric( $interval_seconds ) || $interval_seconds < 1 ) {
107 + $this->log( "Task registration failed: Task '{$hook_name}' has invalid run interval ({$interval_seconds} seconds)", self::LOG_FILE_NAME );
90 108 return false;
91 109 }
92 - $this->callbacks[ $hook_name ] = array(
93 - 'callback' => $callback,
94 - 'interval' => $interval_seconds,
95 - 'args' => $args,
96 - );
97 - $this->save_callbacks();
110 +
111 + if ( is_multisite() ) {
112 + if ( wp_next_scheduled( $hook_name ) ) {
113 + wp_clear_scheduled_hook( $hook_name );
114 + $this->log( "Cleared existing WordPress cron event for '{$hook_name}' to prevent conflicts", self::LOG_FILE_NAME );
115 + }
116 +
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 ) ) {
127 + $start_timestamp = $this->calculate_start_time( $start_time );
128 + $schedule = $this->get_schedule_name( $interval_seconds );
129 + wp_schedule_event( $start_timestamp, $schedule, $hook_name );
130 + }
131 +
132 + // Add the action to the hook (applies to both multisite and single-site).
133 + add_action( $hook_name, $callback );
98 134 }
99 135
100 136 /**
137 + * Calculate the start time for a cron event.
138 + *
139 + * @param int|string|null $start_time The start time specification.
140 + *
141 + * @return int The timestamp when the cron should first run.
142 + */
143 + private function calculate_start_time( $start_time = null ) {
144 + if ( null === $start_time ) {
145 + return defender_get_current_time();
146 + }
147 +
148 + if ( is_numeric( $start_time ) ) {
149 + return (int) $start_time;
150 + }
151 +
152 + if ( is_string( $start_time ) ) {
153 + $timestamp = strtotime( $start_time );
154 +
155 + return false !== $timestamp ? $timestamp : defender_get_current_time();
156 + }
157 +
158 + return defender_get_current_time();
159 + }
160 +
161 + /**
162 + * Get WordPress cron schedule name from interval seconds.
163 + *
164 + * @param int $interval_seconds The interval in seconds.
165 + *
166 + * @return string The schedule name.
167 + */
168 + private function get_schedule_name( $interval_seconds ) {
169 + $schedules = wp_get_schedules();
170 +
171 + foreach ( $schedules as $schedule_name => $schedule_data ) {
172 + if ( isset( $schedule_data['interval'] ) && $schedule_data['interval'] === $interval_seconds ) {
173 + return $schedule_name;
174 + }
175 + }
176 +
177 + switch ( $interval_seconds ) {
178 + case 12 * HOUR_IN_SECONDS:
179 + return 'twicedaily';
180 + case DAY_IN_SECONDS:
181 + return 'daily';
182 + case WEEK_IN_SECONDS:
183 + return 'weekly';
184 + case HOUR_IN_SECONDS:
185 + default:
186 + return 'hourly';
187 + }
188 + }
189 +
190 + /**
101 191 * Check and execute registered callbacks.
102 192 */
103 193 public function check_and_execute_callbacks() {
104 - $this->load_callbacks();
105 - if ( empty( $this->callbacks ) ) {
194 + // Early exit during plugin deletion/uninstall.
195 + if ( defined( 'WP_UNINSTALL_PLUGIN' ) ) {
106 196 return;
107 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 + }
108 216 foreach ( $this->callbacks as $hook_name => $config ) {
109 217 $this->execute_callback( $hook_name, $config );
110 218 }
111 219 }
@@ -116,22 +224,21 @@
116 224 * @param string $hook_name The hook name.
117 225 * @param array $config The callback configuration.
118 226 */
119 227 private function execute_callback( $hook_name, $config ) {
120 - if ( ! $this->should_execute( $hook_name, $config['interval'] ) ) {
228 + if ( ! $this->should_execute( $hook_name, $config ) ) {
121 229 return;
122 230 }
123 231 if ( ! $this->acquire_lock( $hook_name ) ) {
124 - $this->log( "Failed to acquire lock for {$hook_name}", 'cron-manager' );
125 232 return;
126 233 }
234 +
127 235 try {
128 - if ( is_callable( $config['callback'] ) ) {
129 - call_user_func_array( $config['callback'], $config['args'] );
130 - $this->update_last_run( $hook_name );
131 - }
236 + // Execute the hook with arguments.
237 + do_action( $hook_name, ...$config['args'] );
238 + $this->update_last_run( $hook_name );
132 239 } catch ( \Exception $exception ) {
133 - $this->log( "Exception in {$hook_name}: " . $exception->getMessage(), 'cron-manager' );
240 + $this->log( "Task '{$hook_name}' failed to run: " . $exception->getMessage(), self::LOG_FILE_NAME );
134 241 } finally {
135 242 $this->release_lock( $hook_name );
136 243 }
137 244 }
@@ -136,15 +243,18 @@
136 243 }
137 244 }
138 245
139 246 /**
140 - * Check if a callback should be executed based on interval.
247 + * Check if a callback should be executed based on interval and start_time.
141 248 *
142 249 * @param string $hook_name The hook name.
143 - * @param int $interval The interval in seconds.
250 + * @param array $config The callback configuration.
251 + *
144 252 * @return bool Whether the callback should execute.
145 253 */
146 - private function should_execute( $hook_name, $interval ) {
254 + private function should_execute( $hook_name, $config ) {
255 + $interval = $config['interval'];
256 +
147 257 /**
148 258 * Filter to modify execution intervals for network cron jobs.
149 259 *
150 260 * @param int $interval The interval in seconds.
@@ -150,15 +260,26 @@
150 260 * @param int $interval The interval in seconds.
151 261 * @param string $hook_name The hook name being executed.
152 262 */
153 263 $interval = apply_filters( 'wpdef_network_cron_interval', $interval, $hook_name );
264 + $interval = ! is_int( $interval ) ? (int) $interval : $interval;
154 265 $last_run = $this->get_last_run( $hook_name );
155 - $current_time = time();
156 - $time_diff = $current_time - $last_run;
157 - if ( $last_run && $time_diff < $interval ) {
158 - $this->log( "Skipping {$hook_name}: not enough time elapsed ({$time_diff} < {$interval})", 'cron-manager' );
159 - return false;
266 + $current_time = defender_get_current_time();
267 +
268 + if ( ! $last_run && isset( $config['start_time'] ) && null !== $config['start_time'] ) {
269 + $start_timestamp = $this->calculate_start_time( $config['start_time'] );
270 + if ( $current_time < $start_timestamp ) {
271 + return false;
272 + }
160 273 }
274 +
275 + if ( $last_run ) {
276 + $time_diff = $current_time - $last_run;
277 + if ( $time_diff < $interval ) {
278 + return false;
279 + }
280 + }
281 +
161 282 return true;
162 283 }
163 284
164 285 /**
@@ -174,14 +295,15 @@
174 295 * @param int $timeout The timeout in seconds.
175 296 * @param string $hook_name The hook name being locked.
176 297 */
177 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;
178 300 $lock_key = $this->lock_prefix . $hook_name;
179 301 $existing_lock = get_network_option( get_main_network_id(), $lock_key );
180 - if ( $existing_lock && ( time() - $existing_lock ) < $lock_timeout ) {
302 + if ( 0 < $existing_lock && ( defender_get_current_time() - $existing_lock ) < $lock_timeout ) {
181 303 return false;
182 304 }
183 - $lock_value = time();
305 + $lock_value = defender_get_current_time();
184 306 return update_network_option( get_main_network_id(), $lock_key, $lock_value );
185 307 }
186 308
187 309 /**
@@ -211,9 +333,9 @@
211 333 * @param string $hook_name The hook name.
212 334 */
213 335 private function update_last_run( $hook_name ) {
214 336 $lastrun_key = $this->lastrun_prefix . $hook_name;
215 - $timestamp = time();
337 + $timestamp = defender_get_current_time();
216 338 update_network_option( get_main_network_id(), $lastrun_key, $timestamp );
217 339 }
218 340
219 341 /**
@@ -244,6 +366,51 @@
244 366 $this->lock_prefix . '%',
245 367 $this->lastrun_prefix . '%'
246 368 )
247 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;
248 415 }
249 416 }