598 lines
18 KiB
PHP
598 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Scheduling functions
|
|
*
|
|
* @package WPVulnerability
|
|
*
|
|
* @version 4.3.2
|
|
*/
|
|
|
|
defined( 'ABSPATH' ) || die( 'No script kiddies please!' );
|
|
|
|
// Ensure shared helpers are available.
|
|
if ( ! function_exists( 'wpvulnerability_cache_hours' ) ) {
|
|
require_once WPVULNERABILITY_PLUGIN_PATH . '/wpvulnerability-general.php';
|
|
}
|
|
|
|
// Add a 6-hour schedule used when cache duration is set to six hours.
|
|
add_filter( 'cron_schedules', 'wpvulnerability_add_every_six_hours' );
|
|
|
|
/**
|
|
* Registers a custom 6-hour cron schedule.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param array<string, array<string, int|string>> $schedules Existing schedules.
|
|
*
|
|
* @return array<string, array<string, int|string>> Modified schedules.
|
|
*/
|
|
function wpvulnerability_add_every_six_hours( $schedules ) {
|
|
$label = did_action( 'init' ) ? __( 'Every 6 hours', 'wpvulnerability' ) : 'Every 6 hours';
|
|
|
|
$schedules['wpvulnerability_six_hours'] = array(
|
|
'interval' => 6 * HOUR_IN_SECONDS,
|
|
'display' => $label,
|
|
);
|
|
|
|
return $schedules;
|
|
}
|
|
|
|
// Add weekly and daily schedules used by the notification cron.
|
|
// Registered here because this file is always loaded, while the notification
|
|
// scheduling that resolves them runs on init for every request type.
|
|
add_filter( 'cron_schedules', 'wpvulnerability_add_every_week' );
|
|
add_filter( 'cron_schedules', 'wpvulnerability_add_every_day' );
|
|
|
|
/**
|
|
* Registers a custom weekly cron schedule.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param array<string, array<string, int|string>> $schedules Existing schedules.
|
|
* @return array<string, array<string, int|string>> Schedules with the weekly interval added.
|
|
*/
|
|
function wpvulnerability_add_every_week( $schedules ) {
|
|
$schedules['weekly'] = array(
|
|
'interval' => 604800,
|
|
'display' => did_action( 'init' ) ? __( 'Every week', 'wpvulnerability' ) : 'Every week',
|
|
);
|
|
|
|
return $schedules;
|
|
}
|
|
|
|
/**
|
|
* Registers a custom daily cron schedule.
|
|
*
|
|
* @since 2.0.0
|
|
*
|
|
* @param array<string, array<string, int|string>> $schedules Existing schedules.
|
|
* @return array<string, array<string, int|string>> Schedules with the daily interval added.
|
|
*/
|
|
function wpvulnerability_add_every_day( $schedules ) {
|
|
$schedules['daily'] = array(
|
|
'interval' => 86400,
|
|
'display' => did_action( 'init' ) ? __( 'Every day', 'wpvulnerability' ) : 'Every day',
|
|
);
|
|
|
|
return $schedules;
|
|
}
|
|
|
|
// Remove legacy scheduled events on subsites in multisite installs.
|
|
if ( is_multisite() && ! is_main_site() ) {
|
|
wpvulnerability_clear_plugin_cron_hooks();
|
|
}
|
|
|
|
// Schedule the automatic vulnerability database updates on init. Scheduling at
|
|
// file scope called wp_get_schedules() before init, firing other plugins'
|
|
// cron_schedules callbacks (and their __() calls) too early on WordPress 6.7+.
|
|
add_action( 'init', 'wpvulnerability_schedule_core_events', 20 );
|
|
|
|
// Hook the event to the function that updates the database.
|
|
add_action( 'wpvulnerability_update_database', 'wpvulnerability_update_database_data' );
|
|
|
|
/**
|
|
* Calculate the next notification timestamp based on plugin settings.
|
|
*
|
|
* @since 4.1.1
|
|
*
|
|
* @param array<string, mixed> $config Plugin configuration.
|
|
* @return int Timestamp for next notification.
|
|
*/
|
|
function wpvulnerability_get_next_notification_timestamp( $config ) {
|
|
$hour_raw = $config['hour'] ?? 0;
|
|
$hour = is_numeric( $hour_raw ) ? max( 0, min( 23, (int) $hour_raw ) ) : 0;
|
|
$min_raw = $config['minute'] ?? 0;
|
|
$minute = is_numeric( $min_raw ) ? max( 0, min( 59, (int) $min_raw ) ) : 0;
|
|
|
|
$timezone = wp_timezone();
|
|
|
|
$current_time = new DateTime( 'now', $timezone );
|
|
$scheduled_time = new DateTime( 'now', $timezone );
|
|
$scheduled_time->setTime( $hour, $minute, 0 );
|
|
|
|
if ( isset( $config['period'] ) && 'weekly' === $config['period'] ) {
|
|
$day_raw = $config['day'] ?? 'monday';
|
|
$day = is_scalar( $day_raw ) ? strtolower( (string) $day_raw ) : 'monday';
|
|
$weekdays = array( 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' );
|
|
$day_index = array_search( $day, $weekdays, true );
|
|
if ( false === $day_index ) {
|
|
$day_index = 1; // Monday.
|
|
}
|
|
while ( (int) $scheduled_time->format( 'w' ) !== $day_index || $scheduled_time->getTimestamp() <= $current_time->getTimestamp() ) {
|
|
$scheduled_time->modify( '+1 day' );
|
|
}
|
|
} elseif ( $scheduled_time->getTimestamp() <= $current_time->getTimestamp() ) {
|
|
$scheduled_time->modify( '+1 day' );
|
|
}
|
|
|
|
return (int) $scheduled_time->getTimestamp();
|
|
}
|
|
|
|
/**
|
|
* Schedule vulnerability notifications.
|
|
*
|
|
* When $force is false, the function first checks the current cron schedule
|
|
* for the notification hook and returns early if it already matches the
|
|
* desired period from $config. This avoids rewriting the wp_options 'cron'
|
|
* row on every page load when the schedule is already correct.
|
|
*
|
|
* Callers that change notification settings (period, hour, minute, day) must
|
|
* keep $force as true so the event is rescheduled with the new timestamp.
|
|
*
|
|
* @since 4.1.1
|
|
*
|
|
* @param array<string, mixed> $config Plugin configuration.
|
|
* @param bool $force Whether to reschedule unconditionally. Defaults to true.
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_schedule_notification_event( $config, $force = true ) {
|
|
if ( ! $force ) {
|
|
$desired = '';
|
|
if ( isset( $config['period'] )
|
|
&& in_array( $config['period'], array( 'daily', 'weekly' ), true )
|
|
&& ( ! is_multisite() || is_main_site() ) ) {
|
|
$desired = (string) $config['period']; // Narrowed to 'daily'|'weekly' by in_array check above.
|
|
}
|
|
|
|
$current = wp_get_schedule( 'wpvulnerability_notification' );
|
|
if ( false === $current ) {
|
|
$current = '';
|
|
}
|
|
|
|
if ( $current === $desired ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
wp_clear_scheduled_hook( 'wpvulnerability_notification' );
|
|
if ( ! isset( $config['period'] ) || 'never' === $config['period'] ) {
|
|
return;
|
|
}
|
|
|
|
if ( ! is_multisite() || is_main_site() ) {
|
|
$period_val = $config['period'];
|
|
$period_str = is_scalar( $period_val ) ? (string) $period_val : '';
|
|
|
|
// Whitelist the period: a corrupted value would make the event unschedulable.
|
|
if ( ! in_array( $period_str, array( 'daily', 'weekly' ), true ) ) {
|
|
return;
|
|
}
|
|
|
|
$timestamp = wpvulnerability_get_next_notification_timestamp( $config );
|
|
wp_schedule_event( $timestamp, $period_str, 'wpvulnerability_notification' );
|
|
}
|
|
}
|
|
|
|
add_action( 'wpvulnerability_notification', 'wpvulnerability_execute_notification' ); // @phpstan-ignore return.void (function returns bool but action callbacks are not required to be void)
|
|
|
|
/**
|
|
* Schedules the notification event on init.
|
|
*
|
|
* Runs at init so wp_get_schedules() is never called before translations can load.
|
|
*
|
|
* @since 5.1.3
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_schedule_notification_boot() {
|
|
$config_raw = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
|
|
$config = is_array( $config_raw ) ? $config_raw : array();
|
|
|
|
wpvulnerability_schedule_notification_event( $config, false );
|
|
}
|
|
add_action( 'init', 'wpvulnerability_schedule_notification_boot', 20 );
|
|
|
|
/**
|
|
* Returns the WPVulnerability cron hooks.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return array<string> List of cron hooks belonging to the plugin.
|
|
*/
|
|
function wpvulnerability_get_plugin_cron_hooks() {
|
|
return array(
|
|
'wpvulnerability_update_database',
|
|
'wpvulnerability_cleanup_logs',
|
|
'wpvulnerability_notification',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Determines the schedule slug for database updates based on cache hours.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return string Schedule identifier.
|
|
*/
|
|
function wpvulnerability_get_update_schedule_slug() {
|
|
$cache_hours = wpvulnerability_cache_hours();
|
|
$mapping = array(
|
|
1 => 'hourly',
|
|
6 => 'wpvulnerability_six_hours',
|
|
12 => 'twicedaily',
|
|
24 => 'daily',
|
|
);
|
|
|
|
$schedule = isset( $mapping[ $cache_hours ] ) ? $mapping[ $cache_hours ] : 'twicedaily';
|
|
|
|
$schedules = function_exists( 'wp_get_schedules' ) ? wp_get_schedules() : array();
|
|
|
|
if ( ! isset( $schedules[ $schedule ] ) ) {
|
|
return 'twicedaily';
|
|
}
|
|
|
|
return $schedule;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the main site ID with backwards compatibility.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return int Main site ID.
|
|
*/
|
|
function wpvulnerability_get_main_site_id() {
|
|
if ( function_exists( 'get_main_site_id' ) ) {
|
|
return (int) get_main_site_id();
|
|
}
|
|
|
|
$current_site = get_current_site();
|
|
if ( isset( $current_site->blog_id ) ) {
|
|
return (int) $current_site->blog_id;
|
|
}
|
|
|
|
return (int) get_current_blog_id();
|
|
}
|
|
|
|
/**
|
|
* Clears all WPVulnerability cron hooks for the current site.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_clear_plugin_cron_hooks() {
|
|
$hooks = wpvulnerability_get_plugin_cron_hooks();
|
|
|
|
foreach ( $hooks as $hook ) {
|
|
wp_clear_scheduled_hook( $hook );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Schedules core WPVulnerability cron events for the current site.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_schedule_core_events() {
|
|
if ( is_multisite() && ! is_main_site() ) {
|
|
return;
|
|
}
|
|
|
|
$update_schedule = wpvulnerability_get_update_schedule_slug();
|
|
|
|
$current_schedule = wp_get_schedule( 'wpvulnerability_update_database' );
|
|
if ( $current_schedule !== $update_schedule ) {
|
|
wp_clear_scheduled_hook( 'wpvulnerability_update_database' );
|
|
wp_schedule_event( time(), $update_schedule, 'wpvulnerability_update_database' );
|
|
}
|
|
|
|
if ( ! wp_next_scheduled( 'wpvulnerability_cleanup_logs' ) ) {
|
|
wp_schedule_event( time(), 'daily', 'wpvulnerability_cleanup_logs' );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the notification schedule string based on settings.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param array<mixed> $config Plugin configuration.
|
|
*
|
|
* @return string Notification schedule name or empty string when disabled.
|
|
*/
|
|
function wpvulnerability_get_notification_schedule_from_config( $config ) {
|
|
if ( ! isset( $config['period'] ) ) {
|
|
return '';
|
|
}
|
|
|
|
$period_raw = $config['period'];
|
|
$period = is_scalar( $period_raw ) ? strtolower( trim( (string) $period_raw ) ) : '';
|
|
|
|
if ( ! in_array( $period, array( 'daily', 'weekly' ), true ) ) {
|
|
return '';
|
|
}
|
|
|
|
return $period;
|
|
}
|
|
|
|
/**
|
|
* Builds the list of expected cron events for the current site.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param array<string, mixed>|mixed $config Plugin configuration.
|
|
* @param bool $is_main_site Whether the current site is the main site on a multisite network.
|
|
*
|
|
* @return array<int, array<string, mixed>> Expected cron events.
|
|
*/
|
|
function wpvulnerability_get_expected_cron_events( $config, $is_main_site ) {
|
|
if ( ! is_array( $config ) ) {
|
|
$config = is_multisite() ? get_site_option( 'wpvulnerability-config' ) : get_option( 'wpvulnerability-config' );
|
|
if ( ! is_array( $config ) ) {
|
|
$config = array();
|
|
}
|
|
}
|
|
|
|
$expect_main_site = ( ! is_multisite() || $is_main_site );
|
|
$update_schedule = wpvulnerability_get_update_schedule_slug();
|
|
$expected_events = array(
|
|
array(
|
|
'hook' => 'wpvulnerability_update_database',
|
|
'schedule' => $update_schedule,
|
|
'should_exist' => $expect_main_site,
|
|
'label' => __( 'Database updates', 'wpvulnerability' ),
|
|
),
|
|
array(
|
|
'hook' => 'wpvulnerability_cleanup_logs',
|
|
'schedule' => 'daily',
|
|
'should_exist' => $expect_main_site,
|
|
'label' => __( 'Log cleanup', 'wpvulnerability' ),
|
|
),
|
|
);
|
|
|
|
$notification_schedule = '';
|
|
|
|
if ( $expect_main_site ) {
|
|
$notification_schedule = wpvulnerability_get_notification_schedule_from_config( $config );
|
|
}
|
|
|
|
$expected_events[] = array(
|
|
'hook' => 'wpvulnerability_notification',
|
|
'schedule' => $notification_schedule,
|
|
'should_exist' => ( '' !== $notification_schedule ),
|
|
'label' => __( 'Notifications', 'wpvulnerability' ),
|
|
);
|
|
|
|
return $expected_events;
|
|
}
|
|
|
|
/**
|
|
* Collects scheduled WPVulnerability cron entries for the current site.
|
|
*
|
|
* Uses only public WordPress Cron API functions (wp_next_scheduled and
|
|
* wp_get_schedule) to avoid relying on the private _get_cron_array() function.
|
|
* As a result, only hooks registered by wpvulnerability_get_plugin_cron_hooks()
|
|
* are inspected; duplicate-instance detection is not performed.
|
|
*
|
|
* @since 4.3.0
|
|
* @since 5.0.0 Replaced _get_cron_array() with wp_next_scheduled()/wp_get_schedule().
|
|
*
|
|
* @return array<int, array<string, mixed>> Scheduled cron entries.
|
|
*/
|
|
function wpvulnerability_get_cron_snapshot() {
|
|
$hooks = wpvulnerability_get_plugin_cron_hooks();
|
|
$events = array();
|
|
|
|
foreach ( $hooks as $hook ) {
|
|
$timestamp = wp_next_scheduled( $hook );
|
|
if ( false === $timestamp ) {
|
|
continue;
|
|
}
|
|
$schedule = wp_get_schedule( $hook );
|
|
$events[] = array(
|
|
'hook' => $hook,
|
|
'timestamp' => (int) $timestamp,
|
|
'schedule' => is_string( $schedule ) ? sanitize_key( $schedule ) : '',
|
|
);
|
|
}
|
|
|
|
return $events;
|
|
}
|
|
|
|
/**
|
|
* Builds a status report comparing expected and actual cron events.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param array<string, mixed> $config Plugin configuration.
|
|
* @param bool $is_main_site Whether the current site is the main site on a multisite network.
|
|
*
|
|
* @return array<string, array<int, array<string, mixed>>> Report including expected rows and unexpected hooks.
|
|
*/
|
|
function wpvulnerability_get_cron_status( $config, $is_main_site ) {
|
|
$expected = wpvulnerability_get_expected_cron_events( $config, $is_main_site );
|
|
$snapshot = wpvulnerability_get_cron_snapshot();
|
|
$expected_rows = array();
|
|
$extra_events = array();
|
|
|
|
foreach ( $expected as $item ) {
|
|
$hook_raw = $item['hook'] ?? '';
|
|
$hook = is_scalar( $hook_raw ) ? (string) $hook_raw : '';
|
|
$expected_rows[ $hook ] = $item;
|
|
$expected_rows[ $hook ]['schedules_found'] = array();
|
|
$expected_rows[ $hook ]['next_run'] = null;
|
|
$expected_rows[ $hook ]['count'] = 0;
|
|
$expected_rows[ $hook ]['messages'] = array();
|
|
}
|
|
|
|
foreach ( $snapshot as $event ) {
|
|
$hook_raw = $event['hook'] ?? '';
|
|
$hook = is_scalar( $hook_raw ) ? (string) $hook_raw : '';
|
|
$ts_raw = $event['timestamp'] ?? 0;
|
|
$timestamp = is_numeric( $ts_raw ) ? (int) $ts_raw : 0;
|
|
$sched_raw = $event['schedule'] ?? '';
|
|
$schedule = is_scalar( $sched_raw ) ? (string) $sched_raw : '';
|
|
|
|
if ( isset( $expected_rows[ $hook ] ) ) {
|
|
++$expected_rows[ $hook ]['count'];
|
|
if ( null === $expected_rows[ $hook ]['next_run'] || $timestamp < $expected_rows[ $hook ]['next_run'] ) {
|
|
$expected_rows[ $hook ]['next_run'] = $timestamp;
|
|
}
|
|
if ( '' !== $schedule ) {
|
|
$expected_rows[ $hook ]['schedules_found'][ $schedule ] = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ( 0 === strpos( $hook, 'wpvulnerability_' ) ) {
|
|
if ( ! isset( $extra_events[ $hook ] ) ) {
|
|
$extra_events[ $hook ] = array(
|
|
'hook' => $hook,
|
|
'count' => 0,
|
|
'next_run' => null,
|
|
'schedules' => array(),
|
|
);
|
|
}
|
|
|
|
++$extra_events[ $hook ]['count'];
|
|
|
|
if ( null === $extra_events[ $hook ]['next_run'] || $timestamp < $extra_events[ $hook ]['next_run'] ) {
|
|
$extra_events[ $hook ]['next_run'] = $timestamp;
|
|
}
|
|
|
|
if ( '' !== $schedule ) {
|
|
$extra_events[ $hook ]['schedules'][ $schedule ] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ( $expected_rows as $hook => $row ) {
|
|
$sched_raw = $row['schedule'] ?? '';
|
|
$expected_schedule = is_scalar( $sched_raw ) ? (string) $sched_raw : '';
|
|
$should_exist = isset( $row['should_exist'] ) ? (bool) $row['should_exist'] : false;
|
|
$found_schedules = array_keys( $row['schedules_found'] );
|
|
|
|
if ( ! $should_exist ) {
|
|
if ( $row['count'] > 0 ) {
|
|
if ( 'wpvulnerability_notification' === $hook ) {
|
|
$row['status'] = 'needs_attention';
|
|
$row['messages'][] = __( 'Notifications are scheduled, but settings currently disable them. Please review the notifications tab.', 'wpvulnerability' );
|
|
} else {
|
|
$row['status'] = 'unexpected';
|
|
$row['messages'][] = __( 'This event should not be scheduled for this site.', 'wpvulnerability' );
|
|
}
|
|
} else {
|
|
$row['status'] = 'not_expected';
|
|
$row['messages'][] = __( 'Not expected for this site.', 'wpvulnerability' );
|
|
}
|
|
} elseif ( 0 === $row['count'] ) {
|
|
$row['status'] = 'missing';
|
|
$row['messages'][] = __( 'No instances found.', 'wpvulnerability' );
|
|
} else {
|
|
$mismatched_schedule = ( '' !== $expected_schedule && ! in_array( $expected_schedule, $found_schedules, true ) );
|
|
$duplicate_events = ( $row['count'] > 1 );
|
|
|
|
if ( $mismatched_schedule ) {
|
|
$row['messages'][] = __( 'Scheduled with an unexpected interval.', 'wpvulnerability' );
|
|
}
|
|
|
|
if ( $duplicate_events ) {
|
|
$row['messages'][] = __( 'Multiple instances detected.', 'wpvulnerability' );
|
|
}
|
|
|
|
if ( empty( $row['messages'] ) ) {
|
|
$row['status'] = 'ok';
|
|
$row['messages'][] = __( 'Scheduled as expected.', 'wpvulnerability' );
|
|
} else {
|
|
$row['status'] = 'needs_attention';
|
|
}
|
|
}
|
|
|
|
$row['schedules_found'] = $found_schedules;
|
|
$expected_rows[ $hook ] = $row;
|
|
}
|
|
|
|
foreach ( $extra_events as $hook => $row ) {
|
|
$extra_events[ $hook ]['schedules'] = array_keys( $row['schedules'] );
|
|
}
|
|
|
|
return array(
|
|
'expected' => array_values( $expected_rows ),
|
|
'unexpected' => array_values( $extra_events ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Repairs WPVulnerability cron events for the current site.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param array<string, mixed> $config Plugin configuration.
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_repair_cron_events( $config ) {
|
|
wpvulnerability_clear_plugin_cron_hooks();
|
|
wpvulnerability_schedule_core_events();
|
|
wpvulnerability_schedule_notification_event( $config );
|
|
}
|
|
|
|
/**
|
|
* Repairs WPVulnerability cron events across all sites in a network.
|
|
*
|
|
* @since 4.3.0
|
|
*
|
|
* @param array<string, mixed> $config Plugin configuration.
|
|
*
|
|
* @return void
|
|
*/
|
|
function wpvulnerability_repair_network_cron_events( $config ) {
|
|
if ( ! is_multisite() ) {
|
|
return;
|
|
}
|
|
|
|
$sites = get_sites(
|
|
array(
|
|
'fields' => 'ids',
|
|
)
|
|
);
|
|
|
|
if ( empty( $sites ) ) {
|
|
return;
|
|
}
|
|
|
|
$main_site_id = wpvulnerability_get_main_site_id();
|
|
$current_blog_id = get_current_blog_id();
|
|
$sanitized_config = $config;
|
|
|
|
foreach ( $sites as $site_id ) {
|
|
switch_to_blog( (int) $site_id );
|
|
|
|
wpvulnerability_clear_plugin_cron_hooks();
|
|
|
|
if ( (int) $site_id === (int) $main_site_id ) {
|
|
wpvulnerability_schedule_core_events();
|
|
wpvulnerability_schedule_notification_event( $sanitized_config );
|
|
}
|
|
|
|
restore_current_blog();
|
|
}
|
|
|
|
if ( get_current_blog_id() !== $current_blog_id ) {
|
|
switch_to_blog( $current_blog_id );
|
|
}
|
|
}
|