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

HcaptchaAdapter::getInstance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
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 HcaptchaAdapter implements CaptchaInterface
9
{
10
    const CLIENT_API = 'https://hcaptcha.com/1/api.js';
11
    const VERIFY_URL = 'https://hcaptcha.com/siteverify';
12
13
    /**
14
     * The hCaptcha secret key.
15
     *
16
     * @var string
17
     */
18
    protected $secretkey;
19
20
    /**
21
     * The hCaptcha sitekey key.
22
     *
23
     * @var string
24
     */
25
    protected $sitekey;
26
27
    protected $type;
28
29
    /**
30
     * The cached verified responses.
31
     *
32
     * @var array
33
     */
34
    protected $verifiedResponses = [];
35
36
    private static $instance = null;
37
38
    protected $http;
39
40
    /**
41
     * HcaptchaAdapter.
42
     *
43
     * @param array $params
44
     */
45
    private function __construct(array $params)
46
    {
47
        $this->http = new HttpClient();
48
        
49
        $this->secretkey = $params['secret_key'];
50
        $this->sitekey = $params['site_key'];
51
        $this->type = $params['type'];
52
    }
53
54
    /**
55
     * Get Instance
56
     * @param array $params
57
     * @return HcaptchaAdapter
58
     */
59
    public static function getInstance(array $params): HcaptchaAdapter
60
    {
61
        if (self::$instance === null) {
62
            self::$instance = new self($params);
63
        }
64
65
        return self::$instance;
66
    }
67
68
    /**
69
     * Render HTML captcha.
70
     *
71
     * @param array $attributes
72
     *
73
     * @return string
74
     */
75
    public function display(string $formIdentifier = '', array $attributes = []): string
76
    {
77
        $captchaElement = '';
78
        if (strtolower($this->type) == 'visible'){
79
            $captchaElement = $this->getVisibleElement($attributes);
80
        } elseif (strtolower($this->type) == 'invisible') {
81
            $captchaElement = $this->getInvisibleElement($formIdentifier);
82
        }
83
        return $captchaElement;
84
    }
85
86
    /**
87
     * Render js source
88
     *
89
     * @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...
90
     * @param bool   $callback
91
     * @param string $onLoadClass
92
     *
93
     * @return string
94
     */
95
    public function renderJs($lang = null, $callback = false, $onLoadClass = 'onloadCallBack'): string
96
    {
97
        return '<script src="' . $this->getJsLink($lang, $callback, $onLoadClass) . '" async defer></script>' . "\n";
98
    }
99
100
    /**
101
     * Verify hCaptcha response.
102
     *
103
     * @param string $response
104
     * @param string $clientIp
105
     *
106
     * @return bool
107
     */
108
    public function verifyResponse(string $response, string $clientIp = null): bool
109
    {
110
        if (empty($response)) {
111
            return false;
112
        }
113
114
        // Return true if response already verfied before.
115
        if (in_array($response, $this->verifiedResponses)) {
116
            return true;
117
        }
118
119
        $verifyResponse = $this->sendRequestVerify([
120
            'secret'   => $this->secretkey,
121
            'response' => $response,
122
        ]);
123
124
        if (isset($verifyResponse['success']) && $verifyResponse['success'] === true) {
125
            // A response can only be verified once from hCaptcha, so we need to
126
            // cache it to make it work in case we want to verify it multiple times.
127
            $this->verifiedResponses[] = $response;
128
            return true;
129
        } else {
130
            return false;
131
        }
132
    }
133
134
    /**
135
     * Get hCaptcha js link.
136
     *
137
     * @param string  $lang
138
     * @param boolean $callback
139
     * @param string  $onLoadClass
140
     *
141
     * @return string
142
     */
143
    public function getJsLink(string $lang = null, bool $callback = false, string $onLoadClass = 'onloadCallBack'): string
144
    {
145
        $client_api = static::CLIENT_API;
146
        $params = [];
147
148
        $callback ? $this->setCallBackParams($params, $onLoadClass) : false;
149
        $lang ? $params['hl'] = $lang : null;
150
151
        return $client_api . '?' . http_build_query($params);
152
    }
153
154
    /**
155
     * @param $params
156
     * @param $onLoadClass
157
     */
158
    protected function setCallBackParams(&$params, $onLoadClass)
159
    {
160
        $params['render'] = 'explicit';
161
        $params['onload'] = $onLoadClass;
162
    }
163
164
    /**
165
     * Send verify request.
166
     *
167
     * @param array $query
168
     *
169
     * @return array
170
     */
171
    protected function sendRequestVerify(array $query = []): array
172
    {
173
        $this->http->createRequest(static::VERIFY_URL)->setMethod('POST')->setData($query)->start();
174
175
        return (array)$this->http->getResponseBody();
176
    }
177
178
    /**
179
     * Prepare HTML attributes and assure that the correct classes and attributes for captcha are inserted.
180
     *
181
     * @param array $attributes
182
     *
183
     * @return array
184
     */
185
    protected function prepareAttributes(array $attributes): array
186
    {
187
        $attributes['data-sitekey'] = $this->sitekey;
188
        if (!isset($attributes['class'])) {
189
            $attributes['class'] = '';
190
        }
191
        $attributes['class'] = trim('h-captcha ' . $attributes['class']);
192
193
        return $attributes;
194
    }
195
196
    /**
197
     * Build HTML attributes.
198
     *
199
     * @param array $attributes
200
     *
201
     * @return string
202
     */
203
    protected function buildAttributes(array $attributes): string
204
    {
205
        $html = [];
206
207
        foreach ($attributes as $key => $value) {
208
            $html[] = $key . '="' . $value . '"';
209
        }
210
211
        return count($html) ? ' ' . implode(' ', $html) : '';
212
    }
213
214
    private function getInvisibleElement($formIdentifier): string
215
    {
216
        $functionName = 'onSubmit' . str_replace(['-', '=', '\'', '"', '<', '>', '`'], '', $formIdentifier);
217
218
        return '<script>
219
                     document.addEventListener("DOMContentLoaded", function() {
220
                        let button = document.getElementsByTagName("button");
221
                    
222
                        button[0].setAttribute("data-sitekey", "' . $this->sitekey . '");
223
                        button[0].setAttribute("data-callback", "'. $functionName .'");
224
                        button[0].classList.add("h-captcha");
225
                     })
226
                    
227
                    function '. $functionName .'(){
228
                        document.getElementById("'. $formIdentifier .'").submit();
229
                    }
230
                </script>';
231
    }
232
233
    private function getVisibleElement($attributes): string
234
    {
235
        $attributes = $this->prepareAttributes($attributes);
236
        return '<div' . $this->buildAttributes($attributes) . '></div>';
237
    }
238
}