PluginProbe
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification / 5.6.0
Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification v5.6.0
5.6.0 5.5.0 5.4.0 5.3.2 5.3.1 5.1.6 5.1.5 trunk 2.1.5 2.11 2.12 2.13 2.15 3.0.0 3.0.1 3.0.2 3.0.3 3.0.5 3.0.51 3.0.60 3.0.61 3.0.62 3.0.70 3.0.71 3.0.72 All 35 releases
double-opt-in / src / FollowUp / LoopbackClassifier.php

LoopbackClassifier.php in Double Opt-In for Contact Form 7 – Secure, GDPR-Compliant Email Verification 5.6.0, at src/FollowUp/LoopbackClassifier.php

154 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Classifies the transport outcome of an internal HTTP replay.
4 *
5 * @package Forge12\DoubleOptIn\FollowUp
6 * @since 5.6.0
7 */
8
9 declare( strict_types=1 );
10
11 namespace Forge12\DoubleOptIn\FollowUp;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 /**
18 * Turns (transport error, HTTP status, body) into either a decoded
19 * JSON payload with `success === true`, or a FollowUpResult explaining
20 * why the handler's own result is not available.
21 *
22 * The central distinction is *did the request possibly execute?*
23 * - Not sent (DNS, connection refused, connect timeout) → nothing ran,
24 * safe to retry automatically.
25 * - Sent, then timeout / 5xx / garbage → it may have run → `unknown`.
26 * - Rejected before the handler (401/403/404/3xx/429) → nothing ran,
27 * but retrying won't help until the cause is fixed → permanent.
28 */
29 final class LoopbackClassifier {
30
31 /** cURL: operation timed out. */
32 private const CURLE_OPERATION_TIMEDOUT = 28;
33
34 /** @var array<string, mixed>|null */
35 private $json;
36
37 /** @var FollowUpResult|null */
38 private $failure;
39
40 /** @var int */
41 private $httpStatus;
42
43 /** @var string */
44 private $responseKind;
45
46 private function __construct( ?array $json, ?FollowUpResult $failure, int $httpStatus, string $responseKind ) {
47 $this->json = $json;
48 $this->failure = $failure;
49 $this->httpStatus = $httpStatus;
50 $this->responseKind = $responseKind;
51 }
52
53 /**
54 * @param int $transportErrno cURL errno (0 = no transport error).
55 * @param bool $requestSent Whether the request left the client
56 * (cURL: CURLINFO_PRETRANSFER_TIME > 0).
57 * @param int $httpStatus 0 when no response was received.
58 * @param string $body Raw response body.
59 */
60 public static function classify( int $transportErrno, bool $requestSent, int $httpStatus, string $body ): self {
61 if ( $transportErrno !== 0 ) {
62 if ( ! $requestSent ) {
63 return self::fail( FollowUpResult::failedRetryable( 'transport_connect_failed' ), 0, FollowUpResult::RESPONSE_NONE );
64 }
65 $code = $transportErrno === self::CURLE_OPERATION_TIMEDOUT ? 'transport_timeout_unknown' : 'transport_error_unknown';
66 return self::fail( FollowUpResult::unknown( $code ), $httpStatus, FollowUpResult::RESPONSE_NONE );
67 }
68
69 $trimmed = trim( $body );
70 $kind = FollowUpResult::RESPONSE_EMPTY;
71 $json = null;
72 if ( $trimmed !== '' ) {
73 $decoded = json_decode( $trimmed, true );
74 if ( is_array( $decoded ) ) {
75 $json = $decoded;
76 $kind = ( isset( $decoded['success'] ) && $decoded['success'] === true )
77 ? FollowUpResult::RESPONSE_JSON_SUCCESS
78 : FollowUpResult::RESPONSE_JSON_ERROR;
79 } else {
80 $kind = FollowUpResult::RESPONSE_INVALID_JSON;
81 }
82 }
83
84 if ( $httpStatus === 401 || $httpStatus === 403 ) {
85 return self::fail( FollowUpResult::failedPermanent( 'http_forbidden' ), $httpStatus, $kind );
86 }
87 if ( $httpStatus === 404 ) {
88 return self::fail( FollowUpResult::failedPermanent( 'http_not_found' ), $httpStatus, $kind );
89 }
90 if ( $httpStatus === 429 ) {
91 return self::fail( FollowUpResult::failedPermanent( 'http_rate_limited' ), $httpStatus, $kind );
92 }
93 if ( $httpStatus >= 300 && $httpStatus < 400 ) {
94 // Redirects are never followed: the target could be another
95 // host, and a redirect is not the handler's answer.
96 return self::fail( FollowUpResult::failedPermanent( 'http_redirect' ), $httpStatus, $kind );
97 }
98 if ( $httpStatus >= 500 ) {
99 return self::fail( FollowUpResult::unknown( 'http_server_error' ), $httpStatus, $kind );
100 }
101 if ( $httpStatus >= 400 ) {
102 return self::fail( FollowUpResult::failedPermanent( 'http_client_error' ), $httpStatus, $kind );
103 }
104 if ( $httpStatus < 200 ) {
105 return self::fail( FollowUpResult::unknown( 'invalid_response' ), $httpStatus, $kind );
106 }
107
108 if ( $json === null ) {
109 // 200 with no JSON: a fatal error or output buffer mishap
110 // after the handler possibly ran.
111 return self::fail( FollowUpResult::unknown( 'invalid_response' ), $httpStatus, $kind );
112 }
113
114 return new self( $json, null, $httpStatus, $kind );
115 }
116
117 private static function fail( FollowUpResult $result, int $httpStatus, string $kind ): self {
118 return new self( null, $result->withTransport( $httpStatus, $kind ), $httpStatus, $kind );
119 }
120
121 /**
122 * The handler answered with parseable JSON (success true OR false).
123 */
124 public function hasHandlerAnswer(): bool {
125 return $this->json !== null;
126 }
127
128 public function isHandlerSuccess(): bool {
129 return $this->responseKind === FollowUpResult::RESPONSE_JSON_SUCCESS;
130 }
131
132 /**
133 * @return array<string, mixed>|null
134 */
135 public function getJson(): ?array {
136 return $this->json;
137 }
138
139 /**
140 * Transport-level failure, or null when the handler answered.
141 */
142 public function getFailure(): ?FollowUpResult {
143 return $this->failure;
144 }
145
146 public function getHttpStatus(): int {
147 return $this->httpStatus;
148 }
149
150 public function getResponseKind(): string {
151 return $this->responseKind;
152 }
153 }
154