Passed
Branch master (fbf0a6)
by Volodymyr
04:04
created

VerifyReCaptcha   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 267
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 66
dl 0
loc 267
rs 10
c 0
b 0
f 0
wmc 22

8 Methods

Rating   Name   Duplication   Size   Complexity  
A setScoreThreshold() 0 5 1
A setExpectedHostname() 0 5 1
A getSecret() 0 3 1
A __construct() 0 6 1
A setExpectedAction() 0 5 1
C verify() 0 54 15
A setChallengeTimeout() 0 5 1
A setSecret() 0 5 1
1
<?php
2
/**
3
 * Copyright (c) 2019. Volodymyr Hryvinskyi.  All rights reserved.
4
 * @author: <mailto:[email protected]>
5
 * @github: <https://github.com/hryvinskyi>
6
 */
7
8
declare(strict_types=1);
9
10
namespace Hryvinskyi\InvisibleCaptcha\Model\ReCaptcha;
11
12
use Exception;
13
use Hryvinskyi\Base\Helper\Json;
14
15
/**
16
 * Class VerifyVerifyReCaptcha
17
 */
18
class VerifyReCaptcha
19
{
20
    /**
21
     * URL for reCAPTCHA sitevrerify API
22
     *
23
     * @const string
24
     */
25
    const SITE_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
26
27
    /**
28
     * Invalid JSON received
29
     *
30
     * @const string
31
     */
32
    const E_INVALID_JSON = 'invalid-json';
33
34
    /**
35
     * Could not connect to service
36
     *
37
     * @const string
38
     */
39
    const E_CONNECTION_FAILED = 'connection-failed';
40
41
    /**
42
     * Did not receive a 200 from the service
43
     *
44
     * @const string
45
     */
46
    const E_BAD_RESPONSE = 'bad-response';
47
48
    /**
49
     * Not a success, but no error codes received!
50
     *
51
     * @const string
52
     */
53
    const E_UNKNOWN_ERROR = 'unknown-error';
54
55
    /**
56
     * ReCAPTCHA response not provided
57
     *
58
     * @const string
59
     */
60
    const E_MISSING_INPUT_RESPONSE = 'missing-input-response';
61
62
    /**
63
     * Expected hostname did not match
64
     *
65
     * @const string
66
     */
67
    const E_HOSTNAME_MISMATCH = 'hostname-mismatch';
68
69
    /**
70
     * Expected action did not match
71
     *
72
     * @const string
73
     */
74
    const E_ACTION_MISMATCH = 'action-mismatch';
75
76
    /**
77
     * Score threshold not met
78
     *
79
     * @const string
80
     */
81
    const E_SCORE_THRESHOLD_NOT_MET = 'score-threshold-not-met';
82
83
    /**
84
     * Challenge timeout
85
     *
86
     * @const string
87
     */
88
    const E_CHALLENGE_TIMEOUT = 'challenge-timeout';
89
90
    /**
91
     * @var string
92
     */
93
    private $hostname;
94
95
    /**
96
     * @var string
97
     */
98
    private $action;
99
100
    /**
101
     * @var float
102
     */
103
    private $threshold;
104
105
    /**
106
     * @var int
107
     */
108
    private $timeoutSeconds;
109
110
    /**
111
     * Shared secret for the site.
112
     * @var string
113
     */
114
    private $secret;
115
116
    /**
117
     * Method used to communicate with service. Defaults to POST request.
118
     *
119
     * @var RequestMethodInterface
120
     */
121
    private $requestMethod;
122
123
    /**
124
     * @var RequestParameters
125
     */
126
    private $requestParameters;
127
128
    /**
129
     * VerifyReCaptcha constructor.
130
     *
131
     * @param RequestMethodInterface $requestMethod
132
     * @param RequestParameters $requestParameters
133
     */
134
    public function __construct(
135
        RequestMethodInterface $requestMethod,
136
        RequestParameters $requestParameters
137
    ) {
138
        $this->requestMethod = $requestMethod;
139
        $this->requestParameters = $requestParameters;
140
    }
141
142
    /**
143
     * Calls the reCAPTCHA siteverify API to verify whether the user passes
144
     * CAPTCHA test and additionally runs any specified additional checks
145
     *
146
     * @param string $response The user response token provided by reCAPTCHA, verifying the user on your site.
147
     * @param string $remoteIp The end user's IP address.
148
     *
149
     * @return Response
150
     */
151
    public function verify($response, $remoteIp = null): Response
152
    {
153
        $params = $this->requestParameters
154
            ->setSecret($this->getSecret())
155
            ->setResponse($response)
156
            ->setRemoteIp($remoteIp);
157
158
        $answer = $this->requestMethod->submit(self::SITE_VERIFY_URL, $params);
159
        $initialResponse = Response::fromJson($answer);
160
        $validationErrors = [];
161
162
        if (
163
            isset($this->hostname)
164
            && $initialResponse->getHostname()
165
            && strcasecmp($this->hostname, $initialResponse->getHostname()) !== 0
166
        ) {
167
            $validationErrors[] = self::E_HOSTNAME_MISMATCH;
168
        }
169
170
        if (
171
            isset($this->action)
172
            && $initialResponse->getAction()
173
            && strcasecmp($this->action, $initialResponse->getAction()) !== 0
174
        ) {
175
            $validationErrors[] = self::E_ACTION_MISMATCH;
176
        }
177
178
        if (
179
            isset($this->threshold)
180
            && $initialResponse->getScore()
181
            && $this->threshold > $initialResponse->getScore()
182
        ) {
183
            $validationErrors[] = self::E_SCORE_THRESHOLD_NOT_MET;
184
        }
185
186
        if (isset($this->timeoutSeconds) && $initialResponse->getChallengeTs()) {
187
            $challengeTs = strtotime($initialResponse->getChallengeTs());
188
189
            if ($challengeTs > 0 && time() - $challengeTs > $this->timeoutSeconds) {
190
                $validationErrors[] = self::E_CHALLENGE_TIMEOUT;
191
            }
192
        }
193
194
        if (empty($validationErrors)) {
195
            return $initialResponse;
196
        }
197
198
        return new Response(
199
            false,
200
            array_merge($initialResponse->getErrorCodes(), $validationErrors),
201
            $initialResponse->getHostname(),
202
            $initialResponse->getChallengeTs(),
203
            $initialResponse->getScore(),
204
            $initialResponse->getAction()
205
        );
206
    }
207
208
    /**
209
     * @return string
210
     */
211
    public function getSecret(): string
212
    {
213
        return $this->secret;
214
    }
215
216
    /**
217
     * @param string $secret
218
     *
219
     * @return VerifyReCaptcha
220
     */
221
    public function setSecret(string $secret): VerifyReCaptcha
222
    {
223
        $this->secret = $secret;
224
225
        return $this;
226
    }
227
228
    /**
229
     * Provide a hostname to match against in verify()
230
     * This should be without a protocol or trailing slash, e.g. www.google.com
231
     *
232
     * @param string $hostname Expected hostname
233
     *
234
     * @return VerifyReCaptcha
235
     */
236
    public function setExpectedHostname($hostname): VerifyReCaptcha
237
    {
238
        $this->hostname = $hostname;
239
240
        return $this;
241
    }
242
243
    /**
244
     * Provide an action to match against in verify()
245
     * This should be set per page.
246
     *
247
     * @param string $action Expected action
248
     *
249
     * @return VerifyReCaptcha
250
     */
251
    public function setExpectedAction($action): VerifyReCaptcha
252
    {
253
        $this->action = $action;
254
255
        return $this;
256
    }
257
258
    /**
259
     * Provide a threshold to meet or exceed in verify()
260
     * Threshold should be a float between 0 and 1 which will be tested as response >= threshold.
261
     *
262
     * @param float $threshold Expected threshold
263
     *
264
     * @return VerifyReCaptcha
265
     */
266
    public function setScoreThreshold($threshold): VerifyReCaptcha
267
    {
268
        $this->threshold = floatval($threshold);
269
270
        return $this;
271
    }
272
273
    /**
274
     * Provide a timeout in seconds to test against the challenge timestamp in verify()
275
     *
276
     * @param int $timeoutSeconds Expected hostname
277
     *
278
     * @return VerifyReCaptcha
279
     */
280
    public function setChallengeTimeout(int $timeoutSeconds): VerifyReCaptcha
281
    {
282
        $this->timeoutSeconds = $timeoutSeconds;
283
284
        return $this;
285
    }
286
}
287