diff options
Diffstat (limited to 'includes/class-cepis-woo-lookup.php')
| -rw-r--r-- | includes/class-cepis-woo-lookup.php | 135 |
1 files changed, 120 insertions, 15 deletions
diff --git a/includes/class-cepis-woo-lookup.php b/includes/class-cepis-woo-lookup.php index 47e3f16..da2f25a 100644 --- a/includes/class-cepis-woo-lookup.php +++ b/includes/class-cepis-woo-lookup.php @@ -43,6 +43,32 @@ final class Cepis_Woo_Lookup { private const MISS_TTL = DAY_IN_SECONDS; /** + * The fields of a 404's `faixa` object that are passed on. + * + * A whitelist rather than the whole object, so that a field the API grows + * later cannot reach the checkout unreviewed. None of these is ever written + * into a checkout field — see {@see Cepis_Woo_Lookup::faixa()}. + */ + private const FAIXA_FIELDS = array( + 'localidade', + 'uf', + 'estado', + 'regiao', + 'ibge', + 'cep_ini', + 'cep_fim', + 'ceps', + ); + + /** + * The transport the client speaks through, kept so that the body of a 404 + * can be read back after the client has turned it into an exception. + * + * @var Cepis_Woo_Transport|null + */ + private static ?Cepis_Woo_Transport $transport = null; + + /** * Look a CEP up, from cache when possible. * * Never throws: every outcome is described by the returned status and body, @@ -57,21 +83,32 @@ final class Cepis_Woo_Lookup { $cached = get_transient( self::CACHE_PREFIX . $cep ); if ( is_array( $cached ) ) { - return isset( $cached['miss'] ) - ? self::not_found() - : array( - 'status' => 200, - 'body' => $cached, - 'retry_after' => null, - ); + if ( isset( $cached['miss'] ) ) { + $remembered = isset( $cached['faixa'] ) && is_array( $cached['faixa'] ) ? $cached['faixa'] : null; + + return self::not_found( $remembered ); + } + + return array( + 'status' => 200, + 'body' => $cached, + 'retry_after' => null, + ); } try { $address = self::client()->lookup( $cep ); } catch ( Cepis\CepNotFoundException ) { - set_transient( self::CACHE_PREFIX . $cep, array( 'miss' => true ), self::MISS_TTL ); + $faixa = self::faixa( self::transport()->last_body() ); + $miss = array( 'miss' => true ); + + if ( null !== $faixa ) { + $miss['faixa'] = $faixa; + } + + set_transient( self::CACHE_PREFIX . $cep, $miss, self::MISS_TTL ); - return self::not_found(); + return self::not_found( $faixa ); } catch ( Cepis\InvalidCepException ) { return array( 'status' => 400, @@ -144,7 +181,7 @@ final class Cepis_Woo_Lookup { maxAttempts: 2, totalTimeout: 8.0, userAgent: 'cepis-woocommerce/' . CEPIS_WOO_VERSION . ' (+https://cep.is)', - transport: new Cepis_Woo_Transport(), + transport: self::transport(), ); } @@ -152,18 +189,86 @@ final class Cepis_Woo_Lookup { } /** + * The transport, built once per request. + */ + private static function transport(): Cepis_Woo_Transport { + if ( null === self::$transport ) { + self::$transport = new Cepis_Woo_Transport(); + } + + return self::$transport; + } + + /** + * The municipality a missing CEP would have belonged to, out of the API's + * 404 body. + * + * **This is context, not an address.** A CEP that does not exist has no + * address, and nothing here may ever reach a checkout field: a shipping + * address filled from a CEP the Correios do not have is a parcel delivered + * somewhere else. It exists only so the shopper can be told what they most + * likely mistyped — the classic case being the `-000` that 957 + * municipalities never received. + * + * Returns `null` for anything unexpected, so a body that changed shape, or + * is not JSON at all, produces exactly the message this plugin has always + * shown. + * + * @param string $body The response body, as it arrived. + * + * @return array<string, int|string>|null The named fields, or null. + */ + private static function faixa( string $body ): ?array { + if ( '' === $body ) { + return null; + } + + $data = json_decode( $body, true ); + + if ( ! is_array( $data ) || ! isset( $data['faixa'] ) || ! is_array( $data['faixa'] ) ) { + return null; + } + + $faixa = array(); + + foreach ( self::FAIXA_FIELDS as $name ) { + if ( ! isset( $data['faixa'][ $name ] ) || ! is_scalar( $data['faixa'][ $name ] ) ) { + continue; + } + + $value = $data['faixa'][ $name ]; + + $faixa[ $name ] = is_int( $value ) ? $value : sanitize_text_field( (string) $value ); + } + + return array() === $faixa ? null : $faixa; + } + + /** * The API's own 404 body, reproduced so the browser cannot tell the two * modes apart. * + * `faixa` is **omitted** when there is none, never sent as null, which is + * what the API does and what the browser is written to expect. + * + * @param array<string, int|string>|null $faixa The municipality the CEP + * would have belonged to, when the API named one. + * * @return array{status:int, body:array<string, mixed>, retry_after:?int} */ - private static function not_found(): array { + private static function not_found( ?array $faixa = null ): array { + $body = array( + 'erro' => 'cep_nao_encontrado', + 'mensagem' => 'CEP não existe na base do DNE', + ); + + if ( null !== $faixa ) { + $body['faixa'] = $faixa; + } + return array( 'status' => 404, - 'body' => array( - 'erro' => 'cep_nao_encontrado', - 'mensagem' => 'CEP não existe na base do DNE', - ), + 'body' => $body, 'retry_after' => null, ); } |