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 / runner.php

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

303 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Automatic Scanning runner.
4 *
5 * Executes a scheduled scan by reusing the existing scan engine: it never
6 * scans itself, it only decides when to fire `surecookie_start_site_scanning`
7 * and which pages to scan (scope). It also marks the in-flight scan as
8 * auto-triggered so the change-detection recorder can label scan history.
9 *
10 * @package SureCookie\Inc\Modules\AutomaticScanning
11 * @since 1.2.0
12 */
13
14 namespace SureCookie\Inc\Modules\AutomaticScanning;
15
16 use SureCookie\Inc\Functions\Get;
17 use SureCookie\Inc\Functions\Settings;
18 use SureCookie\Inc\Modules\SiteScanner\SaasClient;
19 use SureCookie\Inc\Modules\SiteScanner\Utils;
20 use SureCookie\Inc\Traits\GetInstance;
21 use SureCookie\Inc\Utils\Logger;
22
23 if ( ! defined( 'ABSPATH' ) ) {
24 exit; // Exit if accessed directly.
25 }
26
27 /**
28 * Runner
29 *
30 * @since 1.2.0
31 */
32 class Runner {
33 use GetInstance;
34
35 /**
36 * Transient that marks the in-flight scan as auto-triggered.
37 *
38 * Read by the change-detection recorder when `surecookie_scan_completed`
39 * fires (minutes later, across cron polling requests) to set the scan
40 * history trigger type.
41 *
42 * @since 1.2.0
43 */
44 public const ACTIVE_TRANSIENT = 'surecookie_auto_scan_active';
45
46 /**
47 * One-off retry hook used when the SaaS defers a scheduled scan because the
48 * shared daily page budget is exhausted. Distinct from the recurring
49 * Scheduler::RUN_HOOK so the daily retry can be scheduled/queried without
50 * colliding with the weekly/monthly cadence.
51 *
52 * @since 1.2.0
53 */
54 public const RETRY_HOOK = 'surecookie_auto_scan_retry';
55
56 /**
57 * Transient guarding against two overlapping cron ticks both starting a scan.
58 *
59 * @since 1.2.0
60 */
61 private const LOCK_TRANSIENT = 'surecookie_auto_scan_lock';
62
63 /**
64 * Whether the current request is executing an automatic (scheduled) run.
65 *
66 * @var bool
67 * @since 1.2.0
68 */
69 private $is_auto_run = false;
70
71 /**
72 * Constructor.
73 *
74 * @since 1.2.0
75 */
76 private function __construct() {
77 add_action( Scheduler::RUN_HOOK, [ $this, 'run_scheduled_scan' ] );
78 add_action( self::RETRY_HOOK, [ $this, 'run_scheduled_scan' ] );
79 add_action( 'surecookie_scan_deferred', [ $this, 'handle_deferred_scan' ] );
80 add_filter( 'surecookie_scanner_page_urls_to_scan', [ $this, 'apply_scope' ] );
81 }
82
83 /**
84 * Reschedule a deferred scheduled scan for ~24h out.
85 *
86 * The SaaS defers (rather than partial-runs) a scheduled scan when the
87 * license's shared daily page budget can't fit it today. Retrying daily -
88 * instead of waiting for the next weekly/monthly cycle - means the scan runs
89 * as soon as the budget resets. Nothing actually scanned, so drop the
90 * auto-run marker to avoid mislabeling a later manual scan.
91 *
92 * @since 1.2.0
93 * @return void
94 */
95 public function handle_deferred_scan(): void {
96 delete_transient( self::ACTIVE_TRANSIENT );
97
98 if ( ! wp_next_scheduled( self::RETRY_HOOK ) ) {
99 wp_schedule_single_event( time() + DAY_IN_SECONDS, self::RETRY_HOOK );
100 Logger::get_instance()->save_log( __( 'Automatic scan deferred: daily page budget reached. Retrying in ~24 hours.', 'surecookie' ) );
101 }
102 }
103
104 /**
105 * Run a scheduled scan by firing the existing scan trigger.
106 *
107 * @since 1.2.0
108 * @return void
109 */
110 public function run_scheduled_scan(): void {
111 if ( ! (bool) Settings::get( 'auto_scan_enabled' ) ) {
112 return;
113 }
114
115 // Local/dev hosts can't be reached by the cloud scanner - skip before
116 // touching transients, analytics flags, or logs so a local site doesn't
117 // churn failure entries on every scheduled tick. SaasClient::start_scan()
118 // re-checks this as the last line of defense.
119 if ( SaasClient::is_local_site() ) {
120 return;
121 }
122
123 if ( SaasClient::get_instance()->is_scan_in_progress() ) {
124 Logger::get_instance()->save_log( __( 'Automatic scan skipped: a scan is already in progress.', 'surecookie' ) );
125 return;
126 }
127
128 // Short lock so two near-simultaneous cron ticks can't both start a scan.
129 if ( get_transient( self::LOCK_TRANSIENT ) ) {
130 return;
131 }
132 set_transient( self::LOCK_TRANSIENT, 1, 5 * MINUTE_IN_SECONDS );
133
134 // Mark this scan as auto-triggered for the completion recorder (TTL > scan ceiling).
135 set_transient( self::ACTIVE_TRANSIENT, 1, 45 * MINUTE_IN_SECONDS );
136
137 // Start this run with a clean log, exactly as the manual scan entry point
138 // does. Deliberately after the in-progress and lock guards so a duplicate
139 // cron tick can never wipe the log of the scan that is already running.
140 Logger::get_instance()->cleanup_logs();
141
142 Logger::get_instance()->save_log( __( 'Starting automatic (scheduled) scan.', 'surecookie' ) );
143
144 // Analytics: flag the first automatic (scheduled) scan run + its cadence so
145 // the state-based tracker can emit `first_auto_scan_started`.
146 if ( ! get_option( 'surecookie_first_auto_scan_started_flag', false ) ) {
147 update_option( 'surecookie_first_auto_scan_started_flag', true, false );
148 update_option( 'surecookie_first_auto_scan_frequency', (string) Settings::get( 'auto_scan_frequency' ), false );
149 }
150
151 $this->is_auto_run = true;
152 // Request-scoped signal so the SaaS client tags only this scan-start as
153 // 'scheduled'. Avoids the ACTIVE_TRANSIENT (45-min TTL) mislabeling a
154 // later manual scan if an auto-run crashes before cleanup.
155 add_filter( 'surecookie_saas_scan_trigger', [ $this, 'tag_scheduled' ] );
156
157 try {
158 do_action( 'surecookie_start_site_scanning' );
159 } catch ( \Throwable $e ) {
160 // Scan never started - drop the active marker so a later manual scan isn't mislabeled.
161 delete_transient( self::ACTIVE_TRANSIENT );
162 Logger::get_instance()->save_log( 'Automatic scan failed to start: ' . $e->getMessage() );
163 } finally {
164 remove_filter( 'surecookie_saas_scan_trigger', [ $this, 'tag_scheduled' ] );
165 $this->is_auto_run = false;
166 delete_transient( self::LOCK_TRANSIENT );
167 }
168 }
169
170 /**
171 * Filter callback: tag the in-flight SaaS scan request as scheduled.
172 *
173 * @since 1.2.0
174 * @return string
175 */
176 public function tag_scheduled(): string {
177 return 'scheduled';
178 }
179
180 /**
181 * Apply the configured page scope to automatic runs only.
182 *
183 * Manual scans are never altered.
184 *
185 * @param array<int, string> $urls Page URLs resolved from the manual selection.
186 * @since 1.2.0
187 * @return array<int, string>
188 */
189 public function apply_scope( $urls = [] ) {
190 if ( ! $this->is_auto_run ) {
191 return $urls;
192 }
193
194 $scope = (string) Settings::get( 'auto_scan_scope' );
195
196 if ( $scope === 'all_published' ) {
197 $resolved = $this->all_published_urls();
198 return ! empty( $resolved ) ? $resolved : $urls;
199 }
200
201 if ( $scope === 'selected' ) {
202 $selected = $this->selected_pages_urls();
203 return ! empty( $selected ) ? $selected : $urls;
204 }
205
206 return $urls; // same_as_manual.
207 }
208
209 /**
210 * Permalinks of published content, capped to the plan's page limit.
211 *
212 * Covers the `post` + `page` baseline plus any custom post types opted into
213 * the scanner via the `surecookie_searchable_post_types` filter, so "All
214 * Published Content" stays in sync with what the manual picker can select.
215 *
216 * @since 1.2.0
217 * @return array<int, string>
218 */
219 private function all_published_urls(): array {
220 $max = max( 1, (int) Utils::get_max_scan_pages() );
221
222 $post_types = array_values(
223 array_unique(
224 array_merge( [ 'post', 'page' ], Get::searchable_post_types( 'scanner' ) )
225 )
226 );
227
228 $query = new \WP_Query(
229 [
230 'post_type' => $post_types,
231 'post_status' => 'publish',
232 'posts_per_page' => $max,
233 'fields' => 'ids',
234 'orderby' => 'date',
235 'order' => 'DESC',
236 'no_found_rows' => true,
237 'update_post_term_cache' => false, // Skip term cache for performance.
238 'update_post_meta_cache' => false, // Skip meta cache for performance.
239 ]
240 );
241
242 // WP_Query returns int IDs here (fields => 'ids'), but its typed return is
243 // int|WP_Post; normalize each entry to an int ID so ids_to_permalinks() receives int[].
244 $ids = array_map(
245 static function ( $post ) {
246 return $post instanceof \WP_Post ? $post->ID : (int) $post;
247 },
248 is_array( $query->posts ) ? $query->posts : []
249 );
250
251 return $this->ids_to_permalinks( $ids );
252 }
253
254 /**
255 * Permalinks from the auto_scan_pages selection.
256 *
257 * @since 1.2.0
258 * @return array<int, string>
259 */
260 private function selected_pages_urls(): array {
261 $pages = Settings::get( 'auto_scan_pages' );
262
263 if ( ! is_array( $pages ) ) {
264 return [];
265 }
266
267 $ids = [];
268
269 foreach ( $pages as $page ) {
270 $id = is_array( $page ) ? absint( $page['value'] ?? 0 ) : absint( $page );
271 if ( $id > 0 ) {
272 $ids[] = $id;
273 }
274 }
275
276 return $this->ids_to_permalinks( $ids );
277 }
278
279 /**
280 * Resolve published post IDs to permalinks.
281 *
282 * @param array<int, int> $ids Post IDs.
283 * @since 1.2.0
284 * @return array<int, string>
285 */
286 private function ids_to_permalinks( array $ids ): array {
287 $urls = [];
288
289 foreach ( $ids as $id ) {
290 $post = get_post( (int) $id );
291
292 if ( $post && $post->post_status === 'publish' ) {
293 $permalink = get_permalink( (int) $id );
294 if ( $permalink ) {
295 $urls[] = $permalink;
296 }
297 }
298 }
299
300 return $urls;
301 }
302 }
303