Completed
Pull Request — master (#75)
by ARCANEDEV
03:04
created

NoCaptcha::getApiScript()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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