Completed
Push — master ( cb204c...07fd0e )
by ARCANEDEV
9s
created

NoCaptcha::scriptWithCallback()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 4
Bugs 0 Features 1
Metric Value
c 4
b 0
f 1
dl 0
loc 16
ccs 5
cts 5
cp 1
rs 9.4285
cc 3
eloc 11
nc 2
nop 2
crap 3
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 276
     */
83
    public function __construct($secret, $siteKey, $lang = null, array $attributes = [])
84 276
    {
85 276
        $this->setSecret($secret);
86 276
        $this->setSiteKey($siteKey);
87
        $this->setLang($lang);
88 276
89 276
        $this->setRequestClient(new Request);
90 276
        $this->setAttributes(new Attributes($attributes));
91
    }
92
93
    /* ------------------------------------------------------------------------------------------------
94
     |  Getters & Setters
95
     | ------------------------------------------------------------------------------------------------
96
     */
97
    /**
98
     * Set the secret key.
99
     *
100
     * @param  string  $secret
101
     *
102
     * @return self
103 276
     */
104
    protected function setSecret($secret)
105 276
    {
106
        $this->checkKey('secret key', $secret);
107 276
108
        $this->secret = $secret;
109 276
110
        return $this;
111
    }
112
113
    /**
114
     * Set Site key.
115
     *
116
     * @param  string  $siteKey
117
     *
118
     * @return self
119 276
     */
120
    protected function setSiteKey($siteKey)
121 276
    {
122
        $this->checkKey('site key', $siteKey);
123 276
124
        $this->siteKey = $siteKey;
125 276
126
        return $this;
127
    }
128
129
    /**
130
     * Set language code.
131
     *
132
     * @param  string  $lang
133
     *
134
     * @return self
135 276
     */
136
    public function setLang($lang)
137 276
    {
138
        $this->lang = $lang;
139 276
140
        return $this;
141
    }
142
143
    /**
144
     * Get script source link.
145
     *
146
     * @param  string|null  $callbackName
147
     *
148
     * @return string
149 72
     */
150
    private function getScriptSrc($callbackName = null)
151 72
    {
152
        $queries = [];
153 72
154 36
        if ($this->hasLang()) {
155 27
            $queries['hl'] = $this->lang;
156
        }
157 72
158 12
        if ($this->hasCallbackName($callbackName)) {
159 12
            $queries['onload'] = $callbackName;
160 9
            $queries['render'] = 'explicit';
161
        }
162 72
163
        $queries = count($queries) ? '?' . http_build_query($queries) : '';
164 72
165
        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 276
     */
175
    public function setRequestClient(
176
        Contracts\Utilities\RequestInterface $request
177 276
    ) {
178
        $this->request = $request;
179 276
180
        return $this;
181
    }
182
183
    /**
184
     * Set noCaptcha Attributes.
185
     *
186
     * @param  \Arcanedev\NoCaptcha\Contracts\Utilities\AttributesInterface  $attributes
187
     *
188
     * @return self
189 276
     */
190
    public function setAttributes(
191
        Contracts\Utilities\AttributesInterface $attributes
192 276
    ) {
193
        $this->attributes = $attributes;
194 276
195
        return $this;
196
    }
197
198
    /**
199
     * Get attribute name + id.
200
     *
201
     * @param  string|null $name
202
     *
203
     * @throws \Arcanedev\NoCaptcha\Exceptions\InvalidArgumentException
204 84
     *
205
     * @return array
206 84
     */
207
    protected function getNameAttribute($name)
208
    {
209
        if (is_null($name)) return [];
210
211
        if ($name === self::CAPTCHA_NAME) {
212
            throw new Exceptions\InvalidArgumentException(
213
                'The captcha name must be different from "' . self::CAPTCHA_NAME . '".'
214
            );
215
        }
216
217
        return array_combine(['id', 'name'], [$name, $name]);
218
    }
219
220
    /* ------------------------------------------------------------------------------------------------
221 84
     |  Main Functions
222
     | ------------------------------------------------------------------------------------------------
223 84
     */
224 84
    /**
225 63
     * Display Captcha.
226
     *
227 84
     * @param  string|null  $name
228
     * @param  array        $attributes
229
     *
230
     * @return string
231
     */
232
    public function display($name = null, array $attributes = [])
233
    {
234
        $output = $this->attributes->build(
235
            $this->siteKey, array_merge($this->getNameAttribute($name), $attributes)
236
        );
237
238 12
        return '<div ' . $output . '></div>';
239
    }
240 12
241 12
    /**
242 9
     * Display image Captcha.
243
     *
244
     * @param  string|null  $name
245
     * @param  array        $attributes
246
     *
247
     * @return string
248
     */
249
    public function image($name = null, array $attributes = [])
250
    {
251
        return $this->display(
252
            $name, array_merge($attributes, $this->attributes->getImageAttribute())
253 12
        );
254
    }
255 12
256 12
    /**
257 9
     * Display audio Captcha.
258
     *
259
     * @param  string|null  $name
260
     * @param  array        $attributes
261
     *
262
     * @return string
263
     */
264
    public function audio($name = null, array $attributes = [])
265
    {
266
        return $this->display(
267
            $name, array_merge($attributes, $this->attributes->getAudioAttribute())
268 72
        );
269
    }
270 72
271
    /**
272 72
     * Verify Response.
273 72
     *
274 72
     * @param  string  $response
275 18
     * @param  string  $clientIp
276 54
     *
277
     * @return bool
278 72
     */
279
    public function verify($response, $clientIp = null)
280
    {
281
        if (empty($response)) return false;
282
283
        $response = $this->sendVerifyRequest([
284
            'secret'   => $this->secret,
285
            'response' => $response,
286
            'remoteip' => $clientIp
287
        ]);
288
289 12
        return isset($response['success']) && $response['success'] === true;
290
    }
291 12
292 12
    /**
293
     * Calls the reCAPTCHA siteverify API to verify whether the user passes CAPTCHA
294 12
     * test using a PSR-7 ServerRequest object.
295 12
     *
296 12
     * @param  \Psr\Http\Message\ServerRequestInterface  $request
297
     *
298 12
     * @return bool
299 12
     */
300 12
    public function verifyRequest(ServerRequestInterface $request)
301
    {
302 12
        $body   = $request->getParsedBody();
303
        $server = $request->getServerParams();
304
305
        $response = isset($body[self::CAPTCHA_NAME])
306
            ? $body[self::CAPTCHA_NAME]
307
            : '';
308
309
        $remoteIp = isset($server['REMOTE_ADDR'])
310
            ? $server['REMOTE_ADDR']
311
            : null;
312 72
313
        return $this->verify($response, $remoteIp);
314 72
    }
315
316 72
    /**
317 72
     * Get script tag.
318 72
     *
319 54
     * @param  string|null  $callbackName
320
     *
321 72
     * @return string
322
     */
323
    public function script($callbackName = null)
324
    {
325
        $script = '';
326
327
        if ( ! $this->scriptLoaded) {
328
            $script = '<script src="' . $this->getScriptSrc($callbackName) . '" async defer></script>';
329
            $this->scriptLoaded = true;
330
        }
331
332 12
        return $script;
333
    }
334 12
335
    /**
336 12
     * Get script tag with a callback function.
337 12
     *
338
     * @param  array   $captchas
339
     * @param  string  $callbackName
340 12
     *
341 12
     * @return string
342 12
     */
343 12
    public function scriptWithCallback(array $captchas, $callbackName = 'captchaRenderCallback')
344 12
    {
345 3
        $script = $this->script($callbackName);
346 12
347
        if (empty($script) || empty($captchas)) {
348
            return $script;
349
        }
350
351
        return implode(PHP_EOL, [implode(PHP_EOL, [
352
            '<script>',
353
                "var $callbackName = function() {",
354
                    $this->renderCaptchas($captchas),
355
                '};',
356
            '</script>'
357
        ]), $script]);
358 12
    }
359 12
360 12
    /**
361
     * Rendering captchas with callback function.
362
     *
363
     * @param  array  $captchas
364
     *
365
     * @return string
366
     */
367
    private function renderCaptchas(array $captchas)
368
    {
369
        return implode(PHP_EOL, array_map(function($captcha) {
370
            return "if (document.getElementById('$captcha')) { grecaptcha.render('$captcha', {'sitekey' : '{$this->siteKey}'}); }";
371
        }, $captchas));
372 72
    }
373
374 72
    /* ------------------------------------------------------------------------------------------------
375
     |  Check Functions
376
     | ------------------------------------------------------------------------------------------------
377
     */
378
    /**
379
     * Check if has lang.
380
     *
381
     * @return bool
382
     */
383
    private function hasLang()
384 72
    {
385
        return ! empty($this->lang);
386 72
    }
387
388
    /**
389
     * Check if callback is not empty.
390
     *
391
     * @param  string|null  $callbackName
392
     *
393
     * @return bool
394
     */
395
    private function hasCallbackName($callbackName)
396
    {
397 276
        return ! (is_null($callbackName) || trim($callbackName) === '');
398
    }
399 276
400
    /**
401 276
     * Check key.
402
     *
403 276
     * @param  string  $name
404 276
     * @param  string  $value
405
     *
406
     * @throws \Arcanedev\NoCaptcha\Exceptions\ApiException
407
     */
408
    private function checkKey($name, &$value)
409
    {
410
        $this->checkIsString($name, $value);
411
412
        $value = trim($value);
413
414 276
        $this->checkIsNotEmpty($name, $value);
415
    }
416 276
417 24
    /**
418 24
     * Check if the value is a string value.
419 18
     *
420
     * @param  string  $name
421 276
     * @param  string  $value
422
     *
423
     * @throws \Arcanedev\NoCaptcha\Exceptions\ApiException
424
     */
425
    private function checkIsString($name, $value)
426
    {
427
        if ( ! is_string($value)) {
428
            throw new ApiException(
429
                'The ' . $name . ' must be a string value, ' . gettype($value) . ' given'
430
            );
431 276
        }
432
    }
433 276
434 24
    /**
435
     * Check if the value is not empty.
436 276
     *
437
     * @param string  $name
438
     * @param string  $value
439
     *
440
     * @throws \Arcanedev\NoCaptcha\Exceptions\ApiException
441
     */
442
    private function checkIsNotEmpty($name, $value)
443
    {
444
        if (empty($value)) {
445
            throw new ApiException('The ' . $name . ' must not be empty');
446
        }
447
    }
448
449 72
    /* ------------------------------------------------------------------------------------------------
450
     |  Other functions
451 72
     | ------------------------------------------------------------------------------------------------
452 72
     */
453 72
    /**
454
     * Send verify request to API and get response.
455 72
     *
456
     * @param  array  $query
457
     *
458
     * @return array
459
     */
460
    private function sendVerifyRequest(array $query = [])
461
    {
462
        $query    = array_filter($query);
463
        $url      = static::VERIFY_URL . '?' . http_build_query($query);
464
        $response = $this->request->send($url);
465
466
        return $response;
467
    }
468
}
469