PluginProbe
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking / trunk
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking vtrunk
1.5.0 1.4.0 1.3.0 1.3.1 trunk 0.0.0-alpha.1 0.0.0-alpha.2 0.0.0-alpha.3 0.0.1-beta.1 0.0.1-beta.2 0.0.1-beta.3 0.0.1-beta.4 1.0.0 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4
surecookie / inc / modules / automatic-scanning / scheduler.php

scheduler.php in SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking trunk, at inc/modules/automatic-scanning/scheduler.php

239 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Automatic Scanning scheduler.
4 *
5 * Registers and maintains the recurring WP-Cron event that drives unattended
6 * scans. Free ships the Monthly frequency; Pro adds Weekly to the allowlist.
7 * The allowlist is enforced server-side so a Free user can never run Weekly.
8 *
9 * @package SureCookie\Inc\Modules\AutomaticScanning
10 * @since 1.2.0
11 */
12
13 namespace SureCookie\Inc\Modules\AutomaticScanning;
14
15 use SureCookie\Inc\Functions\Settings;
16 use SureCookie\Inc\Traits\GetInstance;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit; // Exit if accessed directly.
20 }
21
22 /**
23 * Scheduler
24 *
25 * @since 1.2.0
26 */
27 class Scheduler {
28 use GetInstance;
29
30 /**
31 * Recurring cron hook that runs an automatic scan.
32 *
33 * @since 1.2.0
34 */
35 public const RUN_HOOK = 'surecookie_auto_scan_run';
36
37 /**
38 * Custom cron schedule key for the monthly cadence.
39 *
40 * @since 1.2.0
41 */
42 private const MONTHLY_SCHEDULE = 'surecookie_monthly';
43
44 /**
45 * Custom cron schedule key for the weekly cadence (Pro frequency).
46 *
47 * @since 1.2.0
48 */
49 private const WEEKLY_SCHEDULE = 'surecookie_weekly';
50
51 /**
52 * Constructor.
53 *
54 * @since 1.2.0
55 */
56 private function __construct() {
57 add_filter( 'cron_schedules', [ $this, 'register_schedules' ] ); // phpcs:ignore WordPress.WP.CronInterval.ChangeDetected -- Weekly/monthly cadences are well above the 15-minute floor.
58
59 // Reschedule the moment settings are saved - this fires after the option is
60 // written, so it reconciles against the new frequency. admin_init is a
61 // self-heal fallback (re-creates a lost event); scheduling is admin-driven,
62 // so no front-end reconcile is needed.
63 add_action( 'update_option_' . SURECOOKIE_SETTINGS_OPTION, [ $this, 'maybe_schedule' ] );
64 add_action( 'admin_init', [ $this, 'maybe_schedule' ] );
65
66 // Enforce the frequency allowlist server-side so a disallowed value (e.g. Weekly on Free) reverts to Monthly on save.
67 add_filter( 'pre_update_option_' . SURECOOKIE_SETTINGS_OPTION, [ $this, 'gate_frequency' ] );
68 }
69
70 /**
71 * Register the weekly and monthly cron intervals (not provided by core).
72 *
73 * @param array<string, array{interval:int, display:string}> $schedules Existing schedules.
74 * @since 1.2.0
75 * @return array<string, array{interval:int, display:string}>
76 */
77 public function register_schedules( $schedules = [] ) {
78 if ( ! is_array( $schedules ) ) {
79 $schedules = [];
80 }
81
82 if ( ! isset( $schedules[ self::MONTHLY_SCHEDULE ] ) ) {
83 $schedules[ self::MONTHLY_SCHEDULE ] = [
84 'interval' => MONTH_IN_SECONDS,
85 'display' => __( 'Once Monthly (SureCookie)', 'surecookie' ),
86 ];
87 }
88
89 if ( ! isset( $schedules[ self::WEEKLY_SCHEDULE ] ) ) {
90 $schedules[ self::WEEKLY_SCHEDULE ] = [
91 'interval' => WEEK_IN_SECONDS,
92 'display' => __( 'Once Weekly (SureCookie)', 'surecookie' ),
93 ];
94 }
95
96 return $schedules;
97 }
98
99 /**
100 * Allowed scan frequencies. Pro adds 'weekly' only when licensed.
101 *
102 * @since 1.2.0
103 * @return array<int, string>
104 */
105 public static function allowed_frequencies(): array {
106 /**
107 * Filters the scan frequencies available for Automatic Scanning.
108 *
109 * Free exposes only Monthly. Pro adds 'weekly' (when licensed) - the
110 * Weekly option is otherwise locked in the UI and rejected on save.
111 *
112 * @since 1.2.0
113 * @param array<int, string> $frequencies Allowed frequency keys.
114 */
115 $allowed = apply_filters( 'surecookie_auto_scan_frequencies', [ 'monthly' ] );
116
117 if ( ! is_array( $allowed ) || empty( $allowed ) ) {
118 return [ 'monthly' ];
119 }
120
121 return array_values( array_unique( array_map( 'strval', $allowed ) ) );
122 }
123
124 /**
125 * Resolve the effective frequency, clamped to the allowlist.
126 *
127 * @since 1.2.0
128 * @return string
129 */
130 public static function effective_frequency(): string {
131 $frequency = (string) Settings::get( 'auto_scan_frequency' );
132 $allowed = self::allowed_frequencies();
133
134 return in_array( $frequency, $allowed, true ) ? $frequency : ( $allowed[0] ?? 'monthly' );
135 }
136
137 /**
138 * Ensure the recurring event matches the current settings.
139 *
140 * @since 1.2.0
141 * @return void
142 */
143 public function maybe_schedule(): void {
144 if ( ! (bool) Settings::get( 'auto_scan_enabled' ) ) {
145 $this->unschedule();
146 return;
147 }
148
149 $desired = $this->schedule_key( self::effective_frequency() );
150 $current = wp_get_schedule( self::RUN_HOOK );
151
152 if ( $current === $desired ) {
153 return; // Already scheduled with the correct recurrence.
154 }
155
156 // Recurrence changed (or not scheduled) - reset to the desired cadence,
157 // anchored to an off-peak night slot in the site's timezone.
158 $this->unschedule();
159 wp_schedule_event( $this->first_run_timestamp(), $desired, self::RUN_HOOK );
160 }
161
162 /**
163 * Unschedule all instances of the recurring event.
164 *
165 * @since 1.2.0
166 * @return void
167 */
168 public function unschedule(): void {
169 wp_unschedule_hook( self::RUN_HOOK );
170 }
171
172 /**
173 * Clamp a disallowed frequency to the first allowed value before save.
174 *
175 * @param mixed $value Incoming settings option value.
176 * @since 1.2.0
177 * @return mixed
178 */
179 public function gate_frequency( $value = null ) {
180 if ( is_array( $value ) && isset( $value['auto_scan_frequency'] ) ) {
181 $allowed = self::allowed_frequencies();
182
183 if ( ! in_array( (string) $value['auto_scan_frequency'], $allowed, true ) ) {
184 $value['auto_scan_frequency'] = $allowed[0] ?? 'monthly';
185 }
186 }
187
188 return $value;
189 }
190
191 /**
192 * Timestamp of the next scheduled automatic scan (0 when not scheduled).
193 *
194 * @since 1.2.0
195 * @return int
196 */
197 public static function next_run(): int {
198 $timestamp = wp_next_scheduled( self::RUN_HOOK );
199
200 return $timestamp ? (int) $timestamp : 0;
201 }
202
203 /**
204 * Map a frequency to a registered cron schedule key.
205 *
206 * @param string $frequency Frequency key.
207 * @since 1.2.0
208 * @return string
209 */
210 private function schedule_key( string $frequency ): string {
211 return $frequency === 'weekly' ? self::WEEKLY_SCHEDULE : self::MONTHLY_SCHEDULE;
212 }
213
214 /**
215 * Timestamp of the first run: a random slot inside the 01:00–05:00 window in
216 * the SITE's timezone (the next night; tonight if it hasn't passed yet).
217 *
218 * The random minute staggers sites across the 4-hour window so they don't all
219 * hit the SaaS at once (no thundering herd), and it avoids the UTC-midnight
220 * daily-budget reset boundary. The weekly/monthly recurrence then preserves
221 * this night-time slot. Returns a non-immediate time, so enabling the feature
222 * never triggers an instant scan.
223 *
224 * @since 1.2.0
225 * @return int UTC timestamp for wp_schedule_event().
226 */
227 private function first_run_timestamp(): int {
228 $now = new \DateTimeImmutable( 'now', wp_timezone() );
229 $slot = $now->setTime( 0, 0, 0 )
230 ->modify( '+' . wp_rand( 1 * HOUR_IN_SECONDS, ( 5 * HOUR_IN_SECONDS ) - 1 ) . ' seconds' );
231
232 if ( $slot <= $now ) {
233 $slot = $slot->modify( '+1 day' ); // Tonight's window already passed → tomorrow.
234 }
235
236 return $slot->getTimestamp();
237 }
238 }
239