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

404 lines 11.1 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 if ( did_action( 'after_setup_theme' ) > 0 ) {
63 $this->load_callbacks();
64 }
65
66 add_action( 'shutdown', array( $this, 'check_and_execute_callbacks' ), PHP_INT_MAX );
67 }
68
69 /**
70 * Load callbacks from network options.
71 */
72 public function load_callbacks() {
73 if ( array() === $this->callbacks ) {
74 $this->callbacks = get_network_option( get_main_network_id(), $this->callbacks_option, array() );
75 }
76 }
77
78 /**
79 * Save callbacks to network options.
80 */
81 private function save_callbacks() {
82 update_network_option( get_main_network_id(), $this->callbacks_option, $this->callbacks );
83 }
84
85 /**
86 * Register a callback for cron execution.
87 *
88 * Automatically handles both multisite and single-site setups:
89 * - For multisite: Uses Network Cron Manager execution with start_time support
90 * - For single-site: Uses WordPress native cron with proper scheduling
91 *
92 * @param string $hook_name The hook name.
93 * @param callable $callback The callback function.
94 * @param int $interval_seconds The interval in seconds.
95 * @param int|string|null $start_time When to start: timestamp, 'next Thursday', etc. Defaults to defender_get_current_time().
96 * @param array $args Arguments for the callback.
97 *
98 * @return bool|void False on validation failure, void on success.
99 */
100 public function register_callback( string $hook_name, callable $callback, int $interval_seconds, $start_time = null, array $args = array() ) {
101 $hook_name = sanitize_key( $hook_name );
102 if ( '' === $hook_name ) {
103 $this->log( 'Task registration failed: Invalid task name provided', self::LOG_FILE_NAME );
104 return false;
105 }
106 if ( ! is_callable( $callback ) ) {
107 $this->log( "Task registration failed: Task '{$hook_name}' function cannot be called", self::LOG_FILE_NAME );
108 return false;
109 }
110 if ( ! is_numeric( $interval_seconds ) || $interval_seconds < 1 ) {
111 $this->log( "Task registration failed: Task '{$hook_name}' has invalid run interval ({$interval_seconds} seconds)", self::LOG_FILE_NAME );
112 return false;
113 }
114
115 if ( is_multisite() ) {
116 if ( wp_next_scheduled( $hook_name ) ) {
117 wp_clear_scheduled_hook( $hook_name );
118 $this->log( "Cleared existing WordPress cron event for '{$hook_name}' to prevent conflicts", self::LOG_FILE_NAME );
119 }
120
121 // Only save if this is a new callback.
122 if ( ! isset( $this->callbacks[ $hook_name ] ) || $this->callbacks[ $hook_name ]['interval'] !== $interval_seconds ) {
123 $this->callbacks[ $hook_name ] = array(
124 'interval' => $interval_seconds,
125 'start_time' => $start_time,
126 'args' => $args,
127 );
128 $this->save_callbacks();
129 }
130 } elseif ( ! wp_next_scheduled( $hook_name ) ) {
131 $start_timestamp = $this->calculate_start_time( $start_time );
132 $schedule = $this->get_schedule_name( $interval_seconds );
133 wp_schedule_event( $start_timestamp, $schedule, $hook_name );
134 }
135
136 // Add the action to the hook (applies to both multisite and single-site).
137 add_action( $hook_name, $callback );
138 }
139
140 /**
141 * Calculate the start time for a cron event.
142 *
143 * @param int|string|null $start_time The start time specification.
144 *
145 * @return int The timestamp when the cron should first run.
146 */
147 private function calculate_start_time( $start_time = null ) {
148 if ( null === $start_time ) {
149 return defender_get_current_time();
150 }
151
152 if ( is_numeric( $start_time ) ) {
153 return (int) $start_time;
154 }
155
156 if ( is_string( $start_time ) ) {
157 $timestamp = strtotime( $start_time );
158
159 return false !== $timestamp ? $timestamp : defender_get_current_time();
160 }
161
162 return defender_get_current_time();
163 }
164
165 /**
166 * Get WordPress cron schedule name from interval seconds.
167 *
168 * @param int $interval_seconds The interval in seconds.
169 *
170 * @return string The schedule name.
171 */
172 private function get_schedule_name( $interval_seconds ) {
173 $schedules = wp_get_schedules();
174
175 foreach ( $schedules as $schedule_name => $schedule_data ) {
176 if ( isset( $schedule_data['interval'] ) && $schedule_data['interval'] === $interval_seconds ) {
177 return $schedule_name;
178 }
179 }
180
181 switch ( $interval_seconds ) {
182 case 12 * HOUR_IN_SECONDS:
183 return 'twicedaily';
184 case DAY_IN_SECONDS:
185 return 'daily';
186 case WEEK_IN_SECONDS:
187 return 'weekly';
188 case HOUR_IN_SECONDS:
189 default:
190 return 'hourly';
191 }
192 }
193
194 /**
195 * Check and execute registered callbacks.
196 */
197 public function check_and_execute_callbacks() {
198 // Early exit during plugin deletion/uninstall.
199 if ( defined( 'WP_UNINSTALL_PLUGIN' ) ) {
200 return;
201 }
202 if ( array() === $this->callbacks ) {
203 return;
204 }
205 foreach ( $this->callbacks as $hook_name => $config ) {
206 $this->execute_callback( $hook_name, $config );
207 }
208 }
209
210 /**
211 * Execute a specific callback.
212 *
213 * @param string $hook_name The hook name.
214 * @param array $config The callback configuration.
215 */
216 private function execute_callback( $hook_name, $config ) {
217 if ( ! $this->should_execute( $hook_name, $config ) ) {
218 return;
219 }
220 if ( ! $this->acquire_lock( $hook_name ) ) {
221 return;
222 }
223
224 try {
225 // Execute the hook with arguments.
226 do_action( $hook_name, ...$config['args'] );
227 $this->update_last_run( $hook_name );
228 } catch ( \Exception $exception ) {
229 $this->log( "Task '{$hook_name}' failed to run: " . $exception->getMessage(), self::LOG_FILE_NAME );
230 } finally {
231 $this->release_lock( $hook_name );
232 }
233 }
234
235 /**
236 * Check if a callback should be executed based on interval and start_time.
237 *
238 * @param string $hook_name The hook name.
239 * @param array $config The callback configuration.
240 *
241 * @return bool Whether the callback should execute.
242 */
243 private function should_execute( $hook_name, $config ) {
244 $interval = $config['interval'];
245
246 /**
247 * Filter to modify execution intervals for network cron jobs.
248 *
249 * @param int $interval The interval in seconds.
250 * @param string $hook_name The hook name being executed.
251 */
252 $interval = (int) apply_filters( 'wpdef_network_cron_interval', $interval, $hook_name );
253 $last_run = $this->get_last_run( $hook_name );
254 $current_time = defender_get_current_time();
255
256 if ( ! $last_run && isset( $config['start_time'] ) && null !== $config['start_time'] ) {
257 $start_timestamp = $this->calculate_start_time( $config['start_time'] );
258 if ( $current_time < $start_timestamp ) {
259 return false;
260 }
261 }
262
263 if ( $last_run ) {
264 $time_diff = $current_time - $last_run;
265 if ( $time_diff < $interval ) {
266 return false;
267 }
268 }
269
270 return true;
271 }
272
273 /**
274 * Acquire a lock for a specific hook.
275 *
276 * @param string $hook_name The hook name.
277 * @return bool Whether the lock was acquired.
278 */
279 private function acquire_lock( $hook_name ) {
280 /**
281 * Filter to modify lock timeout for network cron jobs.
282 *
283 * @param int $timeout The timeout in seconds.
284 * @param string $hook_name The hook name being locked.
285 */
286 $lock_timeout = (int) apply_filters( 'wpdef_network_cron_lock_timeout', 300, $hook_name );
287 $lock_key = $this->lock_prefix . $hook_name;
288 $existing_lock = get_network_option( get_main_network_id(), $lock_key );
289 if ( $existing_lock && ( defender_get_current_time() - $existing_lock ) < $lock_timeout ) {
290 return false;
291 }
292 $lock_value = defender_get_current_time();
293 return update_network_option( get_main_network_id(), $lock_key, $lock_value );
294 }
295
296 /**
297 * Release a lock for a specific hook.
298 *
299 * @param string $hook_name The hook name.
300 */
301 private function release_lock( $hook_name ) {
302 $lock_key = $this->lock_prefix . $hook_name;
303 delete_network_option( get_main_network_id(), $lock_key );
304 }
305
306 /**
307 * Get the last run timestamp for a hook.
308 *
309 * @param string $hook_name The hook name.
310 * @return int The last run timestamp.
311 */
312 private function get_last_run( $hook_name ) {
313 $lastrun_key = $this->lastrun_prefix . $hook_name;
314 return get_network_option( get_main_network_id(), $lastrun_key, 0 );
315 }
316
317 /**
318 * Update the last run timestamp for a hook.
319 *
320 * @param string $hook_name The hook name.
321 */
322 private function update_last_run( $hook_name ) {
323 $lastrun_key = $this->lastrun_prefix . $hook_name;
324 $timestamp = defender_get_current_time();
325 update_network_option( get_main_network_id(), $lastrun_key, $timestamp );
326 }
327
328 /**
329 * Get all registered callbacks.
330 *
331 * @return array The registered callbacks.
332 */
333 public function get_callbacks() {
334 return $this->callbacks;
335 }
336
337 /**
338 * Remove all Network Cron Manager data during uninstallation.
339 */
340 public function remove_data() {
341 if ( ! is_multisite() ) {
342 return;
343 }
344
345 $network_id = get_main_network_id();
346
347 delete_network_option( $network_id, $this->callbacks_option );
348
349 global $wpdb;
350 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
351 $wpdb->prepare(
352 "DELETE FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s OR meta_key LIKE %s",
353 $this->lock_prefix . '%',
354 $this->lastrun_prefix . '%'
355 )
356 );
357 }
358
359 /**
360 * Remove a specific callback.
361 *
362 * - For multisite: Remove the callback from the network option and release the lock.
363 * - For single-site: Unschedule the task.
364 *
365 * @param string $hook_name The hook name.
366 * @return bool Whether the callback was removed.
367 */
368 public function remove_callback( string $hook_name ) {
369 $hook_name = sanitize_key( $hook_name );
370
371 if ( '' === $hook_name ) {
372 return false;
373 }
374
375 if ( is_multisite() ) {
376 // Ensure callbacks are loaded.
377 $this->load_callbacks();
378
379 if ( isset( $this->callbacks[ $hook_name ] ) ) {
380 unset( $this->callbacks[ $hook_name ] );
381 $this->save_callbacks();
382 $this->log( "Task '{$hook_name}' removed.", self::LOG_FILE_NAME );
383 }
384
385 // Cleanup hook history.
386 $last_run_key = $this->lastrun_prefix . $hook_name;
387 delete_network_option( get_main_network_id(), $last_run_key );
388
389 // Release the lock.
390 $this->release_lock( $hook_name );
391 } else {
392 // Unschedule the task.
393 $timestamp = wp_next_scheduled( $hook_name );
394
395 if ( $timestamp ) {
396 wp_unschedule_event( $timestamp, $hook_name );
397 $this->log( "Task '{$hook_name}' unscheduled.", self::LOG_FILE_NAME );
398 }
399 }
400
401 return true;
402 }
403 }
404