PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
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
defender-security / src / component / class-network-cron-manager.php

class-network-cron-manager.php in Defender Security – Malware Scanner, Login Security & Firewall 5.8.1, at src/component/class-network-cron-manager.php

412 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Network Cron Manager for WP Defender.
4 *
5 * This file contains the Network_Cron_Manager class which manages
6 * centralized cron jobs across a multisite network with locking.
7 *
8 * @package WP_Defender\Component
9 */
10
11 namespace WP_Defender\Component;
12
13 use WP_Defender\Component;
14
15 /**
16 * Network Cron Manager class.
17 *
18 * Provides centralized cron management with locking and multisite execution control.
19 */
20 class Network_Cron_Manager extends Component {
21 /**
22 * Log file name constant.
23 *
24 * @var string
25 */
26 public const LOG_FILE_NAME = 'network-cron-manager';
27
28 /**
29 * Array of registered hook configurations.
30 *
31 * @var array
32 */
33 private $callbacks = array();
34
35 /**
36 * Prefix for lock keys.
37 *
38 * @var string
39 */
40 private $lock_prefix = 'wpdef_cron_manager_lock_';
41
42 /**
43 * Prefix for last run timestamp keys.
44 *
45 * @var string
46 */
47 private $lastrun_prefix = 'wpdef_cron_manager_lastrun_';
48
49 /**
50 * Option name for storing callbacks.
51 *
52 * @var string
53 */
54 private $callbacks_option = 'wpdef_cron_manager_callbacks';
55
56 /**
57 * Constructor.
58 *
59 * Initializes the cron manager and hooks into WordPress.
60 */
61 public function __construct() {
62 add_action( 'shutdown', array( $this, 'check_and_execute_callbacks' ), PHP_INT_MAX );
63 }
64
65 /**
66 * Load callbacks from network options.
67 */
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 }
72 }
73
74 /**
75 * Save callbacks to network options.
76 */
77 private function save_callbacks() {
78 update_network_option( get_main_network_id(), $this->callbacks_option, $this->callbacks );
79 }
80
81 /**
82 * Register a callback for cron execution.
83 *
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 *
94 * @return bool|void False on validation failure, void on success.
95 */
96 public function register_callback( string $hook_name, callable $callback, int $interval_seconds, $start_time = null, array $args = array() ) {
97 $hook_name = sanitize_key( $hook_name );
98 if ( '' === $hook_name ) {
99 $this->log( 'Task registration failed: Invalid task name provided', self::LOG_FILE_NAME );
100 return false;
101 }
102 if ( ! is_callable( $callback ) ) {
103 $this->log( "Task registration failed: Task '{$hook_name}' function cannot be called", self::LOG_FILE_NAME );
104 return false;
105 }
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 );
108 return false;
109 }
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.
118 if ( ! isset( $this->callbacks[ $hook_name ] ) || $this->callbacks[ $hook_name ]['interval'] !== $interval_seconds ) {
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 );
134 }
135
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 /**
191 * Check and execute registered callbacks.
192 */
193 public function check_and_execute_callbacks() {
194 // Early exit during plugin deletion/uninstall.
195 if ( defined( 'WP_UNINSTALL_PLUGIN' ) ) {
196 return;
197 }
198
199 // Skip if page post ID exists, or during AJAX/cron execution to prevent concurrent runs.
200 if ( ! empty( defender_get_data_from_request( null, 'p' ) ) || defined( 'DOING_AJAX' ) || defined( 'DOING_CRON' ) ) {
201 return;
202 }
203
204 if ( did_action( 'after_setup_theme' ) > 0 ) {
205 $this->load_callbacks();
206 }
207
208 if ( array() === $this->callbacks ) {
209 return;
210 }
211 foreach ( $this->callbacks as $hook_name => $config ) {
212 $this->execute_callback( $hook_name, $config );
213 }
214 }
215
216 /**
217 * Execute a specific callback.
218 *
219 * @param string $hook_name The hook name.
220 * @param array $config The callback configuration.
221 */
222 private function execute_callback( $hook_name, $config ) {
223 if ( ! $this->should_execute( $hook_name, $config ) ) {
224 return;
225 }
226 if ( ! $this->acquire_lock( $hook_name ) ) {
227 return;
228 }
229
230 try {
231 // Execute the hook with arguments.
232 do_action( $hook_name, ...$config['args'] );
233 $this->update_last_run( $hook_name );
234 } catch ( \Exception $exception ) {
235 $this->log( "Task '{$hook_name}' failed to run: " . $exception->getMessage(), self::LOG_FILE_NAME );
236 } finally {
237 $this->release_lock( $hook_name );
238 }
239 }
240
241 /**
242 * Check if a callback should be executed based on interval and start_time.
243 *
244 * @param string $hook_name The hook name.
245 * @param array $config The callback configuration.
246 *
247 * @return bool Whether the callback should execute.
248 */
249 private function should_execute( $hook_name, $config ) {
250 $interval = $config['interval'];
251
252 /**
253 * Filter to modify execution intervals for network cron jobs.
254 *
255 * @param int $interval The interval in seconds.
256 * @param string $hook_name The hook name being executed.
257 */
258 $interval = apply_filters( 'wpdef_network_cron_interval', $interval, $hook_name );
259 $interval = ! is_int( $interval ) ? (int) $interval : $interval;
260 $last_run = $this->get_last_run( $hook_name );
261 $current_time = defender_get_current_time();
262
263 if ( ! $last_run && isset( $config['start_time'] ) && null !== $config['start_time'] ) {
264 $start_timestamp = $this->calculate_start_time( $config['start_time'] );
265 if ( $current_time < $start_timestamp ) {
266 return false;
267 }
268 }
269
270 if ( $last_run ) {
271 $time_diff = $current_time - $last_run;
272 if ( $time_diff < $interval ) {
273 return false;
274 }
275 }
276
277 return true;
278 }
279
280 /**
281 * Acquire a lock for a specific hook.
282 *
283 * @param string $hook_name The hook name.
284 * @return bool Whether the lock was acquired.
285 */
286 private function acquire_lock( $hook_name ) {
287 /**
288 * Filter to modify lock timeout for network cron jobs.
289 *
290 * @param int $timeout The timeout in seconds.
291 * @param string $hook_name The hook name being locked.
292 */
293 $lock_timeout = apply_filters( 'wpdef_network_cron_lock_timeout', 300, $hook_name );
294 $lock_timeout = ! is_int( $lock_timeout ) ? (int) $lock_timeout : $lock_timeout;
295 $lock_key = $this->lock_prefix . $hook_name;
296 $existing_lock = get_network_option( get_main_network_id(), $lock_key );
297 if ( 0 < $existing_lock && ( defender_get_current_time() - $existing_lock ) < $lock_timeout ) {
298 return false;
299 }
300 $lock_value = defender_get_current_time();
301 return update_network_option( get_main_network_id(), $lock_key, $lock_value );
302 }
303
304 /**
305 * Release a lock for a specific hook.
306 *
307 * @param string $hook_name The hook name.
308 */
309 private function release_lock( $hook_name ) {
310 $lock_key = $this->lock_prefix . $hook_name;
311 delete_network_option( get_main_network_id(), $lock_key );
312 }
313
314 /**
315 * Get the last run timestamp for a hook.
316 *
317 * @param string $hook_name The hook name.
318 * @return int The last run timestamp.
319 */
320 private function get_last_run( $hook_name ) {
321 $lastrun_key = $this->lastrun_prefix . $hook_name;
322 return get_network_option( get_main_network_id(), $lastrun_key, 0 );
323 }
324
325 /**
326 * Update the last run timestamp for a hook.
327 *
328 * @param string $hook_name The hook name.
329 */
330 private function update_last_run( $hook_name ) {
331 $lastrun_key = $this->lastrun_prefix . $hook_name;
332 $timestamp = defender_get_current_time();
333 update_network_option( get_main_network_id(), $lastrun_key, $timestamp );
334 }
335
336 /**
337 * Get all registered callbacks.
338 *
339 * @return array The registered callbacks.
340 */
341 public function get_callbacks() {
342 return $this->callbacks;
343 }
344
345 /**
346 * Remove all Network Cron Manager data during uninstallation.
347 */
348 public function remove_data() {
349 if ( ! is_multisite() ) {
350 return;
351 }
352
353 $network_id = get_main_network_id();
354
355 delete_network_option( $network_id, $this->callbacks_option );
356
357 global $wpdb;
358 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
359 $wpdb->prepare(
360 "DELETE FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s OR meta_key LIKE %s",
361 $this->lock_prefix . '%',
362 $this->lastrun_prefix . '%'
363 )
364 );
365 }
366
367 /**
368 * Remove a specific callback.
369 *
370 * - For multisite: Remove the callback from the network option and release the lock.
371 * - For single-site: Unschedule the task.
372 *
373 * @param string $hook_name The hook name.
374 * @return bool Whether the callback was removed.
375 */
376 public function remove_callback( string $hook_name ) {
377 $hook_name = sanitize_key( $hook_name );
378
379 if ( '' === $hook_name ) {
380 return false;
381 }
382
383 if ( is_multisite() ) {
384 // Ensure callbacks are loaded.
385 $this->load_callbacks();
386
387 if ( isset( $this->callbacks[ $hook_name ] ) ) {
388 unset( $this->callbacks[ $hook_name ] );
389 $this->save_callbacks();
390 $this->log( "Task '{$hook_name}' removed.", self::LOG_FILE_NAME );
391 }
392
393 // Cleanup hook history.
394 $last_run_key = $this->lastrun_prefix . $hook_name;
395 delete_network_option( get_main_network_id(), $last_run_key );
396
397 // Release the lock.
398 $this->release_lock( $hook_name );
399 } else {
400 // Unschedule the task.
401 $timestamp = wp_next_scheduled( $hook_name );
402
403 if ( $timestamp ) {
404 wp_unschedule_event( $timestamp, $hook_name );
405 $this->log( "Task '{$hook_name}' unscheduled.", self::LOG_FILE_NAME );
406 }
407 }
408
409 return true;
410 }
411 }
412