summaryrefslogtreecommitdiffstats
path: root/includes/class-cepis-woo-rest.php
diff options
context:
space:
mode:
Diffstat (limited to 'includes/class-cepis-woo-rest.php')
-rw-r--r--includes/class-cepis-woo-rest.php120
1 files changed, 120 insertions, 0 deletions
diff --git a/includes/class-cepis-woo-rest.php b/includes/class-cepis-woo-rest.php
new file mode 100644
index 0000000..561ca7e
--- /dev/null
+++ b/includes/class-cepis-woo-rest.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * The optional server-side proxy route.
+ *
+ * @package CepisWooCommerce
+ */
+
+declare(strict_types=1);
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Exposes `GET /wp-json/cepis/v1/lookup/{token}/{cep}`, which answers exactly
+ * what cep.is answers.
+ *
+ * The route is only registered when the store has turned proxy mode on. It is
+ * off by default because the browser can reach cep.is directly in about twenty
+ * milliseconds against a Cloudflare edge cache, and putting a WordPress
+ * bootstrap in front of that — on the checkout page, once per settled CEP —
+ * spends latency where a store can least afford it. Turn it on when a
+ * Content-Security-Policy forbids external XHR, or to get the server-side
+ * cache in {@see Cepis_Woo_Lookup}.
+ *
+ * The token is a path segment rather than a header because the widget's
+ * request is a plain cross-origin-safe GET with no custom headers.
+ * {@see Cepis_Woo_Token} explains why it is not a session-bound nonce.
+ */
+final class Cepis_Woo_Rest {
+
+ /**
+ * REST namespace for the proxy.
+ */
+ public const NAMESPACE_V1 = 'cepis/v1';
+
+ /**
+ * Register the route, when the store asked for it.
+ */
+ public static function init(): void {
+ if ( ! Cepis_Woo_Options::enabled() || 'proxy' !== Cepis_Woo_Options::mode() ) {
+ return;
+ }
+
+ add_action( 'rest_api_init', array( self::class, 'register' ) );
+ }
+
+ /**
+ * Declare the route.
+ */
+ public static function register(): void {
+ register_rest_route(
+ self::NAMESPACE_V1,
+ '/lookup/(?P<token>[A-Za-z0-9]{1,64})/(?P<cep>[0-9]{8})',
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( self::class, 'lookup' ),
+ 'permission_callback' => array( self::class, 'permitted' ),
+ 'args' => array(
+ 'token' => array(
+ 'type' => 'string',
+ 'required' => true,
+ 'sanitize_callback' => 'sanitize_key',
+ ),
+ 'cep' => array(
+ 'type' => 'string',
+ 'required' => true,
+ 'sanitize_callback' => 'sanitize_key',
+ 'validate_callback' => static fn( $value ): bool => 1 === preg_match( '/^[0-9]{8}$/', (string) $value ),
+ ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Reject anything that does not carry a live token.
+ *
+ * @param WP_REST_Request $request Incoming request.
+ *
+ * @return true|WP_Error True when the token checks out.
+ */
+ public static function permitted( WP_REST_Request $request ): bool|WP_Error {
+ if ( Cepis_Woo_Token::verify( (string) $request['token'] ) ) {
+ return true;
+ }
+
+ return new WP_Error(
+ 'cepis_invalid_token',
+ __( 'Lookup token missing, invalid or expired. Reload the page.', 'cepis-woocommerce' ),
+ array( 'status' => 403 )
+ );
+ }
+
+ /**
+ * Answer one lookup.
+ *
+ * @param WP_REST_Request $request Incoming request.
+ */
+ public static function lookup( WP_REST_Request $request ): WP_REST_Response {
+ $result = Cepis_Woo_Lookup::resolve( (string) $request['cep'] );
+
+ $response = new WP_REST_Response( $result['body'], $result['status'] );
+ $response->header( 'Cache-Control', 'no-store' );
+
+ if ( null !== $result['retry_after'] ) {
+ $response->header( 'Retry-After', (string) $result['retry_after'] );
+ }
+
+ return $response;
+ }
+
+ /**
+ * The API root to hand the browser, with a live token already in it.
+ *
+ * Shaped so that appending `/{cep}` to it reaches this route, which is what
+ * the widget does with its `baseUrl`.
+ */
+ public static function base_url(): string {
+ return untrailingslashit( rest_url( self::NAMESPACE_V1 . '/lookup/' . Cepis_Woo_Token::create() ) );
+ }
+}