From 7715aea1351b3aa93a0a2cff68b5cbaeeee65922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20M=C3=BCller?= <p.mueller@pcsg.de> Date: Wed, 29 Jul 2020 12:55:55 +0200 Subject: [PATCH] feat: Google reCAPTCHA v3 quiqqer/captcha#10 --- bin/controls/modules/Google.js | 73 +++++++++++---- locale.xml | 20 +++-- src/QUI/Captcha/Modules/Google.php | 6 +- src/QUI/Captcha/Modules/GoogleV3.php | 93 ++++++++++++++++++++ src/QUI/Captcha/Modules/GoogleV3/Control.php | 24 +++++ 5 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 src/QUI/Captcha/Modules/GoogleV3.php create mode 100644 src/QUI/Captcha/Modules/GoogleV3/Control.php diff --git a/bin/controls/modules/Google.js b/bin/controls/modules/Google.js index 6aec190..7b042d0 100644 --- a/bin/controls/modules/Google.js +++ b/bin/controls/modules/Google.js @@ -20,18 +20,24 @@ define('package/quiqqer/captcha/bin/controls/modules/Google', [ Binds: [ '$onImport', '$onGoogleCaptchaLoaded', - '$onGoogleCatpchaSuccess' + '$onGoogleCatpchaSuccess', + '$renderV2', + '$renderV3' ], options: { - sitekey : false, // Google reCAPTCHA v2 Site Key - invisible: false // use invisible captcha + sitekey : false, // Google reCAPTCHA v2/v3 Site Key + invisible: false, // use invisible captcha + v3 : false, // use reCAPTCHA v3 }, initialize: function (options) { this.parent(options); - this.Loader = new QUILoader(); + this.Loader = new QUILoader(); + this.$DisplayElm = null; + + this.$reloadChallengeOnPrematureClose = true; this.addEvents({ onImport: this.$onImport @@ -66,11 +72,19 @@ define('package/quiqqer/captcha/bin/controls/modules/Google', [ window.$onGoogleCaptchaLoaded = this.$onGoogleCaptchaLoaded; window.loadingGoogleReCaptcha = true; - new Element('script', { - src : 'https://www.google.com/recaptcha/api.js?onload=$onGoogleCaptchaLoaded&render=explicit', - async: true, - defer: true - }).inject(document.head); + if (this.getAttribute('v3')) { + new Element('script', { + src : 'https://www.google.com/recaptcha/api.js?onload=$onGoogleCaptchaLoaded&render=' + this.getAttribute('sitekey'), + async: true, + defer: true + }).inject(document.head); + } else { + new Element('script', { + src : 'https://www.google.com/recaptcha/api.js?onload=$onGoogleCaptchaLoaded&render=explicit', + async: true, + defer: true + }).inject(document.head); + } }, /** @@ -79,17 +93,27 @@ define('package/quiqqer/captcha/bin/controls/modules/Google', [ $onGoogleCaptchaLoaded: function () { window.loadingGoogleReCaptcha = false; - var self = this; - var reloadChallengeOnPrematureClose = true; - this.Loader.hide(); - var DisplayElm = this.$Elm.getElement('.quiqqer-captcha-google-display'); + this.$DisplayElm = this.$Elm.getElement('.quiqqer-captcha-google-display'); + + if (this.getAttribute('v3')) { + this.$renderV3(); + } else { + this.$renderV2(); + } + }, + + /** + * Render Google reCAPTCHA v2 + */ + $renderV2: function () { + var self = this; var Options = { sitekey : this.getAttribute('sitekey'), callback : function (response) { - reloadChallengeOnPrematureClose = false; + self.$reloadChallengeOnPrematureClose = false; self.$onCaptchaSuccess(response); }, 'expired-callback': function () { @@ -97,7 +121,7 @@ define('package/quiqqer/captcha/bin/controls/modules/Google', [ //self.$onCaptchaExpired(); }, 'error-callback' : function () { - reloadChallengeOnPrematureClose = false; + self.$reloadChallengeOnPrematureClose = false; } }; @@ -105,9 +129,9 @@ define('package/quiqqer/captcha/bin/controls/modules/Google', [ Options.size = 'invisible'; } - grecaptcha.render(DisplayElm, Options); + grecaptcha.render(this.$DisplayElm, Options); - if (this.getAttribute('invisible')) { + if (self.getAttribute('invisible')) { grecaptcha.execute(); // Wait for challenge window @@ -120,7 +144,7 @@ define('package/quiqqer/captcha/bin/controls/modules/Google', [ clearInterval(wait); var Observer = new MutationObserver(function (mutations) { - if (!reloadChallengeOnPrematureClose) { + if (!self.$reloadChallengeOnPrematureClose) { return; } @@ -141,6 +165,19 @@ define('package/quiqqer/captcha/bin/controls/modules/Google', [ } }, 1000); } + }, + + /** + * Render Google reCAPTCHA v3 + */ + $renderV3: function () { + var self = this; + + grecaptcha.ready(function () { + grecaptcha.execute(self.getAttribute('sitekey'), {action: 'submit'}).then(function (token) { + self.$onCaptchaSuccess(token); + }); + }); } }); }); \ No newline at end of file diff --git a/locale.xml b/locale.xml index 441f273..26616af 100644 --- a/locale.xml +++ b/locale.xml @@ -49,8 +49,8 @@ <en><![CDATA[Site key]]></en> </locale> <locale name="settings.google.siteKey.description" html="true"> - <de><![CDATA[Googe reCAPTCHA v2 Websiteschlüssel. Siehe <a href="https://www.google.com/recaptcha/admin" target="_blank">https://www.google.com/recaptcha/admin</a>.]]></de> - <en><![CDATA[Google reCAPTCHA v2 Site key. See <a href="https://www.google.com/recaptcha/admin" target="_blank">https://www.google.com/recaptcha/admin</a>.]]></en> + <de><![CDATA[Googe reCAPTCHA v2 or v3 Websiteschlüssel. Siehe <a href="https://www.google.com/recaptcha/admin" target="_blank">https://www.google.com/recaptcha/admin</a>.]]></de> + <en><![CDATA[Google reCAPTCHA v2 or v3 Site key. See <a href="https://www.google.com/recaptcha/admin" target="_blank">https://www.google.com/recaptcha/admin</a>.]]></en> </locale> <locale name="settings.google.secretKey.title"> <de><![CDATA[Geheimer Schlüssel]]></de> @@ -59,21 +59,29 @@ <!-- Module titles and descriptions --> <locale name="captcha.title.Google"> - <de><![CDATA[Google (reCAPTCHA)]]></de> - <en><![CDATA[Google (reCAPTCHA)]]></en> + <de><![CDATA[Google (reCAPTCHA v2)]]></de> + <en><![CDATA[Google (reCAPTCHA v2)]]></en> </locale> <locale name="captcha.descrption.Google"> <de><![CDATA[Benötigt JavaScript.]]></de> <en><![CDATA[Requires JavaScript.]]></en> </locale> <locale name="captcha.title.GoogleInvisible"> - <de><![CDATA[Google (reCAPTCHA - unsichtbar)]]></de> - <en><![CDATA[Google (reCAPTCHA - invisible)]]></en> + <de><![CDATA[Google (reCAPTCHA v2 - unsichtbar)]]></de> + <en><![CDATA[Google (reCAPTCHA v2 - invisible)]]></en> </locale> <locale name="captcha.descrption.GoogleInvisible"> <de><![CDATA[Benötigt JavaScript.]]></de> <en><![CDATA[Requires JavaScript.]]></en> </locale> + <locale name="captcha.title.GoogleV3"> + <de><![CDATA[Google (reCAPTCHA v3)]]></de> + <en><![CDATA[Google (reCAPTCHA v3)]]></en> + </locale> + <locale name="captcha.descrption.GoogleV3"> + <de><![CDATA[Benötigt JavaScript.]]></de> + <en><![CDATA[Requires JavaScript.]]></en> + </locale> </groups> </locales> diff --git a/src/QUI/Captcha/Modules/Google.php b/src/QUI/Captcha/Modules/Google.php index 57d37ce..d7a182d 100644 --- a/src/QUI/Captcha/Modules/Google.php +++ b/src/QUI/Captcha/Modules/Google.php @@ -27,10 +27,10 @@ class Google extends QUI\Captcha\AbstractCaptcha public static function isValid($data) { $url = 'https://www.google.com/recaptcha/api/siteverify?'; - $params = array( + $params = [ 'secret' => self::getSecretKey(), 'response' => $data - ); + ]; $url .= http_build_query($params); @@ -56,7 +56,7 @@ class Google extends QUI\Captcha\AbstractCaptcha if (!empty($validationResponse['error-codes'])) { QUI\System\Log::addError( - 'Google reCAPTCHA response could not be validated: ' . implode(', ', $validationResponse['error-codes']) + 'Google reCAPTCHA response could not be validated: '.implode(', ', $validationResponse['error-codes']) ); return false; diff --git a/src/QUI/Captcha/Modules/GoogleV3.php b/src/QUI/Captcha/Modules/GoogleV3.php new file mode 100644 index 0000000..316ede8 --- /dev/null +++ b/src/QUI/Captcha/Modules/GoogleV3.php @@ -0,0 +1,93 @@ +<?php + +namespace QUI\Captcha\Modules; + +use QUI; +use QUI\Captcha\Modules\GoogleV3\Control; + +/** + * Class GoogleV3 + * + * Captcha provider vor Google reCAPTCHA v3 + */ +class GoogleV3 extends Google +{ + /** + * Get control to show captcha + * + * @return Control + */ + public static function getControl() + { + return new Control(); + } + + /** + * Check if this CAPTCHA has a visible representation or not + * + * @return bool + */ + public static function isInvisible() + { + return true; + } + + /** + * Validate captcha data + * + * @param string $data + * @return bool + * @throws QUI\Exception + */ + public static function isValid($data) + { + $url = 'https://www.google.com/recaptcha/api/siteverify?'; + $params = [ + 'secret' => self::getSecretKey(), + 'response' => $data + ]; + + $url .= http_build_query($params); + + $validationResponse = file_get_contents($url); + + if (empty($validationResponse)) { + QUI\System\Log::addError( + 'Google reCAPTCHA response could not be validated: Empty response.' + ); + + return false; + } + + $validationResponse = json_decode($validationResponse, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + QUI\System\Log::addError( + 'Google reCAPTCHA response could not be validated: Response was no valid JSON.' + ); + + return false; + } + + if (!empty($validationResponse['error-codes'])) { + QUI\System\Log::addError( + 'Google reCAPTCHA response could not be validated: '.implode(', ', $validationResponse['error-codes']) + ); + + return false; + } + + if (empty($validationResponse['success'])) { + return false; + } + + // Evaluate reCAPTCHA v3 score + $score = (float)$validationResponse['score']; + + if ($score <= 0) { + return false; + } + + return true; + } +} diff --git a/src/QUI/Captcha/Modules/GoogleV3/Control.php b/src/QUI/Captcha/Modules/GoogleV3/Control.php new file mode 100644 index 0000000..d1f3fbe --- /dev/null +++ b/src/QUI/Captcha/Modules/GoogleV3/Control.php @@ -0,0 +1,24 @@ +<?php + +namespace QUI\Captcha\Modules\GoogleV3; + +use QUI; +use QUI\Captcha\Modules\Google\Control as GoogleControl; + +/** + * Class Controls + * + * Google reCAPTCHA v3 Control + */ +class Control extends GoogleControl +{ + /** + * ControlWrapper constructor. + * @param array $attributes + */ + public function __construct(array $attributes = []) + { + parent::__construct($attributes); + $this->setJavaScriptControlOption('v3', true); + } +} -- GitLab