Passed
Pull Request — master (#143)
by Arman
05:00 queued 02:30
created

BaseCaptcha::extractAttributes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 5
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 7
rs 10
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 ErrorException;
26
use Exception;
27
28
abstract class BaseCaptcha implements CaptchaInterface
29
{
30
31
    /**
32
     * @var string
33
     */
34
    protected $siteKey;
35
36
    /**
37
     * @var string
38
     */
39
    protected $secretKey;
40
41
    /**
42
     * @var HttpClient
43
     */
44
    protected $http;
45
46
    /**
47
     * @var mixed|null
48
     */
49
    protected $type = null;
50
51
    /**
52
     * @var array
53
     */
54
    protected $errorCodes = [];
55
56
    /**
57
     * @var array
58
     */
59
    protected $elementAttributes = [];
60
61
    /**
62
     * @return string|null
63
     */
64
    public function getType(): ?string
65
    {
66
        return $this->type;
67
    }
68
69
    /**
70
     * @param string $type
71
     * @return CaptchaInterface
72
     * @throws Exception
73
     */
74
    public function setType(string $type): CaptchaInterface
75
    {
76
        if (!$this->isValidCaptchaType($type)) {
77
            throw new Exception('Provided captcha type is not valid');
78
        }
79
80
        $this->type = $type;
81
        return $this;
82
    }
83
84
    /**
85
     * @return string
86
     */
87
    public function getName(): string
88
    {
89
        return $this->name;
90
    }
91
92
    /**
93
     * Generates an HTML code block for the captcha
94
     * @param string $formIdentifier
95
     * @param array $attributes
96
     * @return string
97
     * @throws AssetException
98
     * @throws LangException
99
     * @throws Exception
100
     */
101
    public function addToForm(string $formIdentifier = '', array $attributes = []): string
102
    {
103
        if (!$this->type) {
104
            throw new Exception('Captcha type is not set');
105
        }
106
107
        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...
108
109
        if (strtolower($this->type) == self::CAPTCHA_INVISIBLE) {
110
            return $this->getInvisibleElement($formIdentifier);
111
        } else {
112
            return $this->getVisibleElement($attributes);
113
        }
114
    }
115
116
    /**
117
     * Checks the code given by the captcha
118
     * @param string $code
119
     * @return bool
120
     * @throws LangException
121
     * @throws ErrorException
122
     * @throws AppException
123
     * @throws HttpException
124
     * @throws Exception
125
     */
126
    public function verify(string $code): bool
127
    {
128
        if (is_null($this->secretKey))
0 ignored issues
show
introduced by
The condition is_null($this->secretKey) is always false.
Loading history...
129
            throw new Exception('The secret key is not set');
130
131
        if (empty($code)) {
132
            $this->errorCodes = ['internal-empty-response'];
133
            return false;
134
        }
135
136
        $query = [
137
            'secret' => $this->secretKey,
138
            'response' => $code,
139
            'remoteip' => get_user_ip()
140
        ];
141
142
        $response = $this->http
143
            ->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...
144
            ->setMethod('GET')
145
            ->start()
146
            ->getResponseBody();
147
148
        if (empty($response)) {
149
            $this->errorCodes = ['internal-empty-response'];
150
            return false;
151
        }
152
153
        if (!is_object($response)) {
154
            $this->errorCodes = ['invalid-input-response'];
155
            return false;
156
        }
157
158
        if (isset($response->{'error-codes'}) && is_array($response->{'error-codes'})) {
159
            $this->errorCodes = $response->{'error-codes'};
160
        }
161
162
        if (isset($response->{'challenge_ts'}) && $this->detectReplayAttack($response->{'challenge_ts'})) {
163
            $this->errorCodes = ['replay-attack'];
164
            return false;
165
        }
166
167
        return isset($response->success) && $response->success;
168
    }
169
170
    /**
171
     * @return string|null
172
     */
173
    public function getErrorMessage(): ?string
174
    {
175
        if (!empty($this->errorCodes)) {
176
            return current($this->errorCodes);
177
        }
178
179
        return null;
180
    }
181
182
    /**
183
     * @param array $attributes
184
     * @return string
185
     */
186
    protected function getVisibleElement(array $attributes = []): string
187
    {
188
        $this->extractAttributes($attributes);
189
190
        return '<div class="' . implode(' ', $this->elementClasses) . '" data-sitekey="' . $this->siteKey . '" ' . implode(' ', $this->elementAttributes) . '></div>';
191
    }
192
193
    /**
194
     * @param string $formIdentifier
195
     * @return string
196
     * @throws Exception
197
     */
198
    protected function getInvisibleElement(string $formIdentifier): string
199
    {
200
        if (empty($formIdentifier)) {
201
            throw new Exception('Form identifier is not provided to captcha element');
202
        }
203
204
        return '<script>
205
                 document.addEventListener("DOMContentLoaded", function() {
206
                     const form = document.getElementById("' . $formIdentifier . '");
207
                     const submitButton = form.querySelector("button[type=submit]");
208
                     submitButton.setAttribute("data-sitekey", "' . $this->siteKey . '");
209
                     submitButton.setAttribute("data-callback", "onSubmit");
210
                     submitButton.classList.add("' . reset($this->elementClasses) . '");
211
                 })
212
                function onSubmit (){
213
                    document.getElementById("' . $formIdentifier . '").submit();
214
                }
215
            </script>';
216
    }
217
218
    /**
219
     * @param $type
220
     * @return bool
221
     */
222
    protected function isValidCaptchaType($type): bool
223
    {
224
        $captchaTypes = [
225
            self::CAPTCHA_VISIBLE,
226
            self::CAPTCHA_INVISIBLE
227
        ];
228
229
        return in_array($type, $captchaTypes, true);
230
    }
231
232
    /**
233
     * @param array $attributes
234
     * @return void
235
     */
236
    protected function extractAttributes(array $attributes)
237
    {
238
        foreach ($attributes as $key => $value) {
239
            if ($key == 'class') {
240
                $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...
241
            } else {
242
                $this->elementAttributes[] = $key . '="' . $value . '"';
243
            }
244
        }
245
    }
246
247
    /**
248
     * @param string $challengeTs
249
     * @return bool
250
     */
251
    protected function detectReplayAttack(string $challengeTs): bool
252
    {
253
        if (time() - strtotime($challengeTs) > self::MAX_TIME_DIFF) {
254
            return true;
255
        }
256
257
        return false;
258
    }
259
260
}