MINUTE_IN_SECONDS, 'display' => __( 'Every minute (SiteAuditHub active scans only)', 'siteaudithub' ), ); return $schedules; } /** * Queue runner. */ public static function run_queue() { Scanner::process_queue( (int) Settings::get( 'time_budget' ) ); } /** * Ensure the hourly maintenance event exists (no duplicates). */ public static function ensure_schedules() { if ( ! wp_next_scheduled( self::HOURLY_HOOK ) ) { wp_schedule_event( time() + 300, 'hourly', self::HOURLY_HOOK ); } } /** * Ensure the minute queue event exists while scans are active. */ public static function ensure_queue() { if ( ! wp_next_scheduled( self::QUEUE_HOOK ) ) { wp_schedule_event( time(), 'siteaudithub_every_minute', self::QUEUE_HOOK ); } } /** * Remove the queue event (no active scans). */ public static function clear_queue() { wp_clear_scheduled_hook( self::QUEUE_HOOK ); } /** * Remove all plugin events. */ public static function clear_all() { wp_clear_scheduled_hook( self::QUEUE_HOOK ); wp_clear_scheduled_hook( self::HOURLY_HOOK ); } /** * Schedule state. * * @return array */ public static function state() { $state = get_option( self::STATE, array() ); return is_array( $state ) ? $state : array(); } /** * Interval in seconds for a frequency. * * @param string $freq Frequency. * @return int */ private static function interval( $freq ) { $map = array( 'daily' => DAY_IN_SECONDS, 'weekly' => WEEK_IN_SECONDS, 'monthly' => 30 * DAY_IN_SECONDS, ); return isset( $map[ $freq ] ) ? $map[ $freq ] : 0; } /** * Whether a scheduled job is due. * * @param string $freq Frequency. * @param int $last Last run timestamp. * @return bool */ public static function is_due( $freq, $last ) { $interval = self::interval( $freq ); if ( ! $interval ) { return false; } $hour = (int) current_time( 'G' ); return ( time() - (int) $last ) >= ( $interval - HOUR_IN_SECONDS ) && $hour >= (int) Settings::get( 'schedule_hour' ); } /** * Next due time (approximate, for display). * * @param string $freq Frequency. * @param int $last Last run. * @return int|null */ public static function next_due( $freq, $last ) { $interval = self::interval( $freq ); return $interval ? max( time(), (int) $last + $interval - HOUR_IN_SECONDS ) : null; } /** * Hourly maintenance. */ public static function hourly() { $state = self::state(); foreach ( array( 'site', 'integrity' ) as $type ) { $freq = (string) Settings::get( 'schedule_' . $type ); $last = isset( $state[ $type . '_last' ] ) ? (int) $state[ $type . '_last' ] : 0; if ( 'off' !== $freq && self::is_due( $freq, $last ) ) { $result = Scanner::start( $type, 'scheduled' ); $state[ $type . '_last' ] = time(); if ( is_wp_error( $result ) ) { Logger::log( 'warning', sprintf( 'Scheduled %s scan not started: %s', $type, $result->get_error_message() ) ); } } } $summary_last = isset( $state['summary_last'] ) ? (int) $state['summary_last'] : 0; if ( (int) Settings::get( 'weekly_summary' ) && (int) Settings::get( 'notify_enabled' ) && self::is_due( 'weekly', $summary_last ) ) { Notifications::weekly_summary(); $state['summary_last'] = time(); } $cleanup_last = isset( $state['cleanup_last'] ) ? (int) $state['cleanup_last'] : 0; if ( time() - $cleanup_last > DAY_IN_SECONDS - HOUR_IN_SECONDS ) { self::retention(); Login_Log::prune(); $state['cleanup_last'] = time(); } update_option( self::STATE, $state, false ); if ( Repository::runnable_scans() ) { self::ensure_queue(); } } /** * Delete scans beyond the retention limits (bounded per run). * * @param int $max Max scans to delete in this run. * @return int Deleted. */ public static function retention( $max = 5 ) { $keep = (int) Settings::get( 'retention_scans' ); $days = (int) Settings::get( 'retention_days' ); $cutoff = gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS ); $deleted = 0; foreach ( array( 'site', 'integrity' ) as $type ) { $latest = Repository::latest_completed( $type ); $scans = Repository::list_scans( 500, 0, $type ); $index = 0; foreach ( $scans as $scan ) { ++$index; if ( in_array( $scan->status, Repository::ACTIVE_STATUSES, true ) || ( $latest && $latest->id === $scan->id ) ) { continue; } if ( $index > $keep || $scan->created_at < $cutoff ) { Repository::delete_scan( $scan->id ); ++$deleted; if ( $deleted >= $max ) { return $deleted; } } } } if ( $deleted ) { Logger::log( 'info', sprintf( 'Retention removed %d old scan(s).', $deleted ) ); } return $deleted; } }