diff options
| author | Murilo Ijanc <[email protected]> | 2026-07-26 16:41:48 -0300 |
|---|---|---|
| committer | Murilo Ijanc <[email protected]> | 2026-07-26 16:47:14 -0300 |
| commit | 535dbda6b48171f8115b455e54790de94d97d42a (patch) | |
| tree | 5c0410011e61a883bdf063ee612875a637fb1d22 /includes/class-cepis-woo-rest.php | |
| download | woocommerce-535dbda6b48171f8115b455e54790de94d97d42a.tar.gz | |
Initial importv0.1.0
Diffstat (limited to 'includes/class-cepis-woo-rest.php')
| -rw-r--r-- | includes/class-cepis-woo-rest.php | 120 |
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() ) ); + } +} |