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 / site-scanner / api.php

api.php in SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking trunk, at inc/modules/site-scanner/api.php

382 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Site Scanner API class
4 *
5 * Handles site scanning related REST API endpoints.
6 *
7 * @package SureCookie\Inc\Modules\SiteScanner
8 * @since 0.0.1
9 */
10
11 namespace SureCookie\Inc\Modules\SiteScanner;
12
13 use SureCookie\Inc\API\Base;
14 use SureCookie\Inc\Functions\Helper;
15 use SureCookie\Inc\Functions\Sanitize;
16 use SureCookie\Inc\Functions\SendJson;
17 use SureCookie\Inc\Functions\Settings;
18 use SureCookie\Inc\Functions\Update;
19 use SureCookie\Inc\Traits\GetInstance;
20 use SureCookie\Inc\Utils\Logger;
21 use WP_REST_Server;
22
23 if ( ! defined( 'ABSPATH' ) ) {
24 exit; // Exit if accessed directly.
25 }
26
27 /**
28 * Class Api
29 *
30 * @package SureCookie\Inc\Modules\SiteScanner
31 * @since 0.0.1
32 */
33 class Api extends Base {
34 use GetInstance;
35
36 /**
37 * Route for initiating site-scanning (starts SaaS scan).
38 */
39 protected const INITIATE_SCAN = '/site-scanner/initiate';
40
41 /**
42 * Route for getting scan status.
43 */
44 protected const SCAN_STATUS = '/site-scanner/status';
45
46 /**
47 * Route for getting scan logs.
48 */
49 protected const GET_LOGS = '/site-scanner/get-logs';
50
51 /**
52 * Route for cancelling a scan.
53 */
54 protected const CANCEL_SCAN = '/site-scanner/cancel';
55
56 /**
57 * Route for fetching the daily scan quota.
58 *
59 * @since 0.0.1-beta.2
60 */
61 protected const QUOTA = '/site-scanner/quota';
62
63 /**
64 * Re-run domain verification after the user publishes the DNS record.
65 *
66 * @since 1.5.0
67 */
68 protected const VERIFY_RETRY = '/site-scanner/verify-retry';
69
70 /**
71 * Register API routes.
72 *
73 * @since 0.0.1
74 * @return void
75 */
76 public function register_routes(): void {
77 // Start scan endpoint (triggers SaaS API).
78 register_rest_route(
79 $this->get_api_namespace(),
80 self::INITIATE_SCAN,
81 [
82 'methods' => WP_REST_Server::CREATABLE,
83 'callback' => [ $this, 'start_scan' ],
84 'permission_callback' => [ $this, 'validate_permission' ],
85 'args' => [
86 'pages' => [
87 'type' => 'array',
88 // Not rest_validate_request_arg(): its rest_is_array() runs a scalar
89 // through wp_parse_list() first, so "foo" would pass as [ "foo" ]. No
90 // `default` either, or an omitted `pages` would wipe the stored selection.
91 'validate_callback' => static function ( $value ) {
92 return is_array( $value );
93 },
94 ],
95 ],
96 ]
97 );
98
99 // Retry verification only, without re-registering: step 1 would mint a new
100 // token and invalidate the DNS record the user has just published.
101 register_rest_route(
102 $this->get_api_namespace(),
103 self::VERIFY_RETRY,
104 [
105 'methods' => WP_REST_Server::CREATABLE,
106 'callback' => [ $this, 'retry_verification' ],
107 'permission_callback' => [ $this, 'validate_permission' ],
108 ]
109 );
110
111 // Scan progress logs getting endpoint.
112 register_rest_route(
113 $this->get_api_namespace(),
114 self::GET_LOGS,
115 [
116 'methods' => WP_REST_Server::READABLE,
117 'callback' => [ $this, 'get_scan_progress_logs' ],
118 'permission_callback' => [ $this, 'validate_permission' ],
119 ]
120 );
121
122 // Scan status endpoint.
123 register_rest_route(
124 $this->get_api_namespace(),
125 self::SCAN_STATUS,
126 [
127 'methods' => WP_REST_Server::READABLE,
128 'callback' => [ $this, 'get_scan_status' ],
129 'permission_callback' => [ $this, 'validate_permission' ],
130 ]
131 );
132
133 // Cancel scan endpoint.
134 register_rest_route(
135 $this->get_api_namespace(),
136 self::CANCEL_SCAN,
137 [
138 'methods' => WP_REST_Server::CREATABLE,
139 'callback' => [ $this, 'cancel_scan' ],
140 'permission_callback' => [ $this, 'validate_permission' ],
141 ]
142 );
143
144 // Quota endpoint - returns the cached daily scan quota; ?refresh=1 forces a SaaS round-trip.
145 register_rest_route(
146 $this->get_api_namespace(),
147 self::QUOTA,
148 [
149 'methods' => WP_REST_Server::READABLE,
150 'callback' => [ $this, 'get_quota' ],
151 'permission_callback' => [ $this, 'validate_permission' ],
152 'args' => [
153 'refresh' => [
154 'type' => 'boolean',
155 'default' => false,
156 'sanitize_callback' => 'rest_sanitize_boolean',
157 ],
158 ],
159 ]
160 );
161 }
162
163 /**
164 * Get current scan status.
165 *
166 * @param \WP_REST_Request<array<string, mixed>> $request REST API request object.
167 * @since 0.0.1
168 * @return void
169 */
170 public function get_scan_status( $request ): void {
171 $cron = Cron::get_instance();
172 $status = $cron->get_scan_status();
173
174 SendJson::success( [ 'data' => $status ] );
175 }
176
177 /**
178 * Get scan progress logs.
179 *
180 * @param \WP_REST_Request<array<string, mixed>> $request REST API request object.
181 * @since 0.0.1
182 * @return void
183 */
184 public function get_scan_progress_logs( $request ): void {
185 $logs = Logger::get_instance()->get_log();
186 SendJson::success( [ 'data' => $logs ] );
187 }
188
189 /**
190 * Get the daily scan quota - cache-first, with `?refresh=1` busting the cache.
191 *
192 * @param \WP_REST_Request<array<string, mixed>> $request REST API request object.
193 * @since 0.0.1-beta.2
194 * @return void
195 */
196 public function get_quota( $request ): void {
197 $saas_client = SaasClient::get_instance();
198 $refresh = (bool) $request->get_param( 'refresh' );
199
200 if ( ! $refresh ) {
201 $cached = $saas_client->get_cached_quota();
202 if ( ! empty( $cached ) ) {
203 // Strip the `_plan` sentinel before returning the quota payload -
204 // SaaS-reported plan goes back as a sibling, not nested in quota.
205 $cached_plan = $saas_client->get_cached_plan();
206 unset( $cached['_plan'] );
207 SendJson::success(
208 [
209 'data' => [
210 'quota' => $cached,
211 'plan' => $cached_plan !== '' ? $cached_plan : Utils::get_plan(),
212 'fresh' => false,
213 ],
214 ]
215 );
216 return;
217 }
218 }
219
220 // Refresh requested OR cold start: hit SaaS and surface error_code/message
221 // so the frontend can distinguish auth failure from a transient outage.
222 $result = $saas_client->get_quota();
223 SendJson::success(
224 [
225 'data' => [
226 'quota' => $result['quota'] ?? [],
227 'plan' => $result['plan'] ?? Utils::get_plan(),
228 'fresh' => ! empty( $result['success'] ),
229 'error_code' => $result['error_code'] ?? null,
230 'message' => empty( $result['success'] ) ? ( $result['message'] ?? null ) : null,
231 ],
232 ]
233 );
234 }
235
236 /**
237 * Cancel the current scan.
238 *
239 * @param \WP_REST_Request<array<string, mixed>> $request REST API request object.
240 * @since 0.0.1
241 * @return void
242 */
243 public function cancel_scan( $request ): void {
244 $cron = Cron::get_instance();
245 $cancelled = $cron->cancel_scan();
246
247 if ( $cancelled ) {
248 SendJson::success(
249 [
250 'message' => __( 'Scan cancelled successfully.', 'surecookie' ),
251 ]
252 );
253 } else {
254 SendJson::error(
255 [
256 'message' => __( 'No active scan to cancel.', 'surecookie' ),
257 ]
258 );
259 }
260 }
261
262 /**
263 * Generate sitemap cache (cron-based).
264 *
265 * Uses SaaS API for cookie scanning.
266 *
267 * @param \WP_REST_Request<array<string, mixed>> $request REST API request object.
268 * @since 0.0.1
269 * @return void
270 */
271 public function start_scan( $request ): void {
272 try {
273 $pages = $request->get_param( 'pages' );
274
275 // Persist only when a selection was supplied; absent reuses the stored one.
276 // Never hand Sanitize::settings() a non-array: its [] return would erase the option.
277 if ( is_array( $pages ) ) {
278 Update::option( SURECOOKIE_SETTINGS_OPTION, Sanitize::settings( [ 'scan_pages' => $pages ] ) );
279 }
280
281 // Check if scan is already in progress.
282 $saas_client = SaasClient::get_instance();
283
284 if ( $saas_client->is_scan_in_progress() ) {
285 SendJson::error(
286 [
287 'code' => 'scan_in_progress',
288 'message' => __( 'A scan is already in progress. Please wait for it to complete.', 'surecookie' ),
289 ]
290 );
291 return;
292 }
293
294 // Cleanup old logs to save new ones.
295 Logger::get_instance()->cleanup_logs();
296
297 // Get pages to scan and validate before starting.
298 $pages_to_scan = Cron::get_instance()->get_pages_urls_to_scan();
299
300 if ( empty( $pages_to_scan ) ) {
301 SendJson::error(
302 [
303 'code' => 'no_pages',
304 'message' => __( 'No pages selected for scanning.', 'surecookie' ),
305 ]
306 );
307 return;
308 }
309
310 // Start scan via SaaS API directly to catch errors.
311 $saas_client = SaasClient::get_instance();
312 $result = $saas_client->start_scan( $pages_to_scan );
313
314 if ( ! $result['success'] ) {
315 // Return error with code and rate limit details from SaaS response.
316 SendJson::error(
317 [
318 'code' => $result['code'] ?? 'scan_failed',
319 'message' => $result['message'] ?? __( 'Failed to start scan.', 'surecookie' ),
320 'limit' => $result['limit'] ?? null,
321 'used' => $result['used'] ?? null,
322 'remaining' => $result['remaining'] ?? 0,
323 // Registration can fail with a DNS fallback the user can act on.
324 'dns_verification' => $result['dns_verification'] ?? null,
325 ]
326 );
327 return;
328 }
329
330 SendJson::success(
331 [
332 'message' => __( 'Cookie scan has been initiated.', 'surecookie' ),
333 'description' => __( 'The scan is running. You will be notified when it completes.', 'surecookie' ),
334 // Surfaced so the UI can tell the user some pages were skipped
335 // because the shared daily page budget ran out (auto-trim).
336 'pages_scanned' => $result['pages_scanned'] ?? null,
337 'pages_dropped' => $result['pages_dropped'] ?? 0,
338 ]
339 );
340 } catch ( \Exception $e ) {
341 SendJson::error(
342 [
343 'message' => sprintf(
344 /* translators: %s: Error message */
345 __( 'Failed to start site scanner: %s', 'surecookie' ),
346 $e->getMessage(),
347 ),
348 ]
349 );
350 }
351 }
352
353 /**
354 * Re-run step 2 of registration against the token already issued.
355 *
356 * @since 1.5.0
357 * @return void
358 */
359 public function retry_verification(): void {
360 $result = SaasClient::get_instance()->retry_registration();
361
362 if ( empty( $result['success'] ) ) {
363 SendJson::error(
364 [
365 'code' => $result['code'] ?? 'verification_failed',
366 'message' => $result['message'] ?? __( 'Verification failed.', 'surecookie' ),
367 'dns_verification' => $result['dns_verification'] ?? null,
368 ]
369 );
370 return;
371 }
372
373 SendJson::success(
374 [
375 'message' => __( 'Domain verified.', 'surecookie' ),
376 'description' => __( 'Your site is registered and can now be scanned.', 'surecookie' ),
377 ]
378 );
379 }
380
381 }
382