'POST', 'callback' => [$this, 'handle_consent'], // Security: Public endpoint with nonce verification in handle_consent(). // Nonce is required and verified via wp_verify_nonce() before any state changes. // This endpoint only sets the current user's own consent cookie. 'permission_callback' => '__return_true', 'args' => [ 'consent' => [ 'required' => true, 'type' => 'string', 'validate_callback' => function ($param) { return in_array($param, ['accepted', 'denied'], true); }, 'sanitize_callback' => 'sanitize_text_field', ], 'nonce' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], ], ] ); } /** * Handle consent setting via REST API * * @param \WP_REST_Request $request REST request object * @return \WP_REST_Response|\WP_Error */ public function handle_consent(\WP_REST_Request $request) { // Verify nonce for all users — consent is a state-changing operation. // A cross-site POST without nonce verification could force-accept consent, // enabling PII tracking without genuine user action (GDPR violation). // On cached pages, anonymous users may have a stale/empty nonce — the // request will fail with 403, but the JS cookie still records consent // client-side, and tracking works via the /hit endpoint (PR #235). $nonce = $request->get_param('nonce'); if (!wp_verify_nonce($nonce, 'wp_rest')) { return new \WP_Error( 'rest_forbidden', __('Invalid security token.', 'wp-slimstat'), ['status' => 403] ); } // Check if SlimStat banner is enabled if (empty(\wp_slimstat::$settings['use_slimstat_banner']) || 'on' !== \wp_slimstat::$settings['use_slimstat_banner']) { return new \WP_Error( 'rest_invalid', __('SlimStat banner is not enabled.', 'wp-slimstat'), ['status' => 400] ); } $consent = $request->get_param('consent'); $gdpr_service = new GDPRService(\wp_slimstat::$settings); // Set consent cookie $result = $gdpr_service->setConsent($consent); if (!$result) { return new \WP_Error( 'rest_error', __('Failed to set consent cookie.', 'wp-slimstat'), ['status' => 500] ); } // Fire action hook for consent change do_action('slimstat_gdpr_consent_changed', $consent); return new \WP_REST_Response( [ 'success' => true, 'message' => ('accepted' === $consent) ? __('Consent granted.', 'wp-slimstat') : __('Consent denied.', 'wp-slimstat'), 'consent' => $consent, ], 200 ); } }