1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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() ) );
}
}
|