PluginProbe
WPVulnerability / 5.1.2
WPVulnerability v5.1.2
5.1.6 5.1.2 5.1.1 5.0.1 5.0.0 trunk 0.1 0.2 1.0 1.0.1 1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 57 releases
wpvulnerability / wpvulnerability-schedule.php

wpvulnerability-schedule.php in WPVulnerability 5.1.2, at wpvulnerability-schedule.php

586 lines 17.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Scheduling functions
4 *
5 * @package WPVulnerability
6 *
7 * @version 4.3.2
8 */
9
10 defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
11
12 // Ensure shared helpers are available.
13 if ( ! function_exists( 'wpvulnerability_cache_hours' ) ) {
14 require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-general.php';
15 }
16
17 // Add a 6-hour schedule used when cache duration is set to six hours.
18 add_filter( 'cron_schedules', 'wpvulnerability_add_every_six_hours' );
19
20 /**
21 * Registers a custom 6-hour cron schedule.
22 *
23 * @since 4.3.0
24 *
25 * @param array<string, array<string, int|string>> $schedules Existing schedules.
26 *
27 * @return array<string, array<string, int|string>> Modified schedules.
28 */
29 function wpvulnerability_add_every_six_hours( $schedules ) {
30 $label = did_action( 'init' ) ? __( 'Every 6 hours', 'wpvulnerability' ) : 'Every 6 hours';
31
32 $schedules['wpvulnerability_six_hours'] = array(
33 'interval' => 6 * HOUR_IN_SECONDS,
34 'display' => $label,
35 );
36
37 return $schedules;
38 }
39
40 // Add weekly and daily schedules used by the notification cron.
41 // These must be registered in this file (always loaded pre-init) so that the
42 // on-load notification scheduling at the bottom of this file can resolve the
43 // 'weekly' schedule before notifications.php is loaded at init.
44 add_filter( 'cron_schedules', 'wpvulnerability_add_every_week' );
45 add_filter( 'cron_schedules', 'wpvulnerability_add_every_day' );
46
47 /**
48 * Registers a custom weekly cron schedule.
49 *
50 * @since 2.0.0
51 *
52 * @param array<string, array<string, int|string>> $schedules Existing schedules.
53 * @return array<string, array<string, int|string>> Schedules with the weekly interval added.
54 */
55 function wpvulnerability_add_every_week( $schedules ) {
56 $schedules['weekly'] = array(
57 'interval' => 604800,
58 'display' => did_action( 'init' ) ? __( 'Every week', 'wpvulnerability' ) : 'Every week',
59 );
60
61 return $schedules;
62 }
63
64 /**
65 * Registers a custom daily cron schedule.
66 *
67 * @since 2.0.0
68 *
69 * @param array<string, array<string, int|string>> $schedules Existing schedules.
70 * @return array<string, array<string, int|string>> Schedules with the daily interval added.
71 */
72 function wpvulnerability_add_every_day( $schedules ) {
73 $schedules['daily'] = array(
74 'interval' => 86400,
75 'display' => did_action( 'init' ) ? __( 'Every day', 'wpvulnerability' ) : 'Every day',
76 );
77
78 return $schedules;
79 }
80
81 // Remove legacy scheduled events on subsites in multisite installs.
82 if ( is_multisite() && ! is_main_site() ) {
83 wpvulnerability_clear_plugin_cron_hooks();
84 }
85
86 /**
87 * Schedule Automatic Vulnerability Database Update.
88 * If the 'wpvulnerability_update_database' event is not already scheduled, schedule it to run twice daily.
89 *
90 * @since 2.0.0
91 *
92 * @return void
93 */
94 wpvulnerability_schedule_core_events();
95
96 // Hook the event to the function that updates the database.
97 add_action( 'wpvulnerability_update_database', 'wpvulnerability_update_database_data' );
98
99 /**
100 * Calculate the next notification timestamp based on plugin settings.
101 *
102 * @since 4.1.1
103 *
104 * @param array<string, mixed> $config Plugin configuration.
105 * @return int Timestamp for next notification.
106 */
107 function wpvulnerability_get_next_notification_timestamp( $config ) {
108 $hour_raw = $config['hour'] ?? 0;
109 $hour = is_numeric( $hour_raw ) ? max( 0, min( 23, (int) $hour_raw ) ) : 0;
110 $min_raw = $config['minute'] ?? 0;
111 $minute = is_numeric( $min_raw ) ? max( 0, min( 59, (int) $min_raw ) ) : 0;
112
113 $timezone = wp_timezone();
114
115 $current_time = new DateTime( 'now', $timezone );
116 $scheduled_time = new DateTime( 'now', $timezone );
117 $scheduled_time->setTime( $hour, $minute, 0 );
118
119 if ( isset( $config['period'] ) && 'weekly' === $config['period'] ) {
120 $day_raw = $config['day'] ?? 'monday';
121 $day = is_scalar( $day_raw ) ? strtolower( (string) $day_raw ) : 'monday';
122 $weekdays = array( 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' );
123 $day_index = array_search( $day, $weekdays, true );
124 if ( false === $day_index ) {
125 $day_index = 1; // Monday.
126 }
127 while ( (int) $scheduled_time->format( 'w' ) !== $day_index || $scheduled_time->getTimestamp() <= $current_time->getTimestamp() ) {
128 $scheduled_time->modify( '+1 day' );
129 }
130 } elseif ( $scheduled_time->getTimestamp() <= $current_time->getTimestamp() ) {
131 $scheduled_time->modify( '+1 day' );
132 }
133
134 return (int) $scheduled_time->getTimestamp();
135 }
136
137 /**
138 * Schedule vulnerability notifications.
139 *
140 * When $force is false, the function first checks the current cron schedule
141 * for the notification hook and returns early if it already matches the
142 * desired period from $config. This avoids rewriting the wp_options 'cron'
143 * row on every page load when the schedule is already correct.
144 *
145 * Callers that change notification settings (period, hour, minute, day) must
146 * keep $force as true so the event is rescheduled with the new timestamp.
147 *
148 * @since 4.1.1
149 *
150 * @param array<string, mixed> $config Plugin configuration.
151 * @param bool $force Whether to reschedule unconditionally. Defaults to true.
152 *
153 * @return void
154 */
155 function wpvulnerability_schedule_notification_event( $config, $force = true ) {
156 if ( ! $force ) {
157 $desired = '';
158 if ( isset( $config['period'] )
159 && in_array( $config['period'], array( 'daily', 'weekly' ), true )
160 && ( ! is_multisite() || is_main_site() ) ) {
161 $desired = (string) $config['period']; // Narrowed to 'daily'|'weekly' by in_array check above.
162 }
163
164 $current = wp_get_schedule( 'wpvulnerability_notification' );
165 if ( false === $current ) {
166 $current = '';
167 }
168
169 if ( $current === $desired ) {
170 return;
171 }
172 }
173
174 wp_clear_scheduled_hook( 'wpvulnerability_notification' );
175 if ( ! isset( $config['period'] ) || 'never' === $config['period'] ) {
176 return;
177 }
178
179 if ( ! is_multisite() || is_main_site() ) {
180 $period_val = $config['period'];
181 $period_str = is_scalar( $period_val ) ? (string) $period_val : '';
182 $timestamp = wpvulnerability_get_next_notification_timestamp( $config );
183 wp_schedule_event( $timestamp, $period_str, 'wpvulnerability_notification' );
184 }
185 }
186
187 $wpvulnerability_s_raw = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
188 $wpvulnerability_s = is_array( $wpvulnerability_s_raw ) ? $wpvulnerability_s_raw : array();
189 wpvulnerability_schedule_notification_event( $wpvulnerability_s, false );
190 add_action( 'wpvulnerability_notification', 'wpvulnerability_execute_notification' ); // @phpstan-ignore return.void (function returns bool but action callbacks are not required to be void)
191 unset( $wpvulnerability_s );
192
193 /**
194 * Returns the WPVulnerability cron hooks.
195 *
196 * @since 4.3.0
197 *
198 * @return array<string> List of cron hooks belonging to the plugin.
199 */
200 function wpvulnerability_get_plugin_cron_hooks() {
201 return array(
202 'wpvulnerability_update_database',
203 'wpvulnerability_cleanup_logs',
204 'wpvulnerability_notification',
205 );
206 }
207
208 /**
209 * Determines the schedule slug for database updates based on cache hours.
210 *
211 * @since 4.3.0
212 *
213 * @return string Schedule identifier.
214 */
215 function wpvulnerability_get_update_schedule_slug() {
216 $cache_hours = wpvulnerability_cache_hours();
217 $mapping = array(
218 1 => 'hourly',
219 6 => 'wpvulnerability_six_hours',
220 12 => 'twicedaily',
221 24 => 'daily',
222 );
223
224 $schedule = isset( $mapping[ $cache_hours ] ) ? $mapping[ $cache_hours ] : 'twicedaily';
225
226 $schedules = function_exists( 'wp_get_schedules' ) ? wp_get_schedules() : array();
227
228 if ( ! isset( $schedules[ $schedule ] ) ) {
229 return 'twicedaily';
230 }
231
232 return $schedule;
233 }
234
235 /**
236 * Retrieves the main site ID with backwards compatibility.
237 *
238 * @since 4.3.0
239 *
240 * @return int Main site ID.
241 */
242 function wpvulnerability_get_main_site_id() {
243 if ( function_exists( 'get_main_site_id' ) ) {
244 return (int) get_main_site_id();
245 }
246
247 $current_site = get_current_site();
248 if ( isset( $current_site->blog_id ) ) {
249 return (int) $current_site->blog_id;
250 }
251
252 return (int) get_current_blog_id();
253 }
254
255 /**
256 * Clears all WPVulnerability cron hooks for the current site.
257 *
258 * @since 4.3.0
259 *
260 * @return void
261 */
262 function wpvulnerability_clear_plugin_cron_hooks() {
263 $hooks = wpvulnerability_get_plugin_cron_hooks();
264
265 foreach ( $hooks as $hook ) {
266 wp_clear_scheduled_hook( $hook );
267 }
268 }
269
270 /**
271 * Schedules core WPVulnerability cron events for the current site.
272 *
273 * @since 4.3.0
274 *
275 * @return void
276 */
277 function wpvulnerability_schedule_core_events() {
278 if ( is_multisite() && ! is_main_site() ) {
279 return;
280 }
281
282 $update_schedule = wpvulnerability_get_update_schedule_slug();
283
284 $current_schedule = wp_get_schedule( 'wpvulnerability_update_database' );
285 if ( $current_schedule !== $update_schedule ) {
286 wp_clear_scheduled_hook( 'wpvulnerability_update_database' );
287 wp_schedule_event( time(), $update_schedule, 'wpvulnerability_update_database' );
288 }
289
290 if ( ! wp_next_scheduled( 'wpvulnerability_cleanup_logs' ) ) {
291 wp_schedule_event( time(), 'daily', 'wpvulnerability_cleanup_logs' );
292 }
293 }
294
295 /**
296 * Returns the notification schedule string based on settings.
297 *
298 * @since 4.3.0
299 *
300 * @param array<mixed> $config Plugin configuration.
301 *
302 * @return string Notification schedule name or empty string when disabled.
303 */
304 function wpvulnerability_get_notification_schedule_from_config( $config ) {
305 if ( ! isset( $config['period'] ) ) {
306 return '';
307 }
308
309 $period_raw = $config['period'];
310 $period = is_scalar( $period_raw ) ? strtolower( trim( (string) $period_raw ) ) : '';
311
312 if ( ! in_array( $period, array( 'daily', 'weekly' ), true ) ) {
313 return '';
314 }
315
316 return $period;
317 }
318
319 /**
320 * Builds the list of expected cron events for the current site.
321 *
322 * @since 4.3.0
323 *
324 * @param array<string, mixed>|mixed $config Plugin configuration.
325 * @param bool $is_main_site Whether the current site is the main site on a multisite network.
326 *
327 * @return array<int, array<string, mixed>> Expected cron events.
328 */
329 function wpvulnerability_get_expected_cron_events( $config, $is_main_site ) {
330 if ( ! is_array( $config ) ) {
331 $config = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
332 if ( ! is_array( $config ) ) {
333 $config = array();
334 }
335 }
336
337 $expect_main_site = ( ! is_multisite() || $is_main_site );
338 $update_schedule = wpvulnerability_get_update_schedule_slug();
339 $expected_events = array(
340 array(
341 'hook' => 'wpvulnerability_update_database',
342 'schedule' => $update_schedule,
343 'should_exist' => $expect_main_site,
344 'label' => __( 'Database updates', 'wpvulnerability' ),
345 ),
346 array(
347 'hook' => 'wpvulnerability_cleanup_logs',
348 'schedule' => 'daily',
349 'should_exist' => $expect_main_site,
350 'label' => __( 'Log cleanup', 'wpvulnerability' ),
351 ),
352 );
353
354 $notification_schedule = '';
355
356 if ( $expect_main_site ) {
357 $notification_schedule = wpvulnerability_get_notification_schedule_from_config( $config );
358 }
359
360 $expected_events[] = array(
361 'hook' => 'wpvulnerability_notification',
362 'schedule' => $notification_schedule,
363 'should_exist' => ( '' !== $notification_schedule ),
364 'label' => __( 'Notifications', 'wpvulnerability' ),
365 );
366
367 return $expected_events;
368 }
369
370 /**
371 * Collects scheduled WPVulnerability cron entries for the current site.
372 *
373 * Uses only public WordPress Cron API functions (wp_next_scheduled and
374 * wp_get_schedule) to avoid relying on the private _get_cron_array() function.
375 * As a result, only hooks registered by wpvulnerability_get_plugin_cron_hooks()
376 * are inspected; duplicate-instance detection is not performed.
377 *
378 * @since 4.3.0
379 * @since 5.0.0 Replaced _get_cron_array() with wp_next_scheduled()/wp_get_schedule().
380 *
381 * @return array<int, array<string, mixed>> Scheduled cron entries.
382 */
383 function wpvulnerability_get_cron_snapshot() {
384 $hooks = wpvulnerability_get_plugin_cron_hooks();
385 $events = array();
386
387 foreach ( $hooks as $hook ) {
388 $timestamp = wp_next_scheduled( $hook );
389 if ( false === $timestamp ) {
390 continue;
391 }
392 $schedule = wp_get_schedule( $hook );
393 $events[] = array(
394 'hook' => $hook,
395 'timestamp' => (int) $timestamp,
396 'schedule' => is_string( $schedule ) ? sanitize_key( $schedule ) : '',
397 );
398 }
399
400 return $events;
401 }
402
403 /**
404 * Builds a status report comparing expected and actual cron events.
405 *
406 * @since 4.3.0
407 *
408 * @param array<string, mixed> $config Plugin configuration.
409 * @param bool $is_main_site Whether the current site is the main site on a multisite network.
410 *
411 * @return array<string, array<int, array<string, mixed>>> Report including expected rows and unexpected hooks.
412 */
413 function wpvulnerability_get_cron_status( $config, $is_main_site ) {
414 $expected = wpvulnerability_get_expected_cron_events( $config, $is_main_site );
415 $snapshot = wpvulnerability_get_cron_snapshot();
416 $expected_rows = array();
417 $extra_events = array();
418
419 foreach ( $expected as $item ) {
420 $hook_raw = $item['hook'] ?? '';
421 $hook = is_scalar( $hook_raw ) ? (string) $hook_raw : '';
422 $expected_rows[ $hook ] = $item;
423 $expected_rows[ $hook ]['schedules_found'] = array();
424 $expected_rows[ $hook ]['next_run'] = null;
425 $expected_rows[ $hook ]['count'] = 0;
426 $expected_rows[ $hook ]['messages'] = array();
427 }
428
429 foreach ( $snapshot as $event ) {
430 $hook_raw = $event['hook'] ?? '';
431 $hook = is_scalar( $hook_raw ) ? (string) $hook_raw : '';
432 $ts_raw = $event['timestamp'] ?? 0;
433 $timestamp = is_numeric( $ts_raw ) ? (int) $ts_raw : 0;
434 $sched_raw = $event['schedule'] ?? '';
435 $schedule = is_scalar( $sched_raw ) ? (string) $sched_raw : '';
436
437 if ( isset( $expected_rows[ $hook ] ) ) {
438 ++$expected_rows[ $hook ]['count'];
439 if ( null === $expected_rows[ $hook ]['next_run'] || $timestamp < $expected_rows[ $hook ]['next_run'] ) {
440 $expected_rows[ $hook ]['next_run'] = $timestamp;
441 }
442 if ( '' !== $schedule ) {
443 $expected_rows[ $hook ]['schedules_found'][ $schedule ] = true;
444 }
445 continue;
446 }
447
448 if ( 0 === strpos( $hook, 'wpvulnerability_' ) ) {
449 if ( ! isset( $extra_events[ $hook ] ) ) {
450 $extra_events[ $hook ] = array(
451 'hook' => $hook,
452 'count' => 0,
453 'next_run' => null,
454 'schedules' => array(),
455 );
456 }
457
458 ++$extra_events[ $hook ]['count'];
459
460 if ( null === $extra_events[ $hook ]['next_run'] || $timestamp < $extra_events[ $hook ]['next_run'] ) {
461 $extra_events[ $hook ]['next_run'] = $timestamp;
462 }
463
464 if ( '' !== $schedule ) {
465 $extra_events[ $hook ]['schedules'][ $schedule ] = true;
466 }
467 }
468 }
469
470 foreach ( $expected_rows as $hook => $row ) {
471 $sched_raw = $row['schedule'] ?? '';
472 $expected_schedule = is_scalar( $sched_raw ) ? (string) $sched_raw : '';
473 $should_exist = isset( $row['should_exist'] ) ? (bool) $row['should_exist'] : false;
474 $found_schedules = array_keys( $row['schedules_found'] );
475
476 if ( ! $should_exist ) {
477 if ( $row['count'] > 0 ) {
478 if ( 'wpvulnerability_notification' === $hook ) {
479 $row['status'] = 'needs_attention';
480 $row['messages'][] = __( 'Notifications are scheduled, but settings currently disable them. Please review the notifications tab.', 'wpvulnerability' );
481 } else {
482 $row['status'] = 'unexpected';
483 $row['messages'][] = __( 'This event should not be scheduled for this site.', 'wpvulnerability' );
484 }
485 } else {
486 $row['status'] = 'not_expected';
487 $row['messages'][] = __( 'Not expected for this site.', 'wpvulnerability' );
488 }
489 } elseif ( 0 === $row['count'] ) {
490 $row['status'] = 'missing';
491 $row['messages'][] = __( 'No instances found.', 'wpvulnerability' );
492 } else {
493 $mismatched_schedule = ( '' !== $expected_schedule && ! in_array( $expected_schedule, $found_schedules, true ) );
494 $duplicate_events = ( $row['count'] > 1 );
495
496 if ( $mismatched_schedule ) {
497 $row['messages'][] = __( 'Scheduled with an unexpected interval.', 'wpvulnerability' );
498 }
499
500 if ( $duplicate_events ) {
501 $row['messages'][] = __( 'Multiple instances detected.', 'wpvulnerability' );
502 }
503
504 if ( empty( $row['messages'] ) ) {
505 $row['status'] = 'ok';
506 $row['messages'][] = __( 'Scheduled as expected.', 'wpvulnerability' );
507 } else {
508 $row['status'] = 'needs_attention';
509 }
510 }
511
512 $row['schedules_found'] = $found_schedules;
513 $expected_rows[ $hook ] = $row;
514 }
515
516 foreach ( $extra_events as $hook => $row ) {
517 $extra_events[ $hook ]['schedules'] = array_keys( $row['schedules'] );
518 }
519
520 return array(
521 'expected' => array_values( $expected_rows ),
522 'unexpected' => array_values( $extra_events ),
523 );
524 }
525
526 /**
527 * Repairs WPVulnerability cron events for the current site.
528 *
529 * @since 4.3.0
530 *
531 * @param array<string, mixed> $config Plugin configuration.
532 *
533 * @return void
534 */
535 function wpvulnerability_repair_cron_events( $config ) {
536 wpvulnerability_clear_plugin_cron_hooks();
537 wpvulnerability_schedule_core_events();
538 wpvulnerability_schedule_notification_event( $config );
539 }
540
541 /**
542 * Repairs WPVulnerability cron events across all sites in a network.
543 *
544 * @since 4.3.0
545 *
546 * @param array<string, mixed> $config Plugin configuration.
547 *
548 * @return void
549 */
550 function wpvulnerability_repair_network_cron_events( $config ) {
551 if ( ! is_multisite() ) {
552 return;
553 }
554
555 $sites = get_sites(
556 array(
557 'fields' => 'ids',
558 )
559 );
560
561 if ( empty( $sites ) ) {
562 return;
563 }
564
565 $main_site_id = wpvulnerability_get_main_site_id();
566 $current_blog_id = get_current_blog_id();
567 $sanitized_config = $config;
568
569 foreach ( $sites as $site_id ) {
570 switch_to_blog( (int) $site_id );
571
572 wpvulnerability_clear_plugin_cron_hooks();
573
574 if ( (int) $site_id === (int) $main_site_id ) {
575 wpvulnerability_schedule_core_events();
576 wpvulnerability_schedule_notification_event( $sanitized_config );
577 }
578
579 restore_current_blog();
580 }
581
582 if ( get_current_blog_id() !== $current_blog_id ) {
583 switch_to_blog( $current_blog_id );
584 }
585 }
586