array( 'time' => 'tenminutes', 'hook' => array( __CLASS__, 'generate_reports' ), 'args' => array(), ), 'wsal_periodic_reports_daily' => array( 'time' => 'daily', 'hook' => array( __CLASS__, 'generate_daily_reports' ), 'args' => array(), 'next_run' => '00:00 tomorrow', ), 'wsal_periodic_reports_weekly' => array( 'time' => 'weekly', 'hook' => array( __CLASS__, 'generate_weekly_reports' ), 'args' => array(), 'next_run' => '00:00 next monday', ), 'wsal_periodic_reports_monthly' => array( 'time' => 'monthly', 'hook' => array( __CLASS__, 'generate_monthly_reports' ), 'args' => array(), 'next_run' => '00:00 first day of next month', ), 'wsal_periodic_reports_quarterly' => array( 'time' => 'quarterly', 'hook' => array( __CLASS__, 'generate_quarterly_reports' ), 'args' => array(), 'next_run' => '00:00 first day of +4 months', ), 'wsal_reports_pruning_cron' => array( 'time' => 'daily', 'hook' => array( __CLASS__, 'clear_reports' ), 'args' => array(), ), 'wsal_cleanup_hook' => array( 'time' => 'hourly', 'hook' => array( __CLASS__, 'cleanup_hook' ), 'args' => array(), ), 'wsal_summary_daily_report' => array( 'time' => 'daily', 'hook' => array( __CLASS__, 'generate_daily_summary_reports' ), 'args' => array(), 'next_run' => '03:00 tomorrow', ), 'wsal_summary_weekly_report' => array( 'time' => 'weekly', 'hook' => array( __CLASS__, 'generate_weekly_summary_reports' ), 'args' => array(), 'next_run' => '03:00 next monday', ), ); /** * The name of the option where plugin stores the cron jobs names (related to the plugin itself). * * @since 5.0.0 */ public const CRON_JOBS_SETTINGS_NAME = 'cron_jobs_options'; /** * Class cache for initialization * * @var boolean * * @since 5.3.2 */ private static $initialized = false; /** * Inits the class and its hooks. * * @return void * * @since 5.0.0 */ public static function init() { // Add custom schedules for WSAL early otherwise they won't work. \add_filter( 'cron_schedules', array( __CLASS__, 'recurring_schedules' ), PHP_INT_MAX ); \add_filter( 'wsal_cron_hooks', array( __CLASS__, 'settings_hooks' ) ); \add_filter( 'after_setup_theme', array( __CLASS__, 'initialize_hooks' ), 30000 ); \add_action( 'update_option_start_of_week', array( __CLASS__, 'reschedule_weekly_crons' ) ); if ( Settings_Helper::get_boolean_option_value( 'pruning-date-e', false ) ) { \add_action( 'wsal_cleanup', array( Occurrences_Entity::class, 'prune_records' ) ); } } /** * Adds cron jobs stored in the globals settings (options table). * * @param array $crons - The list of cron jobs to add. * * @since 5.0.0 */ public static function settings_hooks( array $crons ): array { $available_cron_jobs = Settings_Helper::get_option_value( self::CRON_JOBS_SETTINGS_NAME, array() ); if ( ! empty( $available_cron_jobs ) ) { $crons = array_merge( $crons, $available_cron_jobs ); } return $crons; } /** * Hook method for generating the reports. * * @return void * * @since 5.0.0 */ public static function generate_reports() { \do_action( 'wsal_generate_reports', array() ); } /** * Hook method for generating the reports. * * @return void * * @since 5.0.0 */ public static function generate_daily_reports() { \do_action( 'wsal_generate_reports_daily', array() ); } /** * Hook method for generating the reports. * * @return void * * @since 5.0.0 */ public static function generate_weekly_reports() { \do_action( 'wsal_generate_reports_weekly', array() ); } /** * Hook method for generating the reports. * * @return void * * @since 5.0.0 */ public static function generate_monthly_reports() { \do_action( 'wsal_generate_reports_monthly', array() ); } /** * Hook method for generating the reports. * * @return void * * @since 5.0.0 */ public static function generate_quarterly_reports() { \do_action( 'wsal_generate_reports_quarterly', array() ); } /** * Hook method for generating the reports. * * @return void * * @since 5.3.0 */ public static function generate_daily_summary_reports() { Notifications::send_daily_summary_cron(); } /** * Hook method for generating the weekly reports. * * @return void * * @since 5.3.0 */ public static function generate_weekly_summary_reports() { Notifications::send_weekly_summary_cron(); } /** * Hook method for clearing the reports. * * @return void * * @since 5.0.0 */ public static function clear_reports() { \do_action( 'wsal_clear_reports', array() ); } /** * Hook method for clearing the plugins data. * * @return void * * @since 5.0.0 */ public static function cleanup_hook() { \do_action( 'wsal_cleanup', array() ); } /** * Extend WP cron time intervals for scheduling. * * @param array $schedules - Array of schedules. * * @return array * * @since 5.0.0 */ public static function recurring_schedules( $schedules ) { global $wp_actions; $remove_it = false; // if ( ! isset( $wp_actions['after_setup_theme'] ) ) { // $remove_it = true; // $wp_actions['after_setup_theme'] = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // } $schedules['sixhours'] = array( 'interval' => 21600, 'display' => __( 'Every 6 hours', 'wp-security-audit-log' ), ); $schedules['fortyfiveminutes'] = array( 'interval' => 2700, 'display' => __( 'Every 45 minutes', 'wp-security-audit-log' ), ); $schedules['thirtyminutes'] = array( 'interval' => 1800, 'display' => __( 'Every 30 minutes', 'wp-security-audit-log' ), ); $schedules['fifteenminutes'] = array( 'interval' => 900, 'display' => __( 'Every 15 minutes', 'wp-security-audit-log' ), ); $schedules['tenminutes'] = array( 'interval' => 600, 'display' => __( 'Every 10 minutes', 'wp-security-audit-log' ), ); $schedules['fiveminutes'] = array( 'interval' => 300, 'display' => __( 'Every 5 minutes', 'wp-security-audit-log' ), ); $schedules['oneminute'] = array( 'interval' => 60, 'display' => __( 'Every minute', 'wp-security-audit-log' ), ); $schedules['monthly'] = array( 'interval' => 2635200, 'display' => __( 'Once monthly', 'wp-security-audit-log' ), ); $schedules['quarterly'] = array( 'interval' => 2635200 * 4, 'display' => __( 'Once quarterly', 'wp-security-audit-log' ), ); if ( $remove_it ) { unset( $wp_actions['after_setup_theme'] ); } return $schedules; } /** * Adds a cron job to the stored ones. * * @param array $cron_job - Array with the cron job information. Every cron job information includes 'time', 'hook', 'args', if it is a recurring one - and 'next_run'. * * Example: * 'hook_name' => array( * 'time' => 'monthly', * 'hook' => array( __CLASS_TO_CALL__, 'method_to_call' ), * 'args' => array(), * 'next_run' => '00:00 first day of next month', * ) * . * * @return void * * @throws \InvalidArgumentException When cron job information passed not contains required keys. * * @since 5.0.0 */ public static function store_cron_option( array $cron_job ) { if ( empty( $cron_job ) || 1 < count( $cron_job ) ) { throw new \InvalidArgumentException( __( 'Only one cron at a time', 'wp-security-audit-log' ) ); } $keys = array( 'time', 'hook', 'args', ); if ( count( $keys ) === count( array_filter( array_keys( \reset( $cron_job ) ), function ( $key ) use ( $keys ) { return in_array( $key, $keys, true ); } ) ) ) { $available_cron_jobs = Settings_Helper::get_option_value( self::CRON_JOBS_SETTINGS_NAME, array() ); $available_cron_jobs = array_merge( $available_cron_jobs, $cron_job ); Settings_Helper::set_option_value( self::CRON_JOBS_SETTINGS_NAME, $available_cron_jobs ); } else { throw new \InvalidArgumentException( __( 'Invalid cron job format', 'wp-security-audit-log' ) ); } } /** * Unset cron job from global settings. * * @param string $cron_name - The name of the cron job to remove. * * @return void * * @since 5.0.0 */ public static function remove_cron_option( string $cron_name ) { $available_cron_jobs = Settings_Helper::get_option_value( self::CRON_JOBS_SETTINGS_NAME, array() ); if ( isset( $available_cron_jobs[ $cron_name ] ) ) { \wp_clear_scheduled_hook( $cron_name, $available_cron_jobs[ $cron_name ]['args'] ); unset( $available_cron_jobs[ $cron_name ] ); Settings_Helper::set_option_value( self::CRON_JOBS_SETTINGS_NAME, $available_cron_jobs ); } } /** * Removes event from the cron by given name. * * @param string $event_name -The name of the event. * @param array $args - Arguments passed to the cron event. * * @return void * * @since 4.4.2.1 */ public static function un_schedule_event( string $event_name, array $args = array() ) { $schedule_time = \wp_next_scheduled( $event_name, $args ); if ( $schedule_time ) { \wp_unschedule_event( $schedule_time, $event_name, $args ); } } /** * Returns the next occurrence of the WP start-of-week day as a strtotime-compatible string. * * @param string $time_prefix - The time prefix to use (e.g. '00:00' or '03:00'). * * @return string * * @since 5.6.2 */ private static function get_next_week_start( string $time_prefix = '00:00' ): string { $days = array( 0 => 'sunday', 1 => 'monday', 2 => 'tuesday', 3 => 'wednesday', 4 => 'thursday', 5 => 'friday', 6 => 'saturday', ); $start_of_week = (int) \get_option( 'start_of_week', 1 ); $day_name = $days[ $start_of_week ] ?? 'monday'; return $time_prefix . ' next ' . $day_name; } /** * Unschedules weekly cron jobs so they get recreated with the new start-of-week day. * * @return void * * @since 5.6.2 */ public static function reschedule_weekly_crons() { self::un_schedule_event( 'wsal_periodic_reports_weekly' ); self::un_schedule_event( 'wsal_summary_weekly_report' ); } /** * Initializes the plugin cron jobs. * * @return void * * @since 5.0.0 * @since 5.6.2 Weekly cron jobs respect the WP start_of_week setting. */ public static function initialize_hooks() { $hooks_array = self::CRON_JOBS_NAMES; $hooks_array['wsal_periodic_reports_weekly']['next_run'] = self::get_next_week_start( '00:00' ); $hooks_array['wsal_summary_weekly_report']['next_run'] = self::get_next_week_start( '03:00' ); if ( WP_Helper::is_multisite() || 'free' === \WpSecurityAuditLog::get_plugin_version() ) { /* * Multisite crons are running for every single sub-site instance. This is completely wrong as it leads to multiple reports being generated / fired. For that reason only the main site is allowed to run them, and that is the reason for that code existence. * * Also Free version of the plugin does not need these to be present */ if ( ! \is_main_site() || 'free' === \WpSecurityAuditLog::get_plugin_version() ) { unset( $hooks_array['wsal_generate_reports_cron'], $hooks_array['wsal_periodic_reports_daily'], $hooks_array['wsal_periodic_reports_weekly'], $hooks_array['wsal_periodic_reports_monthly'], $hooks_array['wsal_periodic_reports_quarterly'] ); } if ( 'free' === \WpSecurityAuditLog::get_plugin_version() ) { unset( $hooks_array['wsal_reports_pruning_cron'] ); } } if ( WP_Helper::is_multisite() ) { if ( ! \is_main_site() || 'free' !== \WpSecurityAuditLog::get_plugin_version() ) { $per_site_report = ( isset( Notifications::get_global_notifications_setting()['notification_summary_multisite_individual_site'] ) ? Notifications::get_global_notifications_setting()['notification_summary_multisite_individual_site'] : true ); if ( ! $per_site_report ) { unset( $hooks_array['wsal_summary_daily_report'], $hooks_array['wsal_summary_weekly_report'] ); } } if ( ! \is_main_site() ) { unset( $hooks_array['wsal_cleanup_hook'] ); } } $built_notifications = Settings_Helper::get_option_value( Notifications::BUILT_IN_NOTIFICATIONS_SETTINGS_NAME, array() ); if ( ! isset( $built_notifications['daily_summary_notification'] ) || ! $built_notifications['daily_summary_notification'] ) { unset( $hooks_array['wsal_summary_daily_report'] ); self::un_schedule_event( 'wsal_summary_daily_report' ); } if ( ! isset( $built_notifications['weekly_summary_notification'] ) || ! $built_notifications['weekly_summary_notification'] ) { unset( $hooks_array['wsal_summary_weekly_report'] ); self::un_schedule_event( 'wsal_summary_weekly_report' ); } /** * Gives an option to add hooks which must be enabled. * * @var array - The current hooks. * * @since 5.0.0 */ $hooks_array = \apply_filters( 'wsal_cron_hooks', $hooks_array ); foreach ( $hooks_array as $name => $parameters ) { if ( ! \wp_next_scheduled( $name, ( isset( $parameters['args'] ) ) ? $parameters['args'] : array() ) ) { $time = time(); if ( isset( $parameters['next_run'] ) ) { $ve = get_option( 'gmt_offset' ) > 0 ? ' -' : ' +'; $time = strtotime( $parameters['next_run'] . $ve . get_option( 'gmt_offset' ) . ' HOURS' ); } \wp_schedule_event( $time, ( isset( $parameters['time'] ) ) ? $parameters['time'] : 'daily', $name ); } \add_action( $name, $parameters['hook'] ); } } /** * Legacy cron hook names that may still exist on older installs. * * @since 5.6.2 */ public const LEGACY_CRON_HOOKS = array( 'wsal_cleanup', 'wsal_delete_logins', 'wsal_daily_summary_report', 'wsal_auto_destroy_sessions', ); /** * Extension cron hooks registered via the wsal_cron_hooks filter. * * These cannot be discovered at deactivation time because the filter * callbacks may not be loaded yet, so they are listed here explicitly. * * @since 5.6.2 */ public const EXTENSION_CRON_HOOKS = array( 'wsal_run_archiving', ); /** * Unschedules all WSAL cron jobs. Called on plugin deactivation. * * Clears all hooks from the CRON_JOBS_NAMES constant, any dynamically * registered hooks stored in the cron_jobs_options setting, extension * hooks registered via filter, and legacy hooks that may still exist * on older installs. * * @return void * * @since 5.6.2 */ public static function single_site_unschedule_all_cron_jobs() { $hooks = array_keys( self::CRON_JOBS_NAMES ); $dynamic_hooks = Settings_Helper::get_option_value( self::CRON_JOBS_SETTINGS_NAME, array() ); if ( ! empty( $dynamic_hooks ) ) { $hooks = array_merge( $hooks, array_keys( $dynamic_hooks ) ); } $hooks = array_merge( $hooks, self::LEGACY_CRON_HOOKS ); $hooks = array_merge( $hooks, self::EXTENSION_CRON_HOOKS ); $hooks = array_merge( $hooks, array_keys( MainWP_Helper::CRON_JOBS ) ); foreach ( $hooks as $hook_name ) { \wp_clear_scheduled_hook( $hook_name ); } } /** * Unschedules all WSAL cron jobs across all sites in a multisite network. Called on plugin deactivation in a multisite context. * * @return void * * @since 5.6.2 */ public static function multisite_unschedule_all_cron_jobs() { // By default get_sites only returns 100 sites, so we need to set 'number' to 0 to get all of them. $sites = \get_sites( array( 'fields' => 'ids', 'number' => 0, ) ); foreach ( $sites as $site_id ) { \switch_to_blog( $site_id ); self::single_site_unschedule_all_cron_jobs(); \restore_current_blog(); } } /** * Unschedules all WSAL cron jobs. Routes to single-site or multisite cleanup based on context. * * @param bool $network_deactivating - Whether the plugin is being deactivated network-wide. * * @return void * * @since 5.6.2 */ public static function unschedule_all_cron_jobs( $network_deactivating = false ) { if ( $network_deactivating && WP_Helper::is_multisite() ) { self::multisite_unschedule_all_cron_jobs(); } else { self::single_site_unschedule_all_cron_jobs(); } } } }