├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src
├── ReCaptcha.php
├── ReCaptcha2.php
├── ReCaptcha3.php
├── ReCaptchaBaseValidator.php
├── ReCaptchaConfig.php
├── ReCaptchaValidator.php
├── ReCaptchaValidator2.php
└── ReCaptchaValidator3.php
└── tests
├── ReCaptchaValidatorTest.php
└── bootstrap.php
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014-2019 HimikLab
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Google reCAPTCHA widget for Yii2
2 | ================================
3 | Based on Google reCaptcha API 2.0 and 3.0.
4 |
5 | []() []() []()
6 |
7 | Upgrade to 2.x version
8 | ------------
9 | Warning! Classes `ReCaptcha` and `ReCaptchaValidator` is deprecated. Please replace their to `ReCaptchaConfig`,
10 | `ReCaptcha2` and `ReCaptchaValidator2`.
11 |
12 | Installation
13 | ------------
14 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
15 |
16 | * Either run
17 |
18 | ```
19 | php composer.phar require --prefer-dist "himiklab/yii2-recaptcha-widget" "*"
20 | ```
21 |
22 | or add
23 |
24 | ```json
25 | "himiklab/yii2-recaptcha-widget" : "*"
26 | ```
27 |
28 | to the `require` section of your application's `composer.json` file.
29 |
30 | * [Sign up for an reCAPTCHA API keys](https://www.google.com/recaptcha/admin/create).
31 |
32 | * Configure the component in your configuration file (web.php). The parameters siteKey and secret are optional.
33 | But if you leave them out you need to set them in every validation rule and every view where you want to use this widget.
34 | If a siteKey or secret is set in an individual view or validation rule that would overrule what is set in the config.
35 |
36 | ```php
37 | 'components' => [
38 | 'reCaptcha' => [
39 | 'class' => 'himiklab\yii2\recaptcha\ReCaptchaConfig',
40 | 'siteKeyV2' => 'your siteKey v2',
41 | 'secretV2' => 'your secret key v2',
42 | 'siteKeyV3' => 'your siteKey v3',
43 | 'secretV3' => 'your secret key v3',
44 | ],
45 | ...
46 | ```
47 |
48 | or use DI container:
49 |
50 | ```php
51 | 'container' => [
52 | 'definitions' => [
53 | himiklab\yii2\recaptcha\ReCaptcha2::className() => function ($container, $params, $config) {
54 | return new himiklab\yii2\recaptcha\ReCaptcha2(
55 | 'your siteKey v2',
56 | '', // default
57 | $config
58 | );
59 | },
60 | himiklab\yii2\recaptcha\ReCaptchaValidator2::className() => function ($container, $params, $config) {
61 | return new himiklab\yii2\recaptcha\ReCaptchaValidator2(
62 | 'your secret key v2',
63 | '', // default
64 | null, // default
65 | null, // default
66 | $config
67 | );
68 | },
69 | ],
70 | ],
71 | ```
72 |
73 | * Add `ReCaptchaValidator2` or `ReCaptchaValidator3` in your model, for example:
74 |
75 | v2
76 | ```php
77 | public $reCaptcha;
78 |
79 | public function rules()
80 | {
81 | return [
82 | // ...
83 | [['reCaptcha'], \himiklab\yii2\recaptcha\ReCaptchaValidator2::className(),
84 | 'secret' => 'your secret key', // unnecessary if reСaptcha is already configured
85 | 'uncheckedMessage' => 'Please confirm that you are not a bot.'],
86 | ];
87 | }
88 | ```
89 |
90 | v3
91 | ```php
92 | public $reCaptcha;
93 |
94 | public function rules()
95 | {
96 | return [
97 | // ...
98 | [['reCaptcha'], \himiklab\yii2\recaptcha\ReCaptchaValidator3::className(),
99 | 'secret' => 'your secret key', // unnecessary if reСaptcha is already configured
100 | 'threshold' => 0.5,
101 | 'action' => 'homepage',
102 | ],
103 | ];
104 | }
105 | ```
106 |
107 | Usage
108 | -----
109 | For example:
110 |
111 | v2
112 | ```php
113 | = $form->field($model, 'reCaptcha')->widget(
114 | \himiklab\yii2\recaptcha\ReCaptcha2::className(),
115 | [
116 | 'siteKey' => 'your siteKey', // unnecessary is reCaptcha component was set up
117 | ]
118 | ) ?>
119 | ```
120 |
121 | v3
122 | ```php
123 | = $form->field($model, 'reCaptcha')->widget(
124 | \himiklab\yii2\recaptcha\ReCaptcha3::className(),
125 | [
126 | 'siteKey' => 'your siteKey', // unnecessary is reCaptcha component was set up
127 | 'action' => 'homepage',
128 | ]
129 | ) ?>
130 | ```
131 |
132 | or
133 |
134 | v2
135 | ```php
136 | = \himiklab\yii2\recaptcha\ReCaptcha2::widget([
137 | 'name' => 'reCaptcha',
138 | 'siteKey' => 'your siteKey', // unnecessary is reCaptcha component was set up
139 | 'widgetOptions' => ['class' => 'col-sm-offset-3'],
140 | ]) ?>
141 | ```
142 |
143 | v3
144 | ```php
145 | = \himiklab\yii2\recaptcha\ReCaptcha3::widget([
146 | 'name' => 'reCaptcha',
147 | 'siteKey' => 'your siteKey', // unnecessary is reCaptcha component was set up
148 | 'action' => 'homepage',
149 | 'widgetOptions' => ['class' => 'col-sm-offset-3'],
150 | ]) ?>
151 | ```
152 |
153 | * NOTE: Please disable ajax validation for ReCaptcha field!
154 |
155 | Resources
156 | ---------
157 | * [Google reCAPTCHA v2](https://developers.google.com/recaptcha)
158 | * [Google reCAPTCHA v3](https://developers.google.com/recaptcha/docs/v3)
159 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "himiklab/yii2-recaptcha-widget",
3 | "description": "Yii2 Google reCAPTCHA v2 and v3 widget",
4 | "keywords": ["yii2", "captcha", "recaptcha", "google", "widget"],
5 | "type": "yii2-extension",
6 | "license": "MIT",
7 | "support": {
8 | "source": "https://github.com/himiklab/yii2-recaptcha-widget",
9 | "issues": "https://github.com/himiklab/yii2-recaptcha-widget/issues"
10 | },
11 | "authors": [
12 | {
13 | "name": "HimikLab",
14 | "homepage": "https://github.com/himiklab/"
15 | }
16 | ],
17 | "require": {
18 | "yiisoft/yii2": "~2.0.11",
19 | "yiisoft/yii2-httpclient": "*"
20 | },
21 | "require-dev": {
22 | "phpunit/phpunit": "^4.8.34"
23 | },
24 | "autoload": {
25 | "psr-4": {
26 | "himiklab\\yii2\\recaptcha\\": "src/"
27 | }
28 | },
29 | "autoload-dev": {
30 | "psr-4": {
31 | "himiklab\\yii2\\recaptcha\\tests\\": "tests/"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | ./tests
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/ReCaptcha.php:
--------------------------------------------------------------------------------
1 | field($model, 'reCaptcha')->widget(
20 | * ReCaptcha::className(),
21 | * ['siteKey' => 'your siteKey']
22 | * ) ?>
23 | * ```
24 | *
25 | * or
26 | *
27 | * ```php
28 | * = ReCaptcha::widget([
29 | * 'name' => 'reCaptcha',
30 | * 'siteKey' => 'your siteKey',
31 | * 'widgetOptions' => ['class' => 'col-sm-offset-3']
32 | * ]) ?>
33 | * ```
34 | *
35 | * @see https://developers.google.com/recaptcha
36 | * @author HimikLab
37 | * @package himiklab\yii2\recaptcha
38 | * @deprecated
39 | */
40 | class ReCaptcha extends ReCaptcha2
41 | {
42 | const JS_API_URL_DEFAULT = '//www.google.com/recaptcha/api.js';
43 | const JS_API_URL_ALTERNATIVE = '//www.recaptcha.net/recaptcha/api.js';
44 |
45 | const SIZE_INVISIBLE = 'invisible';
46 |
47 | /** @var string Your secret. */
48 | public $secret;
49 |
50 | /** @var string Use [[SITE_VERIFY_URL_ALTERNATIVE]] when [[SITE_VERIFY_URL_DEFAULT]] is not accessible. */
51 | public $siteVerifyUrl;
52 |
53 | /** @var boolean */
54 | public $checkHostName = false;
55 |
56 | /** @var \yii\httpclient\Request */
57 | public $httpClientRequest;
58 |
59 | public function init()
60 | {
61 | }
62 |
63 | public function run()
64 | {
65 | /** @var self $reCaptchaConfig */
66 | $reCaptchaConfig = Yii::$app->get('reCaptcha', false);
67 |
68 | if (!$this->siteKey) {
69 | if ($reCaptchaConfig && $reCaptchaConfig->siteKey) {
70 | $this->siteKey = $reCaptchaConfig->siteKey;
71 | } else {
72 | throw new InvalidConfigException('Required `siteKey` param isn\'t set.');
73 | }
74 | }
75 | if (!$this->jsApiUrl) {
76 | if ($reCaptchaConfig && $reCaptchaConfig->jsApiUrl) {
77 | $this->jsApiUrl = $reCaptchaConfig->jsApiUrl;
78 | } else {
79 | $this->jsApiUrl = self::JS_API_URL_DEFAULT;
80 | }
81 | }
82 |
83 | parent::run();
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/ReCaptcha2.php:
--------------------------------------------------------------------------------
1 | field($model, 'reCaptcha')->widget(
22 | * ReCaptcha::className(),
23 | * [
24 | * 'siteKey' => 'your siteKey' // unnecessary is reCaptcha component was set up
25 | * ]
26 | * ) ?>
27 | * ```
28 | *
29 | * or
30 | *
31 | * ```php
32 | * = ReCaptcha::widget([
33 | * 'name' => 'reCaptcha',
34 | * 'siteKey' => 'your siteKey', // unnecessary is reCaptcha component was set up
35 | * 'widgetOptions' => ['class' => 'col-sm-offset-3']
36 | * ]) ?>
37 | * ```
38 | *
39 | * @see https://developers.google.com/recaptcha
40 | * @author HimikLab
41 | * @package himiklab\yii2\recaptcha
42 | */
43 | class ReCaptcha2 extends InputWidget
44 | {
45 | const THEME_LIGHT = 'light';
46 | const THEME_DARK = 'dark';
47 |
48 | const TYPE_IMAGE = 'image';
49 | const TYPE_AUDIO = 'audio';
50 |
51 | const SIZE_NORMAL = 'normal';
52 | const SIZE_COMPACT = 'compact';
53 |
54 | /** @var string Your sitekey. */
55 | public $siteKey;
56 |
57 | /**
58 | * @var string Use [[ReCaptchaConfig::JS_API_URL_ALTERNATIVE]] when [[ReCaptchaConfig::JS_API_URL_DEFAULT]]
59 | * is not accessible.
60 | */
61 | public $jsApiUrl;
62 |
63 | /** @var string The color theme of the widget. [[THEME_LIGHT]] (default) or [[THEME_DARK]] */
64 | public $theme;
65 |
66 | /** @var string The type of CAPTCHA to serve. [[TYPE_IMAGE]] (default) or [[TYPE_AUDIO]] */
67 | public $type;
68 |
69 | /** @var string The size of the widget. [[SIZE_NORMAL]] (default) or [[SIZE_COMPACT]] */
70 | public $size;
71 |
72 | /** @var integer The tabindex of the widget */
73 | public $tabIndex;
74 |
75 | /** @var string Your JS callback function that's executed when the user submits a successful reCAPTCHA response. */
76 | public $jsCallback;
77 |
78 | /**
79 | * @var string Your JS callback function that's executed when the recaptcha response expires and the user
80 | * needs to solve a new CAPTCHA.
81 | */
82 | public $jsExpiredCallback;
83 |
84 | /** @var string Your JS callback function that's executed when reCAPTCHA encounters an error (usually network
85 | * connectivity) and cannot continue until connectivity is restored. If you specify a function here, you are
86 | * responsible for informing the user that they should retry.
87 | */
88 | public $jsErrorCallback;
89 |
90 | /** @var string */
91 | public $configComponentName = 'reCaptcha';
92 |
93 | /** @var array Additional html widget options, such as `class`. */
94 | public $widgetOptions = [];
95 |
96 | public function __construct($siteKey = null, $jsApiUrl = null, $config = [])
97 | {
98 | if ($siteKey && !$this->siteKey) {
99 | $this->siteKey = $siteKey;
100 | }
101 | if ($jsApiUrl && !$this->jsApiUrl) {
102 | $this->jsApiUrl = $jsApiUrl;
103 | }
104 |
105 | parent::__construct($config);
106 | }
107 |
108 | public function init()
109 | {
110 | parent::init();
111 | $this->configComponentProcess();
112 | }
113 |
114 | public function run()
115 | {
116 | parent::run();
117 | $view = $this->view;
118 | $arguments = \http_build_query([
119 | 'hl' => $this->getLanguageSuffix(),
120 | 'render' => 'explicit',
121 | 'onload' => 'recaptchaOnloadCallback',
122 | ]);
123 |
124 | $view->registerJsFile(
125 | $this->jsApiUrl . '?' . $arguments,
126 | ['position' => $view::POS_END, 'async' => true, 'defer' => true]
127 | );
128 | $view->registerJs(
129 | <<<'JS'
130 | function recaptchaOnloadCallback() {
131 | "use strict";
132 | jQuery(".g-recaptcha").each(function () {
133 | const reCaptcha = jQuery(this);
134 | if (reCaptcha.data("recaptcha-client-id") === undefined) {
135 | const recaptchaClientId = grecaptcha.render(reCaptcha.attr("id"), {
136 | "callback": function (response) {
137 | if (reCaptcha.data("form-id") !== "") {
138 | jQuery("#" + reCaptcha.data("input-id"), "#" + reCaptcha.data("form-id")).val(response)
139 | .trigger("change");
140 | } else {
141 | jQuery("#" + reCaptcha.data("input-id")).val(response).trigger("change");
142 | }
143 |
144 | if (reCaptcha.attr("data-callback")) {
145 | eval("(" + reCaptcha.attr("data-callback") + ")(response)");
146 | }
147 | },
148 | "expired-callback": function () {
149 | if (reCaptcha.data("form-id") !== "") {
150 | jQuery("#" + reCaptcha.data("input-id"), "#" + reCaptcha.data("form-id")).val("");
151 | } else {
152 | jQuery("#" + reCaptcha.data("input-id")).val("");
153 | }
154 |
155 | if (reCaptcha.attr("data-expired-callback")) {
156 | eval("(" + reCaptcha.attr("data-expired-callback") + ")()");
157 | }
158 | },
159 | });
160 | reCaptcha.data("recaptcha-client-id", recaptchaClientId);
161 | }
162 | });
163 | }
164 | JS
165 | , $view::POS_END);
166 |
167 | if (Yii::$app->request->isAjax) {
168 | $view->registerJs(<<<'JS'
169 | if (typeof grecaptcha !== "undefined") {
170 | recaptchaOnloadCallback();
171 | }
172 | JS
173 | , $view::POS_END
174 | );
175 | }
176 |
177 | $this->customFieldPrepare();
178 | echo Html::tag('div', '', $this->buildDivOptions());
179 | }
180 |
181 | protected function getReCaptchaId()
182 | {
183 | if (isset($this->widgetOptions['id'])) {
184 | return $this->widgetOptions['id'];
185 | }
186 |
187 | if ($this->hasModel()) {
188 | return Html::getInputId($this->model, $this->attribute);
189 | }
190 |
191 | return $this->id . '-' . $this->inputNameToId($this->name);
192 | }
193 |
194 | protected function getLanguageSuffix()
195 | {
196 | $currentAppLanguage = Yii::$app->language;
197 | $langsExceptions = ['zh-CN', 'zh-TW', 'zh-TW'];
198 |
199 | if (\strpos($currentAppLanguage, '-') === false) {
200 | return $currentAppLanguage;
201 | }
202 |
203 | if (\in_array($currentAppLanguage, $langsExceptions)) {
204 | return $currentAppLanguage;
205 | }
206 |
207 | return \substr($currentAppLanguage, 0, \strpos($currentAppLanguage, '-'));
208 | }
209 |
210 | protected function customFieldPrepare()
211 | {
212 | $inputId = $this->getReCaptchaId();
213 |
214 | if ($this->hasModel()) {
215 | $inputName = Html::getInputName($this->model, $this->attribute);
216 | } else {
217 | $inputName = $this->name;
218 | }
219 |
220 | $options = $this->options;
221 | $options['id'] = $inputId;
222 |
223 | echo Html::input('hidden', $inputName, null, $options);
224 | }
225 |
226 | protected function buildDivOptions()
227 | {
228 | $divOptions = [
229 | 'class' => 'g-recaptcha',
230 | 'data-sitekey' => $this->siteKey
231 | ];
232 | $divOptions += $this->widgetOptions;
233 |
234 | if ($this->jsCallback) {
235 | $divOptions['data-callback'] = $this->jsCallback;
236 | }
237 | if ($this->jsExpiredCallback) {
238 | $divOptions['data-expired-callback'] = $this->jsExpiredCallback;
239 | }
240 | if ($this->jsErrorCallback) {
241 | $divOptions['data-error-callback'] = $this->jsErrorCallback;
242 | }
243 | if ($this->theme) {
244 | $divOptions['data-theme'] = $this->theme;
245 | }
246 | if ($this->type) {
247 | $divOptions['data-type'] = $this->type;
248 | }
249 | if ($this->size) {
250 | $divOptions['data-size'] = $this->size;
251 | }
252 | if ($this->tabIndex) {
253 | $divOptions['data-tabindex'] = $this->tabIndex;
254 | }
255 |
256 | if (isset($this->widgetOptions['class'])) {
257 | $divOptions['class'] = "{$divOptions['class']} {$this->widgetOptions['class']}";
258 | }
259 | $divOptions['data-input-id'] = $this->getReCaptchaId();
260 |
261 | if ($this->field && $this->field->form) {
262 | if ($this->field->form->options['id']) {
263 | $divOptions['data-form-id'] = $this->field->form->options['id'];
264 | } else {
265 | $divOptions['data-form-id'] = $this->field->form->id;
266 | }
267 | } else {
268 | $divOptions['data-form-id'] = '';
269 | }
270 |
271 | $divOptions['id'] = $this->getReCaptchaId() . '-recaptcha' .
272 | ($divOptions['data-form-id'] ? ('-' . $divOptions['data-form-id']) : '');
273 |
274 | return $divOptions;
275 | }
276 |
277 | protected function inputNameToId($name)
278 | {
279 | return \str_replace(['[]', '][', '[', ']', ' ', '.'], ['', '-', '-', '', '-', '-'], \strtolower($name));
280 | }
281 |
282 | protected function configComponentProcess()
283 | {
284 | /** @var ReCaptchaConfig $reCaptchaConfig */
285 | $reCaptchaConfig = Yii::$app->get($this->configComponentName, false);
286 |
287 | if (!$this->siteKey) {
288 | if ($reCaptchaConfig && $reCaptchaConfig->siteKeyV2) {
289 | $this->siteKey = $reCaptchaConfig->siteKeyV2;
290 | } else {
291 | throw new InvalidConfigException('Required `siteKey` param isn\'t set.');
292 | }
293 | }
294 | if (!$this->jsApiUrl) {
295 | if ($reCaptchaConfig && $reCaptchaConfig->jsApiUrl) {
296 | $this->jsApiUrl = $reCaptchaConfig->jsApiUrl;
297 | } else {
298 | $this->jsApiUrl = ReCaptchaConfig::JS_API_URL_DEFAULT;
299 | }
300 | }
301 | }
302 | }
303 |
--------------------------------------------------------------------------------
/src/ReCaptcha3.php:
--------------------------------------------------------------------------------
1 | field($model, 'reCaptcha')->widget(
22 | * ReCaptcha3::className(),
23 | * [
24 | * 'siteKey' => 'your siteKey', // unnecessary is reCaptcha component was set up
25 | * 'threshold' => 0.5,
26 | * 'action' => 'homepage',
27 | * ]
28 | * ) ?>
29 | *```
30 | *
31 | * or
32 | *
33 | *```php
34 | * = ReCaptcha3::widget([
35 | * 'name' => 'reCaptcha',
36 | * 'siteKey' => 'your siteKey', // unnecessary is reCaptcha component was set up
37 | * 'threshold' => 0.5,
38 | * 'action' => 'homepage',
39 | * 'widgetOptions' => ['class' => 'col-sm-offset-3'],
40 | * ]) ?>
41 | *```
42 | *
43 | * @see https://developers.google.com/recaptcha/docs/v3
44 | * @author HimikLab
45 | * @package himiklab\yii2\recaptcha
46 | */
47 | class ReCaptcha3 extends InputWidget
48 | {
49 | /** @var string Your sitekey. */
50 | public $siteKey;
51 |
52 | /**
53 | * @var string Use [[ReCaptchaConfig::JS_API_URL_ALTERNATIVE]] when [[ReCaptchaConfig::JS_API_URL_DEFAULT]]
54 | * is not accessible.
55 | */
56 | public $jsApiUrl;
57 |
58 | /** @var string reCAPTCHA v3 action for this page. */
59 | public $action;
60 |
61 | /** @var string Your JS callback function that's executed when reCAPTCHA executed. */
62 | public $jsCallback;
63 |
64 | /** @var string */
65 | public $configComponentName = 'reCaptcha';
66 |
67 | public function __construct($siteKey = null, $jsApiUrl = null, $config = [])
68 | {
69 | if ($siteKey && !$this->siteKey) {
70 | $this->siteKey = $siteKey;
71 | }
72 | if ($jsApiUrl && !$this->jsApiUrl) {
73 | $this->jsApiUrl = $jsApiUrl;
74 | }
75 |
76 | parent::__construct($config);
77 | }
78 |
79 | public function init()
80 | {
81 | parent::init();
82 | $this->configComponentProcess();
83 | }
84 |
85 | public function run()
86 | {
87 | parent::run();
88 | $view = $this->view;
89 |
90 | $arguments = \http_build_query([
91 | 'render' => $this->siteKey,
92 | ]);
93 |
94 | $view->registerJsFile(
95 | $this->jsApiUrl . '?' . $arguments,
96 | ['position' => $view::POS_END]
97 | );
98 | $view->registerJs(
99 | <<siteKey}", {action: "{$this->action}"}).then(function(token) {
103 | jQuery("#" + "{$this->getReCaptchaId()}").val(token);
104 |
105 | const jsCallback = "{$this->jsCallback}";
106 | if (jsCallback) {
107 | eval("(" + jsCallback + ")(token)");
108 | }
109 | });
110 | });
111 | JS
112 | , $view::POS_READY);
113 |
114 | $this->customFieldPrepare();
115 | }
116 |
117 | protected function customFieldPrepare()
118 | {
119 | if ($this->hasModel()) {
120 | $inputName = Html::getInputName($this->model, $this->attribute);
121 | } else {
122 | $inputName = $this->name;
123 | }
124 |
125 | $options = $this->options;
126 | $options['id'] = $this->getReCaptchaId();
127 |
128 | echo Html::input('hidden', $inputName, null, $options);
129 | }
130 |
131 | protected function getReCaptchaId()
132 | {
133 | if (isset($this->options['id'])) {
134 | return $this->options['id'];
135 | }
136 |
137 | if ($this->hasModel()) {
138 | return Html::getInputId($this->model, $this->attribute);
139 | }
140 |
141 | return $this->id . '-' . $this->inputNameToId($this->name);
142 | }
143 |
144 | protected function inputNameToId($name)
145 | {
146 | return \str_replace(['[]', '][', '[', ']', ' ', '.'], ['', '-', '-', '', '-', '-'], \strtolower($name));
147 | }
148 |
149 | protected function configComponentProcess()
150 | {
151 | /** @var ReCaptchaConfig $reCaptchaConfig */
152 | $reCaptchaConfig = Yii::$app->get($this->configComponentName, false);
153 |
154 | if (!$this->siteKey) {
155 | if ($reCaptchaConfig && $reCaptchaConfig->siteKeyV3) {
156 | $this->siteKey = $reCaptchaConfig->siteKeyV3;
157 | } else {
158 | throw new InvalidConfigException('Required `siteKey` param isn\'t set.');
159 | }
160 | }
161 | if (!$this->jsApiUrl) {
162 | if ($reCaptchaConfig && $reCaptchaConfig->jsApiUrl) {
163 | $this->jsApiUrl = $reCaptchaConfig->jsApiUrl;
164 | } else {
165 | $this->jsApiUrl = ReCaptchaConfig::JS_API_URL_DEFAULT;
166 | }
167 | }
168 | if (!$this->action) {
169 | $this->action = \preg_replace('/[^a-zA-Z\d\/]/', '', \urldecode(Yii::$app->request->url));
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/ReCaptchaBaseValidator.php:
--------------------------------------------------------------------------------
1 | siteVerifyUrl) {
55 | $this->siteVerifyUrl = $siteVerifyUrl;
56 | }
57 | if ($checkHostName && $this->checkHostName !== null) {
58 | $this->checkHostName = $checkHostName;
59 | }
60 | if ($httpClientRequest && !$this->httpClientRequest) {
61 | $this->httpClientRequest = $httpClientRequest;
62 | }
63 |
64 | parent::__construct($config);
65 | }
66 |
67 | public function init()
68 | {
69 | parent::init();
70 |
71 | if ($this->message === null) {
72 | $this->message = Yii::t('yii', 'The verification code is incorrect.');
73 | }
74 | }
75 |
76 | /**
77 | * @param string $value
78 | * @return array
79 | * @throws Exception
80 | * @throws \yii\base\InvalidParamException
81 | */
82 | protected function getResponse($value)
83 | {
84 | $response = $this->httpClientRequest
85 | ->setMethod('GET')
86 | ->setUrl($this->siteVerifyUrl)
87 | ->setData(['secret' => $this->secret, 'response' => $value, 'remoteip' => Yii::$app->request->userIP])
88 | ->send();
89 | if (!$response->isOk) {
90 | throw new Exception('Unable connection to the captcha server. Status code ' . $response->statusCode);
91 | }
92 |
93 | return $response->data;
94 | }
95 |
96 | protected function configComponentProcess()
97 | {
98 | /** @var ReCaptchaConfig $reCaptchaConfig */
99 | $reCaptchaConfig = Yii::$app->get($this->configComponentName, false);
100 |
101 | if (!$this->siteVerifyUrl) {
102 | if ($reCaptchaConfig && $reCaptchaConfig->siteVerifyUrl) {
103 | $this->siteVerifyUrl = $reCaptchaConfig->siteVerifyUrl;
104 | } else {
105 | $this->siteVerifyUrl = ReCaptchaConfig::SITE_VERIFY_URL_DEFAULT;
106 | }
107 | }
108 |
109 | if ($this->checkHostName === null) {
110 | if ($reCaptchaConfig && $reCaptchaConfig->checkHostName !== null) {
111 | $this->checkHostName = $reCaptchaConfig->checkHostName;
112 | } else {
113 | $this->checkHostName = false;
114 | }
115 | }
116 |
117 | if (!$this->httpClientRequest) {
118 | if ($reCaptchaConfig && $reCaptchaConfig->httpClientRequest) {
119 | $this->httpClientRequest = $reCaptchaConfig->httpClientRequest;
120 | } else {
121 | $this->httpClientRequest = (new HttpClient())->createRequest();
122 | }
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/ReCaptchaConfig.php:
--------------------------------------------------------------------------------
1 | get('reCaptcha', false);
29 |
30 | if (!$this->secret) {
31 | if ($reCaptchaConfig && $reCaptchaConfig->secret) {
32 | $this->secret = $reCaptchaConfig->secret;
33 | } else {
34 | throw new InvalidConfigException('Required `secret` param isn\'t set.');
35 | }
36 | }
37 | if (!$this->siteVerifyUrl) {
38 | if ($reCaptchaConfig && $reCaptchaConfig->siteVerifyUrl) {
39 | $this->siteVerifyUrl = $reCaptchaConfig->siteVerifyUrl;
40 | } else {
41 | $this->siteVerifyUrl = self::SITE_VERIFY_URL_DEFAULT;
42 | }
43 | }
44 |
45 | parent::init();
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/ReCaptchaValidator2.php:
--------------------------------------------------------------------------------
1 | secret) {
33 | $this->secret = $secret;
34 | }
35 |
36 | parent::__construct($siteVerifyUrl, $checkHostName, $httpClientRequest, $config);
37 | }
38 |
39 | public function init()
40 | {
41 | parent::init();
42 | $this->configComponentProcess();
43 | }
44 |
45 | /**
46 | * @param \yii\base\Model $model
47 | * @param string $attribute
48 | * @param \yii\web\View $view
49 | * @return string
50 | */
51 | public function clientValidateAttribute($model, $attribute, $view)
52 | {
53 | $message = \addslashes($this->uncheckedMessage ?: Yii::t(
54 | 'yii',
55 | '{attribute} cannot be blank.',
56 | ['attribute' => $model->getAttributeLabel($attribute)]
57 | ));
58 |
59 | return <<isValid === null) {
75 | if (!$value) {
76 | $this->isValid = false;
77 | } else {
78 | $response = $this->getResponse($value);
79 | if (!isset($response['success'], $response['hostname']) ||
80 | ($this->checkHostName && $response['hostname'] !== $this->getHostName())
81 | ) {
82 | throw new Exception('Invalid recaptcha verify response.');
83 | }
84 |
85 | $this->isValid = $response['success'] === true;
86 | }
87 | }
88 |
89 | return $this->isValid ? null : [$this->message, []];
90 | }
91 |
92 | protected function configComponentProcess()
93 | {
94 | parent::configComponentProcess();
95 |
96 | /** @var ReCaptchaConfig $reCaptchaConfig */
97 | $reCaptchaConfig = Yii::$app->get($this->configComponentName, false);
98 |
99 | if (!$this->secret) {
100 | if ($reCaptchaConfig && $reCaptchaConfig->secretV2) {
101 | $this->secret = $reCaptchaConfig->secretV2;
102 | } else {
103 | throw new InvalidConfigException('Required `secret` param isn\'t set.');
104 | }
105 | }
106 | }
107 |
108 | protected function getHostName()
109 | {
110 | return Yii::$app->request->hostName;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/ReCaptchaValidator3.php:
--------------------------------------------------------------------------------
1 | secret) {
37 | $this->secret = $secret;
38 | }
39 |
40 | parent::__construct($siteVerifyUrl, $checkHostName, $httpClientRequest, $config);
41 | }
42 |
43 | public function init()
44 | {
45 | parent::init();
46 | $this->configComponentProcess();
47 |
48 | if ($this->action === null) {
49 | $this->action = \preg_replace('/[^a-zA-Z\d\/]/', '', \urldecode(Yii::$app->request->url));
50 | }
51 | }
52 |
53 | /**
54 | * @param string|array $value
55 | * @return array|null
56 | * @throws Exception
57 | * @throws \yii\base\InvalidParamException
58 | */
59 | protected function validateValue($value)
60 | {
61 | if ($this->isValid === null) {
62 | if (!$value) {
63 | $this->isValid = false;
64 | } else {
65 | $response = $this->getResponse($value);
66 | if (isset($response['error-codes'])) {
67 | $this->isValid = false;
68 | } else {
69 | if (!isset($response['success'], $response['action'], $response['hostname'], $response['score']) ||
70 | $response['success'] !== true ||
71 | ($this->action !== false && $response['action'] !== $this->action) ||
72 | ($this->checkHostName && $response['hostname'] !== Yii::$app->request->hostName)
73 | ) {
74 | throw new Exception('Invalid recaptcha verify response.');
75 | }
76 |
77 | if (\is_callable($this->threshold)) {
78 | $this->isValid = (bool)\call_user_func($this->threshold, $response['score']);
79 | } else {
80 | $this->isValid = $response['score'] >= $this->threshold;
81 | }
82 | }
83 | }
84 | }
85 |
86 | return $this->isValid ? null : [$this->message, []];
87 | }
88 |
89 | protected function configComponentProcess()
90 | {
91 | parent::configComponentProcess();
92 |
93 | /** @var ReCaptchaConfig $reCaptchaConfig */
94 | $reCaptchaConfig = Yii::$app->get($this->configComponentName, false);
95 |
96 | if (!$this->secret) {
97 | if ($reCaptchaConfig && $reCaptchaConfig->secretV3) {
98 | $this->secret = $reCaptchaConfig->secretV3;
99 | } else {
100 | throw new InvalidConfigException('Required `secret` param isn\'t set.');
101 | }
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/tests/ReCaptchaValidatorTest.php:
--------------------------------------------------------------------------------
1 | validatorClass
20 | ->expects($this->once())
21 | ->method('getResponse')
22 | ->willReturn(['success' => true, 'hostname' => 'localhost']);
23 |
24 | $this->assertNull($this->validatorMethod->invoke($this->validatorClass, 'test'));
25 | $this->assertNull($this->validatorMethod->invoke($this->validatorClass, 'test'));
26 | }
27 |
28 | public function testValidateValueFailure()
29 | {
30 | $this->validatorClass
31 | ->expects($this->once())
32 | ->method('getResponse')
33 | ->willReturn(['success' => false, 'hostname' => 'localhost']);
34 |
35 | $this->assertNotNull($this->validatorMethod->invoke($this->validatorClass, 'test'));
36 | $this->assertNotNull($this->validatorMethod->invoke($this->validatorClass, 'test'));
37 | }
38 |
39 | public function testValidateValueException()
40 | {
41 | $this->validatorClass
42 | ->expects($this->once())
43 | ->method('getResponse')
44 | ->willReturn([]);
45 |
46 | $this->setExpectedException('yii\base\Exception');
47 | $this->validatorMethod->invoke($this->validatorClass, 'test');
48 | }
49 |
50 | public function testHostNameValidateFailure()
51 | {
52 | $this->validatorClass
53 | ->expects($this->once())
54 | ->method('getResponse')
55 | ->willReturn(['success' => false, 'hostname' => 'localhost']);
56 | $this->validatorClass
57 | ->expects($this->once())
58 | ->method('getHostName')
59 | ->willReturn('test');
60 | $this->validatorClass->checkHostName = true;
61 |
62 | $this->setExpectedException('yii\base\Exception');
63 | $this->validatorMethod->invoke($this->validatorClass, 'test');
64 | }
65 |
66 | public function testHostNameValidateSuccess()
67 | {
68 | $this->validatorClass
69 | ->expects($this->once())
70 | ->method('getResponse')
71 | ->willReturn(['success' => false, 'hostname' => 'localhost']);
72 | $this->validatorClass
73 | ->expects($this->once())
74 | ->method('getHostName')
75 | ->willReturn('localhost');
76 | $this->validatorClass->checkHostName = true;
77 |
78 | $this->validatorMethod->invoke($this->validatorClass, 'test');
79 | }
80 |
81 | public function setUp()
82 | {
83 | parent::setUp();
84 | $this->validatorClass = $this->getMockBuilder(ReCaptchaValidator::className())
85 | ->disableOriginalConstructor()
86 | ->setMethods(['getResponse', 'getHostName'])
87 | ->getMock();
88 |
89 | $this->validatorMethod = (new ReflectionClass(ReCaptchaValidator::className()))->getMethod('validateValue');
90 | $this->validatorMethod->setAccessible(true);
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |