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

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

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