Passed
Pull Request — master (#135)
by
unknown
03:34
created

RecaptchaAdapter::verifyResponse()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 1
b 0
f 0
nc 5
nop 2
dl 0
loc 32
rs 8.8333
1
<?php
2
3
namespace Quantum\Libraries\Captcha\Adapters;
4
5
use Quantum\Libraries\Captcha\CaptchaInterface;
6
use Quantum\Libraries\Curl\HttpClient;
7
8
class RecaptchaAdapter implements CaptchaInterface
9
{
10
    /**
11
     * ReCAPTCHA URL verifying
12
     *
13
     * @var string
14
     */
15
    const VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
16
17
    const CLIENT_API = 'https://www.google.com/recaptcha/api.js';
18
19
    /**
20
     * Public key
21
     *
22
     * @var string
23
     */
24
    private $sitekey;
25
26
    /**
27
     * Private key
28
     *
29
     * @var string
30
     */
31
    private $secretkey;
32
33
    private $http;
34
35
    /**
36
     * Remote IP address
37
     *
38
     * @var string
39
     */
40
    protected $remoteIp = null;
41
42
    /**
43
     * Supported types
44
     *
45
     * @var array
46
     * @see https://developers.google.com/recaptcha/docs/display#config
47
     */
48
    protected static $types = array('image', 'audio');
49
50
    /**
51
     * Captcha type. Default : image
52
     *
53
     * @var string
54
     * @see https://developers.google.com/recaptcha/docs/display#config
55
     */
56
    protected $type = null;
57
58
    /**
59
     * Captcha language. Default : auto-detect
60
     *
61
     * @var string
62
     * @see https://developers.google.com/recaptcha/docs/language
63
     */
64
    protected $language = null;
65
66
67
    /**
68
     * Captcha size. Default : normal
69
     *
70
     * @var string
71
     * @see https://developers.google.com/recaptcha/docs/display#render_param
72
     */
73
    protected $size = null;
74
75
    private static $instance = null;
76
77
    /**
78
     * List of errors
79
     *
80
     * @var array
81
     */
82
    protected $errorCodes = array();
83
84
85
    /**
86
     * RecaptchaAdapter
87
     *
88
     * @param array $params
89
     * @return void
90
     */
91
    private function __construct(array $params)
92
    {
93
        $this->http = new HttpClient();
94
95
        $this->secretkey = $params['secret_key'];
96
        $this->sitekey = $params['site_key'];
97
        $this->type = $params['type'];
98
    }
99
100
    /**
101
     * Get Instance
102
     * @param array $params
103
     * @return RecaptchaAdapter
104
     */
105
    public static function getInstance(array $params): RecaptchaAdapter
106
    {
107
        if (self::$instance === null) {
108
            self::$instance = new self($params);
109
        }
110
111
        return self::$instance;
112
    }
113
114
    /**
115
     * Generate the JS code of the captcha
116
     *
117
     * @param null $lang
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $lang is correct as it would always require null to be passed?
Loading history...
118
     * @param bool $callback
119
     * @param string $onLoadClass
120
     * @return string
121
     */
122
    public function renderJs($lang = null, $callback = false, $onLoadClass = 'onloadCallBack'): string
123
    {
124
        return '<script src="'. self::CLIENT_API .'"></script>';
125
    }
126
127
    /**
128
     * Generate the HTML code block for the captcha
129
     *
130
     * @param string $formIdentifier
131
     * @param array $attributes
132
     * @return string
133
     */
134
    public function display(string $formIdentifier = '', array $attributes = []): string
135
    {
136
        $captchaElement = '';
137
        if (strtolower($this->type) == 'visible'){
138
            $captchaElement = $this->getVisibleElement();
139
        } elseif (strtolower($this->type) == 'invisible') {
140
            $captchaElement = $this->getInvisibleElement($formIdentifier);
141
        }
142
        return $captchaElement;
143
    }
144
145
    /**
146
     * Checks the code given by the captcha
147
     *
148
     * @param string $response Response code after submitting form (usually $_POST['g-recaptcha-response'])
149
     * @return bool
150
     */
151
    public function verifyResponse(string $response, $clientIp = null): bool
152
    {
153
        if (is_null($this->secretkey))
0 ignored issues
show
introduced by
The condition is_null($this->secretkey) is always false.
Loading history...
154
            throw new \Exception('You must set your secret key');
155
156
        if (empty($response)) {
157
158
            $this->errorCodes = array('internal-empty-response');
159
160
            return false;
161
        }
162
163
        $query = array(
164
            'secret' => $this->secretkey,
165
            'response' => $response,
166
            'remoteip' => $this->remoteIp,
167
        );
168
169
        $url = self::VERIFY_URL . '?' . http_build_query($query);
170
171
        $this->http->createRequest($url)->setMethod('GET')->start();
172
        $response = (array)$this->http->getResponseBody();
173
174
        if (empty($response) || is_null($response) || !$response) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $response of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
175
            return false;
176
        }
177
178
        if (isset($response['error-codes'])) {
179
            $this->errorCodes = $response['error-codes'];
180
        }
181
182
        return $response['success'];
183
    }
184
185
    /**
186
     * Returns the errors encountered
187
     *
188
     * @return array Errors code and name
189
     */
190
    public function getErrorCodes(): array
191
    {
192
        $errors = array();
193
194
        if (count($this->errorCodes) > 0) {
195
            foreach ($this->errorCodes as $error) {
196
                switch ($error) {
197
                    case 'timeout-or-duplicate':
198
                        $errors[] = array(
199
                            'code' => $error,
200
                            'name' => 'Timeout or duplicate.',
201
                        );
202
                        break;
203
204
                    case 'missing-input-secret':
205
                        $errors[] = array(
206
                            'code' => $error,
207
                            'name' => 'The secret parameter is missing.',
208
                        );
209
                        break;
210
211
                    case 'invalid-input-secret':
212
                        $errors[] = array(
213
                            'code' => $error,
214
                            'name' => 'The secret parameter is invalid or malformed.',
215
                        );
216
                        break;
217
218
                    case 'missing-input-response':
219
                        $errors[] = array(
220
                            'code' => $error,
221
                            'name' => 'The response parameter is missing.',
222
                        );
223
                        break;
224
225
                    case 'invalid-input-response':
226
                        $errors[] = array(
227
                            'code' => $error,
228
                            'name' => 'The response parameter is invalid or malformed.',
229
                        );
230
                        break;
231
232
                    case 'bad-request':
233
                        $errors[] = array(
234
                            'code' => $error,
235
                            'name' => 'The request is invalid or malformed.',
236
                        );
237
                        break;
238
239
                    case 'internal-empty-response':
240
                        $errors[] = array(
241
                            'code' => $error,
242
                            'name' => 'The recaptcha response is required.',
243
                        );
244
                        break;
245
246
                    default:
247
                        $errors[] = array(
248
                            'code' => $error,
249
                            'name' => $error,
250
                        );
251
                }
252
            }
253
        }
254
255
        return $errors;
256
    }
257
258
    private function getInvisibleElement(string $formIdentifier): string
259
    {
260
        return '<script>
261
                 document.addEventListener("DOMContentLoaded", function() {
262
                    let button = document.getElementsByTagName("button");
263
                
264
                    button[0].setAttribute("data-sitekey", "' . $this->sitekey . '");
265
                    button[0].setAttribute("data-callback", "onSubmit");
266
                    button[0].setAttribute("data-action", "submit");
267
                    button[0].classList.add("g-recaptcha");
268
                 })
269
                
270
                function onSubmit (){
271
                    document.getElementById("'. $formIdentifier .'").submit();
272
                }
273
            </script>';
274
    }
275
276
    private function getVisibleElement(): string
277
    {
278
        $data = 'data-sitekey="' . $this->sitekey . '"';
279
280
        return '<div class="col s1 offset-s2 g-recaptcha" ' . $data . '></div>';
281
    }
282
}