Completed
Pull Request — master (#36)
by ARCANEDEV
09:05
created

NoCaptcha   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 100%

Importance

Changes 25
Bugs 0 Features 1
Metric Value
wmc 37
c 25
b 0
f 1
lcom 1
cbo 7
dl 0
loc 455
ccs 118
cts 118
cp 1
rs 8.6

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A setSecret() 0 8 1
A setSiteKey() 0 8 1
A setLang() 0 6 1
A getScriptSrc() 0 17 4
A setRequestClient() 0 7 1
A setAttributes() 0 7 1
A getNameAttribute() 0 12 3
A display() 0 8 1
A image() 0 6 1
A audio() 0 6 1
A verify() 0 12 3
A verifyRequest() 0 15 3
A script() 0 11 2
A scriptWithCallback() 0 16 3
A renderCaptchas() 0 6 1
A hasLang() 0 4 1
A hasCallbackName() 0 4 2
A checkKey() 0 8 1
A checkIsString() 0 8 2
A checkIsNotEmpty() 0 6 2
A sendVerifyRequest() 0 8 1
1
<?php namespace Arcanedev\NoCaptcha;
2
3
use Arcanedev\NoCaptcha\Exceptions\ApiException;
4
use Arcanedev\NoCaptcha\Utilities\Attributes;
5
use Arcanedev\NoCaptcha\Utilities\Request;
6
use Psr\Http\Message\ServerRequestInterface;
7
8
/**
9
 * Class     NoCaptcha
10
 *
11
 * @package  Arcanedev\NoCaptcha
12
 * @author   ARCANEDEV <[email protected]>
13
 */
14
class NoCaptcha implements Contracts\NoCaptcha
15
{
16
    /* ------------------------------------------------------------------------------------------------
17
     |  Constants
18
     | ------------------------------------------------------------------------------------------------
19
     */
20
    const CLIENT_URL   = 'https://www.google.com/recaptcha/api.js';
21
    const VERIFY_URL   = 'https://www.google.com/recaptcha/api/siteverify';
22
    const CAPTCHA_NAME = 'g-recaptcha-response';
23
24
    /* ------------------------------------------------------------------------------------------------
25
     |  Properties
26
     | ------------------------------------------------------------------------------------------------
27
     */
28
    /**
29
     * The shared key between your site and ReCAPTCHA
30
     *
31
     * @var string
32
     */
33
    private $secret;
34
35
    /**
36
     * Your site key
37
     *
38
     * @var string
39
     */
40
    private $siteKey;
41
42
    /**
43
     * Forces the widget to render in a specific language.
44
     * Auto-detects the user's language if unspecified.
45
     *
46
     * @var string
47
     */
48
    protected $lang;
49
50
    /**
51
     * Decides if we've already loaded the script file or not.
52
     *
53
     * @param bool
54
     */
55
    protected $scriptLoaded = false;
56
57
    /**
58
     * HTTP Request Client
59
     *
60
     * @var \Arcanedev\NoCaptcha\Contracts\Utilities\RequestInterface
61
     */
62
    protected $request;
63
64
    /**
65
     * noCaptcha Attributes
66
     *
67
     * @var \Arcanedev\NoCaptcha\Contracts\Utilities\AttributesInterface
68
     */
69
    protected $attributes;
70
71
    /* ------------------------------------------------------------------------------------------------
72
     |  Constructor
73
     | ------------------------------------------------------------------------------------------------
74
     */
75
    /**
76
     * NoCaptcha constructor.
77
     *
78
     * @param  string       $secret
79
     * @param  string       $siteKey
80
     * @param  string|null  $lang
81
     * @param  array        $attributes
82
     */
83 324
    public function __construct($secret, $siteKey, $lang = null, array $attributes = [])
84
    {
85 324
        $this->setSecret($secret);
86 324
        $this->setSiteKey($siteKey);
87 324
        $this->setLang($lang);
88
89 324
        $this->setRequestClient(new Request);
90 324
        $this->setAttributes(new Attributes($attributes));
91 324
    }
92
93
    /* ------------------------------------------------------------------------------------------------
94
     |  Getters & Setters
95
     | ------------------------------------------------------------------------------------------------
96
     */
97
    /**
98
     * Set the secret key.
99
     *
100
     * @param  string  $secret
101
     *
102
     * @return self
103
     */
104 324
    protected function setSecret($secret)
105
    {
106 324
        $this->checkKey('secret key', $secret);
107
108 324
        $this->secret = $secret;
109
110 324
        return $this;
111
    }
112
113
    /**
114
     * Set Site key.
115
     *
116
     * @param  string  $siteKey
117
     *
118
     * @return self
119
     */
120 324
    protected function setSiteKey($siteKey)
121
    {
122 324
        $this->checkKey('site key', $siteKey);
123
124 324
        $this->siteKey = $siteKey;
125
126 324
        return $this;
127
    }
128
129
    /**
130
     * Set language code.
131
     *
132
     * @param  string  $lang
133
     *
134
     * @return self
135
     */
136 324
    public function setLang($lang)
137
    {
138 324
        $this->lang = $lang;
139
140 324
        return $this;
141
    }
142
143
    /**
144
     * Get script source link.
145
     *
146
     * @param  string|null  $callbackName
147
     *
148
     * @return string
149
     */
150 72
    private function getScriptSrc($callbackName = null)
151
    {
152 72
        $queries = [];
153
154 72
        if ($this->hasLang()) {
155 36
            $queries['hl'] = $this->lang;
156 27
        }
157
158 72
        if ($this->hasCallbackName($callbackName)) {
159 12
            $queries['onload'] = $callbackName;
160 12
            $queries['render'] = 'explicit';
161 9
        }
162
163 72
        $queries = count($queries) ? '?' . http_build_query($queries) : '';
164
165 72
        return static::CLIENT_URL . $queries;
166
    }
167
168
    /**
169
     * Set HTTP Request Client.
170
     *
171
     * @param  \Arcanedev\NoCaptcha\Contracts\Utilities\RequestInterface  $request
172
     *
173
     * @return self
174
     */
175 324
    public function setRequestClient(
176
        Contracts\Utilities\RequestInterface $request
177
    ) {
178 324
        $this->request = $request;
179
180 324
        return $this;
181
    }
182
183
    /**
184
     * Set noCaptcha Attributes.
185
     *
186
     * @param  \Arcanedev\NoCaptcha\Contracts\Utilities\AttributesInterface  $attributes
187
     *
188
     * @return self
189
     */
190 324
    public function setAttributes(
191
        Contracts\Utilities\AttributesInterface $attributes
192
    ) {
193 324
        $this->attributes = $attributes;
194
195 324
        return $this;
196
    }
197
198
    /**
199
     * Get attribute name + id.
200
     *
201
     * @param  string|null $name
202
     *
203
     * @throws \Arcanedev\NoCaptcha\Exceptions\InvalidArgumentException
204
     *
205
     * @return array
206
     */
207 132
    protected function getNameAttribute($name)
208
    {
209 132
        if (is_null($name)) return [];
210
211 120
        if ($name === self::CAPTCHA_NAME) {
212 36
            throw new Exceptions\InvalidArgumentException(
213 36
                'The captcha name must be different from "' . self::CAPTCHA_NAME . '".'
214 27
            );
215
        }
216
217 84
        return array_combine(['id', 'name'], [$name, $name]);
218
    }
219
220
    /* ------------------------------------------------------------------------------------------------
221
     |  Main Functions
222
     | ------------------------------------------------------------------------------------------------
223
     */
224
    /**
225
     * Display Captcha.
226
     *
227
     * @param  string|null  $name
228
     * @param  array        $attributes
229
     *
230
     * @return string
231
     */
232 132
    public function display($name = null, array $attributes = [])
233
    {
234 132
        $output = $this->attributes->build(
235 132
            $this->siteKey, array_merge($this->getNameAttribute($name), $attributes)
236 72
        );
237
238 96
        return '<div ' . $output . '></div>';
239
    }
240
241
    /**
242
     * Display image Captcha.
243
     *
244
     * @param  string|null  $name
245
     * @param  array        $attributes
246
     *
247
     * @return string
248
     */
249 36
    public function image($name = null, array $attributes = [])
250
    {
251 36
        return $this->display(
252 36
            $name, array_merge($attributes, $this->attributes->getImageAttribute())
253 27
        );
254
    }
255
256
    /**
257
     * Display audio Captcha.
258
     *
259
     * @param  string|null  $name
260
     * @param  array        $attributes
261
     *
262
     * @return string
263
     */
264 36
    public function audio($name = null, array $attributes = [])
265
    {
266 36
        return $this->display(
267 36
            $name, array_merge($attributes, $this->attributes->getAudioAttribute())
268 27
        );
269
    }
270
271
    /**
272
     * Verify Response.
273
     *
274
     * @param  string  $response
275
     * @param  string  $clientIp
276
     *
277
     * @return bool
278
     */
279 72
    public function verify($response, $clientIp = null)
280
    {
281 72
        if (empty($response)) return false;
282
283 72
        $response = $this->sendVerifyRequest([
284 72
            'secret'   => $this->secret,
285 72
            'response' => $response,
286 18
            'remoteip' => $clientIp
287 54
        ]);
288
289 72
        return isset($response['success']) && $response['success'] === true;
290
    }
291
292
    /**
293
     * Calls the reCAPTCHA siteverify API to verify whether the user passes CAPTCHA
294
     * test using a PSR-7 ServerRequest object.
295
     *
296
     * @param  \Psr\Http\Message\ServerRequestInterface  $request
297
     *
298
     * @return bool
299
     */
300 12
    public function verifyRequest(ServerRequestInterface $request)
301
    {
302 12
        $body   = $request->getParsedBody();
303 12
        $server = $request->getServerParams();
304
305 12
        $response = isset($body[self::CAPTCHA_NAME])
306 12
            ? $body[self::CAPTCHA_NAME]
307 12
            : '';
308
309 12
        $remoteIp = isset($server['REMOTE_ADDR'])
310 12
            ? $server['REMOTE_ADDR']
311 12
            : null;
312
313 12
        return $this->verify($response, $remoteIp);
314
    }
315
316
    /**
317
     * Get script tag.
318
     *
319
     * @param  string|null  $callbackName
320
     *
321
     * @return string
322
     */
323 72
    public function script($callbackName = null)
324
    {
325 72
        $script = '';
326
327 72
        if ( ! $this->scriptLoaded) {
328 72
            $script = '<script src="' . $this->getScriptSrc($callbackName) . '" async defer></script>';
329 72
            $this->scriptLoaded = true;
330 54
        }
331
332 72
        return $script;
333
    }
334
335
    /**
336
     * Get script tag with a callback function.
337
     *
338
     * @param  array   $captchas
339
     * @param  string  $callbackName
340
     *
341
     * @return string
342
     */
343 12
    public function scriptWithCallback(array $captchas, $callbackName = 'captchaRenderCallback')
344
    {
345 12
        $script = $this->script($callbackName);
346
347 12
        if (empty($script) || empty($captchas)) {
348 12
            return $script;
349
        }
350
351 12
        return implode(PHP_EOL, [implode(PHP_EOL, [
352 12
            '<script>',
353 12
                "var $callbackName = function() {",
354 12
                    $this->renderCaptchas($captchas),
355 12
                '};',
356 3
            '</script>'
357 12
        ]), $script]);
358
    }
359
360
    /**
361
     * Rendering captchas with callback function.
362
     *
363
     * @param  array  $captchas
364
     *
365
     * @return string
366
     */
367
    private function renderCaptchas(array $captchas)
368
    {
369 12
        return implode(PHP_EOL, array_map(function($captcha) {
370 12
            return "if (document.getElementById('$captcha')) { grecaptcha.render('$captcha', {'sitekey' : '{$this->siteKey}'}); }";
371 12
        }, $captchas));
372
    }
373
374
    /* ------------------------------------------------------------------------------------------------
375
     |  Check Functions
376
     | ------------------------------------------------------------------------------------------------
377
     */
378
    /**
379
     * Check if has lang.
380
     *
381
     * @return bool
382
     */
383 72
    private function hasLang()
384
    {
385 72
        return ! empty($this->lang);
386
    }
387
388
    /**
389
     * Check if callback is not empty.
390
     *
391
     * @param  string|null  $callbackName
392
     *
393
     * @return bool
394
     */
395 72
    private function hasCallbackName($callbackName)
396
    {
397 72
        return ! (is_null($callbackName) || trim($callbackName) === '');
398
    }
399
400
    /**
401
     * Check key.
402
     *
403
     * @param  string  $name
404
     * @param  string  $value
405
     *
406
     * @throws \Arcanedev\NoCaptcha\Exceptions\ApiException
407
     */
408 324
    private function checkKey($name, &$value)
409
    {
410 324
        $this->checkIsString($name, $value);
411
412 324
        $value = trim($value);
413
414 324
        $this->checkIsNotEmpty($name, $value);
415 324
    }
416
417
    /**
418
     * Check if the value is a string value.
419
     *
420
     * @param  string  $name
421
     * @param  string  $value
422
     *
423
     * @throws \Arcanedev\NoCaptcha\Exceptions\ApiException
424
     */
425 324
    private function checkIsString($name, $value)
426
    {
427 324
        if ( ! is_string($value)) {
428 24
            throw new ApiException(
429 24
                'The ' . $name . ' must be a string value, ' . gettype($value) . ' given'
430 18
            );
431
        }
432 324
    }
433
434
    /**
435
     * Check if the value is not empty.
436
     *
437
     * @param string  $name
438
     * @param string  $value
439
     *
440
     * @throws \Arcanedev\NoCaptcha\Exceptions\ApiException
441
     */
442 324
    private function checkIsNotEmpty($name, $value)
443
    {
444 324
        if (empty($value)) {
445 24
            throw new ApiException('The ' . $name . ' must not be empty');
446
        }
447 324
    }
448
449
    /* ------------------------------------------------------------------------------------------------
450
     |  Other functions
451
     | ------------------------------------------------------------------------------------------------
452
     */
453
    /**
454
     * Send verify request to API and get response.
455
     *
456
     * @param  array  $query
457
     *
458
     * @return array
459
     */
460 72
    private function sendVerifyRequest(array $query = [])
461
    {
462 72
        $query    = array_filter($query);
463 72
        $url      = static::VERIFY_URL . '?' . http_build_query($query);
464 72
        $response = $this->request->send($url);
465
466 72
        return $response;
467
    }
468
}
469