Passed
Pull Request — master (#143)
by Arman
04:03
created

BaseCaptcha::verify()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 42
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 26
c 1
b 0
f 0
nc 10
nop 1
dl 0
loc 42
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 2.9.0
13
 */
14
15
namespace Quantum\Libraries\Captcha\Adapters;
16
17
use Quantum\Libraries\Captcha\CaptchaInterface;
18
use Quantum\Libraries\Asset\AssetManager;
19
use Quantum\Libraries\Curl\HttpClient;
20
use Quantum\Exceptions\AssetException;
21
use Quantum\Exceptions\HttpException;
22
use Quantum\Exceptions\LangException;
23
use Quantum\Exceptions\AppException;
24
use Quantum\Libraries\Asset\Asset;
25
use Exception;
26
27
abstract class BaseCaptcha implements CaptchaInterface
28
{
29
30
    /**
31
     * @var string
32
     */
33
    protected $siteKey;
34
35
    /**
36
     * @var string
37
     */
38
    protected $secretKey;
39
40
    /**
41
     * @var HttpClient
42
     */
43
    protected $http;
44
45
    /**
46
     * @var mixed|null
47
     */
48
    protected $type = null;
49
50
    /**
51
     * @var array
52
     */
53
    protected $errorCodes = [];
54
55
    /**
56
     * @var array
57
     */
58
    protected $elementAttributes = [];
59
60
    /**
61
     * @return string|null
62
     */
63
    public function getType(): ?string
64
    {
65
        return $this->type;
66
    }
67
68
    /**
69
     * @param string $type
70
     * @return CaptchaInterface
71
     * @throws Exception
72
     */
73
    public function setType(string $type): CaptchaInterface
74
    {
75
        if (!$this->isValidCaptchaType($type)) {
76
            throw new Exception('Provided captcha type is not valid');
77
        }
78
79
        $this->type = $type;
80
        return $this;
81
    }
82
83
    /**
84
     * @return string
85
     */
86
    public function getName(): string
87
    {
88
        return $this->name;
89
    }
90
91
    /**
92
     * Generates an HTML code block for the captcha
93
     * @param string $formIdentifier
94
     * @param array $attributes
95
     * @return string
96
     * @throws AssetException
97
     * @throws LangException
98
     * @throws Exception
99
     */
100
    public function addToForm(string $formIdentifier = '', array $attributes = []): string
101
    {
102
        if (!$this->type) {
103
            throw new Exception('Captcha type is not set');
104
        }
105
106
        AssetManager::getInstance()->registerAsset(new Asset(Asset::JS, static::CLIENT_API, 'captcha', -1, ['async', 'defer']));
0 ignored issues
show
Bug introduced by
The constant Quantum\Libraries\Captch...BaseCaptcha::CLIENT_API was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
107
108
        if (strtolower($this->type) == self::CAPTCHA_INVISIBLE) {
109
            return $this->getInvisibleElement($formIdentifier);
110
        } else {
111
            return $this->getVisibleElement($attributes);
112
        }
113
    }
114
115
    /**
116
     * Checks the code given by the captcha
117
     * @param string $code
118
     * @return bool
119
     * @throws LangException
120
     * @throws ErrorException
121
     * @throws AppException
122
     * @throws HttpException
123
     * @throws Exception
124
     */
125
    public function verify(string $code): bool
126
    {
127
        if (is_null($this->secretKey))
0 ignored issues
show
introduced by
The condition is_null($this->secretKey) is always false.
Loading history...
128
            throw new Exception('The secret key is not set');
129
130
        if (empty($code)) {
131
            $this->errorCodes = ['internal-empty-response'];
132
            return false;
133
        }
134
135
        $query = [
136
            'secret' => $this->secretKey,
137
            'response' => $code,
138
            'remoteip' => get_user_ip()
139
        ];
140
141
        $response = $this->http
142
            ->createRequest(static::VERIFY_URL . '?' . http_build_query($query))
0 ignored issues
show
Bug introduced by
The constant Quantum\Libraries\Captch...BaseCaptcha::VERIFY_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
143
            ->setMethod('GET')
144
            ->start()
145
            ->getResponseBody();
146
147
        if (empty($response)) {
148
            $this->errorCodes = ['internal-empty-response'];
149
            return false;
150
        }
151
152
        if (!is_object($response)) {
153
            $this->errorCodes = ['invalid-input-response'];
154
            return false;
155
        }
156
157
        if (isset($response->{'error-codes'}) && is_array($response->{'error-codes'})) {
158
            $this->errorCodes = $response->{'error-codes'};
159
        }
160
161
        if (isset($response->{'challenge_ts'}) && $this->detectReplayAttack($response->{'challenge_ts'})) {
162
            $this->errorCodes = ['replay-attack'];
163
            return false;
164
        }
165
166
        return isset($response->success) && $response->success;
167
    }
168
169
    /**
170
     * @return string|null
171
     */
172
    public function getErrorMessage(): ?string
173
    {
174
        if (!empty($this->errorCodes)) {
175
            return current($this->errorCodes);
176
        }
177
178
        return null;
179
    }
180
181
    /**
182
     * @param array $attributes
183
     * @return string
184
     */
185
    protected function getVisibleElement(array $attributes = []): string
186
    {
187
        $this->extractAttributes($attributes);
188
189
        return '<div class="' . implode(' ', $this->elementClasses) . '" data-sitekey="' . $this->siteKey . '" ' . implode(' ', $this->elementAttributes) . '></div>';
190
    }
191
192
    /**
193
     * @param string $formIdentifier
194
     * @return string
195
     * @throws Exception
196
     */
197
    protected function getInvisibleElement(string $formIdentifier): string
198
    {
199
        if (empty($formIdentifier)) {
200
            throw new Exception('Form identifier is not provided to captcha element');
201
        }
202
203
        return '<script>
204
                 document.addEventListener("DOMContentLoaded", function() {
205
                     const form = document.getElementById("' . $formIdentifier . '");
206
                     const submitButton = form.querySelector("button[type=submit]");
207
                     submitButton.setAttribute("data-sitekey", "' . $this->siteKey . '");
208
                     submitButton.setAttribute("data-callback", "onSubmit");
209
                     submitButton.classList.add("' . reset($this->elementClasses) . '");
210
                 })
211
                function onSubmit (){
212
                    document.getElementById("' . $formIdentifier . '").submit();
213
                }
214
            </script>';
215
    }
216
217
    /**
218
     * @param $type
219
     * @return bool
220
     */
221
    protected function isValidCaptchaType($type): bool
222
    {
223
        $captchaTypes = [
224
            self::CAPTCHA_VISIBLE,
225
            self::CAPTCHA_INVISIBLE
226
        ];
227
228
        return in_array($type, $captchaTypes, true);
229
    }
230
231
    /**
232
     * @param array $attributes
233
     * @return void
234
     */
235
    protected function extractAttributes(array $attributes)
236
    {
237
        foreach ($attributes as $key => $value) {
238
            if ($key == 'class') {
239
                $this->elementClasses[] = $value;
0 ignored issues
show
Bug Best Practice introduced by
The property elementClasses does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
240
            } else {
241
                $this->elementAttributes[] = $key . '="' . $value . '"';
242
            }
243
        }
244
    }
245
246
    /**
247
     * @param string $challengeTs
248
     * @return bool
249
     */
250
    protected function detectReplayAttack(string $challengeTs): bool
251
    {
252
        if (time() - strtotime($challengeTs) > self::MAX_TIME_DIFF) {
253
            return true;
254
        }
255
256
        return false;
257
    }
258
259
}