diff --git a/ReCaptchaCustomer/Observer/EditCustomerObserver.php b/ReCaptchaCustomer/Observer/EditCustomerObserver.php
new file mode 100644
index 00000000..6ab19f0e
--- /dev/null
+++ b/ReCaptchaCustomer/Observer/EditCustomerObserver.php
@@ -0,0 +1,74 @@
+url = $url;
+ $this->isCaptchaEnabled = $isCaptchaEnabled;
+ $this->requestHandler = $requestHandler;
+ }
+
+ /**
+ * @inheritdoc
+ *
+ * @param Observer $observer
+ * @return void
+ * @throws \Magento\Framework\Exception\LocalizedException
+ */
+ public function execute(Observer $observer): void
+ {
+ $key = 'customer_edit';
+ if ($this->isCaptchaEnabled->isCaptchaEnabledFor($key)) {
+ /** @var Action $controller */
+ $controller = $observer->getControllerAction();
+ $request = $controller->getRequest();
+ $response = $controller->getResponse();
+ $redirectOnFailureUrl = $this->url->getUrl('*/*/edit', ['_secure' => true]);
+
+ $this->requestHandler->execute($key, $request, $response, $redirectOnFailureUrl);
+ }
+ }
+}
diff --git a/ReCaptchaCustomer/Test/Integration/EditFromTest.php b/ReCaptchaCustomer/Test/Integration/EditFromTest.php
new file mode 100644
index 00000000..b62e0983
--- /dev/null
+++ b/ReCaptchaCustomer/Test/Integration/EditFromTest.php
@@ -0,0 +1,316 @@
+mutableScopeConfig = $this->_objectManager->get(MutableScopeConfig::class);
+ $this->formKey = $this->_objectManager->get(FormKey::class);
+ $this->customerRepository = $this->_objectManager->get(CustomerRepositoryInterface::class);
+ $this->session = $this->_objectManager->get(Session::class);
+ $this->url = $this->_objectManager->get(UrlInterface::class);
+
+ $this->captchaValidationResultMock = $this->createMock(ValidationResult::class);
+ $captchaValidationResultMock = $this->createMock(Validator::class);
+ $captchaValidationResultMock->expects($this->any())
+ ->method('isValid')
+ ->willReturn($this->captchaValidationResultMock);
+ $this->_objectManager->addSharedInstance($captchaValidationResultMock, Validator::class);
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/public_key test_public_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/private_key test_private_key
+ */
+ public function testGetRequestIfReCaptchaIsDisabled(): void
+ {
+ $this->setConfig(false, 'test_public_key', 'test_private_key');
+
+ $this->checkSuccessfulGetResponse();
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_for/customer_edit invisible
+ *
+ * It's needed for proper work of "ifconfig" in layout during tests running
+ * @magentoConfigFixture default_store recaptcha_frontend/type_for/customer_edit invisible
+ */
+ public function testGetRequestIfReCaptchaKeysAreNotConfigured(): void
+ {
+ $this->setConfig(true, null, null);
+
+ $this->checkSuccessfulGetResponse();
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/public_key test_public_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/private_key test_private_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_for/customer_edit invisible
+ *
+ * It's needed for proper work of "ifconfig" in layout during tests running
+ * @magentoConfigFixture default_store recaptcha_frontend/type_for/customer_edit invisible
+ */
+ public function testGetRequestIfReCaptchaIsEnabled(): void
+ {
+ $this->setConfig(true, 'test_public_key', 'test_private_key');
+
+ $this->checkSuccessfulGetResponse(true);
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/public_key test_public_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/private_key test_private_key
+ */
+ public function testPostRequestIfReCaptchaIsDisabled(): void
+ {
+ $this->setConfig(false, 'test_public_key', 'test_private_key');
+
+ $this->checkSuccessfulPostResponse();
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_for/customer_edit invisible
+ */
+ public function testPostRequestIfReCaptchaKeysAreNotConfigured(): void
+ {
+ $this->setConfig(true, null, null);
+
+ $this->checkSuccessfulPostResponse();
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/public_key test_public_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/private_key test_private_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_for/customer_edit invisible
+ */
+ public function testPostRequestWithSuccessfulReCaptchaValidation(): void
+ {
+ $this->setConfig(true, 'test_public_key', 'test_private_key');
+ $this->captchaValidationResultMock->expects($this->once())->method('isValid')->willReturn(true);
+
+ $this->checkSuccessfulPostResponse(
+ [CaptchaResponseResolverInterface::PARAM_RECAPTCHA => 'test']
+ );
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/public_key test_public_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/private_key test_private_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_for/customer_edit invisible
+ */
+ public function testPostRequestIfReCaptchaParameterIsMissed(): void
+ {
+ $this->setConfig(true, 'test_public_key', 'test_private_key');
+
+ $this->checkFailedPostResponse();
+ }
+
+ /**
+ * @magentoConfigFixture default_store customer/captcha/enable 0
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/public_key test_public_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_invisible/private_key test_private_key
+ * @magentoConfigFixture base_website recaptcha_frontend/type_for/customer_edit invisible
+ */
+ public function testPostRequestWithFailedReCaptchaValidation(): void
+ {
+ $this->setConfig(true, 'test_public_key', 'test_private_key');
+ $this->captchaValidationResultMock->expects($this->once())->method('isValid')->willReturn(false);
+
+ $this->checkFailedPostResponse(
+ [CaptchaResponseResolverInterface::PARAM_RECAPTCHA => 'test']
+ );
+ }
+
+ /**
+ * @param bool $shouldContainReCaptcha
+ * @return void
+ */
+ private function checkSuccessfulGetResponse($shouldContainReCaptcha = false): void
+ {
+ $this->session->loginById(1);
+ $this->dispatch('customer/account/edit');
+ $content = $this->getResponse()->getBody();
+
+ self::assertNotEmpty($content);
+
+ $shouldContainReCaptcha
+ ? $this->assertStringContainsString('field-recaptcha', $content)
+ : $this->assertStringNotContainsString('field-recaptcha', $content);
+
+ self::assertEmpty($this->getSessionMessages(MessageInterface::TYPE_ERROR));
+ }
+
+ /**
+ * @param array $postValues
+ * @return void
+ */
+ private function checkSuccessfulPostResponse(array $postValues = []): void
+ {
+ $this->session->loginById(1);
+ $this->makePostRequest($postValues);
+
+ $this->assertRedirect(self::equalTo($this->url->getRouteUrl('customer/account')));
+ self::assertEmpty($this->getSessionMessages(MessageInterface::TYPE_ERROR));
+
+ $customer = $this->customerRepository->getById(1);
+ $this->assertEquals('Test First Name', $customer->getFirstname());
+ $this->assertEquals('Test Last Name', $customer->getLastname());
+ $this->assertEquals('customer@example.com', $customer->getEmail());
+ }
+
+ /**
+ * @param array $postValues
+ * @return void
+ */
+ private function checkFailedPostResponse(array $postValues = []): void
+ {
+ $this->session->loginById(1);
+ $this->makePostRequest($postValues);
+
+ $this->assertRedirect(self::stringStartsWith($this->url->getRouteUrl('customer/account/edit')));
+ $this->assertSessionMessages(
+ self::equalTo(['reCAPTCHA verification failed']),
+ MessageInterface::TYPE_ERROR
+ );
+
+ $customer = $this->customerRepository->getById(1);
+ $this->assertEquals('John', $customer->getFirstname());
+ $this->assertEquals('Smith', $customer->getLastname());
+ $this->assertEquals('customer@example.com', $customer->getEmail());
+ }
+
+ /**
+ * @param array $postValues
+ * @return void
+ */
+ private function makePostRequest(array $postValues = []): void
+ {
+ $this->getRequest()
+ ->setMethod(Http::METHOD_POST)
+ ->setPostValue(
+ array_replace_recursive(
+ [
+ 'form_key' => $this->formKey->getFormKey(),
+ 'firstname' => 'Test First Name',
+ 'lastname' => 'Test Last Name',
+ ],
+ $postValues
+ )
+ );
+
+ $this->dispatch('customer/account/editpost');
+ }
+
+ /**
+ * @param bool $isEnabled
+ * @param string|null $public
+ * @param string|null $private
+ * @return void
+ */
+ private function setConfig(bool $isEnabled, ?string $public, ?string $private): void
+ {
+ $this->mutableScopeConfig->setValue(
+ 'recaptcha_frontend/type_for/customer_edit',
+ $isEnabled ? 'invisible' : null,
+ ScopeInterface::SCOPE_WEBSITE
+ );
+ $this->mutableScopeConfig->setValue(
+ 'recaptcha_frontend/type_invisible/public_key',
+ $public,
+ ScopeInterface::SCOPE_WEBSITE
+ );
+ $this->mutableScopeConfig->setValue(
+ 'recaptcha_frontend/type_invisible/private_key',
+ $private,
+ ScopeInterface::SCOPE_WEBSITE
+ );
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ $this->mutableScopeConfig->setValue(
+ 'recaptcha_frontend/type_for/customer_edit',
+ null,
+ ScopeInterface::SCOPE_WEBSITE
+ );
+ $this->mutableScopeConfig->setValue(
+ 'recaptcha_frontend/type_invisible/public_key',
+ null,
+ ScopeInterface::SCOPE_WEBSITE
+ );
+ $this->mutableScopeConfig->setValue(
+ 'recaptcha_frontend/type_invisible/private_key',
+ null,
+ ScopeInterface::SCOPE_WEBSITE
+ );
+ }
+}
diff --git a/ReCaptchaCustomer/etc/adminhtml/system.xml b/ReCaptchaCustomer/etc/adminhtml/system.xml
index a9ed7e6d..27b3bd28 100644
--- a/ReCaptchaCustomer/etc/adminhtml/system.xml
+++ b/ReCaptchaCustomer/etc/adminhtml/system.xml
@@ -25,6 +25,11 @@
Magento\ReCaptchaAdminUi\Model\OptionSource\Type
+
+
+ Magento\ReCaptchaAdminUi\Model\OptionSource\Type
+
diff --git a/ReCaptchaCustomer/etc/config.xml b/ReCaptchaCustomer/etc/config.xml
index e3ead5e4..11ee5351 100644
--- a/ReCaptchaCustomer/etc/config.xml
+++ b/ReCaptchaCustomer/etc/config.xml
@@ -13,6 +13,7 @@
+
diff --git a/ReCaptchaCustomer/etc/frontend/events.xml b/ReCaptchaCustomer/etc/frontend/events.xml
index aec4e0a7..8166d9b6 100644
--- a/ReCaptchaCustomer/etc/frontend/events.xml
+++ b/ReCaptchaCustomer/etc/frontend/events.xml
@@ -16,6 +16,9 @@
+
+
+
diff --git a/ReCaptchaCustomer/view/frontend/layout/customer_account_edit.xml b/ReCaptchaCustomer/view/frontend/layout/customer_account_edit.xml
new file mode 100644
index 00000000..11a1de9a
--- /dev/null
+++ b/ReCaptchaCustomer/view/frontend/layout/customer_account_edit.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ customer_edit
+
+ -
+
-
+
- Magento_ReCaptchaFrontendUi/js/reCaptcha
+
+
+
+
+
+
+
+
diff --git a/ReCaptchaCustomer/view/frontend/web/css/source/_module.less b/ReCaptchaCustomer/view/frontend/web/css/source/_module.less
index 8ef62177..f2958ae5 100644
--- a/ReCaptchaCustomer/view/frontend/web/css/source/_module.less
+++ b/ReCaptchaCustomer/view/frontend/web/css/source/_module.less
@@ -2,7 +2,9 @@
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-.login-container, .form-login {
+.login-container,
+.form-login,
+.form-edit-account {
.g-recaptcha {
margin-bottom: 10px !important;
}
diff --git a/ReCaptchaFrontendUi/view/frontend/web/js/nonInlineReCaptchaRenderer.js b/ReCaptchaFrontendUi/view/frontend/web/js/nonInlineReCaptchaRenderer.js
new file mode 100644
index 00000000..b9f16c2e
--- /dev/null
+++ b/ReCaptchaFrontendUi/view/frontend/web/js/nonInlineReCaptchaRenderer.js
@@ -0,0 +1,57 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+/* global grecaptcha */
+define([
+ 'jquery'
+], function ($) {
+ 'use strict';
+
+ var reCaptchaEntities = [],
+ initialized = false,
+ rendererRecaptchaId = 'recaptcha-invisible',
+ rendererReCaptcha = null;
+
+ return {
+ /**
+ * Add reCaptcha entity to checklist.
+ *
+ * @param {jQuery} reCaptchaEntity
+ * @param {Object} parameters
+ */
+ add: function (reCaptchaEntity, parameters) {
+ if (!initialized) {
+ this.init();
+ grecaptcha.render(rendererRecaptchaId, parameters);
+ setInterval(this.resolveVisibility, 100);
+ initialized = true;
+ }
+
+ reCaptchaEntities.push(reCaptchaEntity);
+ },
+
+ /**
+ * Show additional reCaptcha instance if any other should be visible, otherwise hide it.
+ */
+ resolveVisibility: function () {
+ reCaptchaEntities.some(function (entity) {
+ return entity.is(':visible') &&
+ // 900 is some magic z-index value of modal popups.
+ (entity.closest('[data-role=\'modal\']').length === 0 || entity.zIndex() > 900);
+ }) ? rendererReCaptcha.show() : rendererReCaptcha.hide();
+ },
+
+ /**
+ * Initialize additional reCaptcha instance.
+ */
+ init: function () {
+ rendererReCaptcha = $('', {
+ 'id': rendererRecaptchaId
+ });
+ rendererReCaptcha.hide();
+ $('body').append(rendererReCaptcha);
+ }
+ };
+});
diff --git a/ReCaptchaFrontendUi/view/frontend/web/js/reCaptcha.js b/ReCaptchaFrontendUi/view/frontend/web/js/reCaptcha.js
index 63688881..ddd627f2 100644
--- a/ReCaptchaFrontendUi/view/frontend/web/js/reCaptcha.js
+++ b/ReCaptchaFrontendUi/view/frontend/web/js/reCaptcha.js
@@ -3,17 +3,17 @@
* See COPYING.txt for license details.
*/
-/* eslint-disable no-undef */
-// jscs:disable jsDoc
+/* global grecaptcha */
define(
[
'uiComponent',
'jquery',
'ko',
+ 'underscore',
'Magento_ReCaptchaFrontendUi/js/registry',
- 'Magento_ReCaptchaFrontendUi/js/reCaptchaScriptLoader'
- ],
- function (Component, $, ko, registry, reCaptchaLoader, undefined) {
+ 'Magento_ReCaptchaFrontendUi/js/reCaptchaScriptLoader',
+ 'Magento_ReCaptchaFrontendUi/js/nonInlineReCaptchaRenderer'
+ ], function (Component, $, ko, _, registry, reCaptchaLoader, nonInlineReCaptchaRenderer) {
'use strict';
return Component.extend({
@@ -22,8 +22,10 @@ define(
template: 'Magento_ReCaptchaFrontendUi/reCaptcha',
reCaptchaId: 'recaptcha'
},
- _isApiRegistered: undefined,
+ /**
+ * @inheritdoc
+ */
initialize: function () {
this._super();
this._loadApi();
@@ -75,8 +77,7 @@ define(
* Initialize reCAPTCHA after first rendering
*/
initCaptcha: function () {
- var me = this,
- $parentForm,
+ var $parentForm,
$wrapper,
$reCaptcha,
widgetId,
@@ -102,21 +103,24 @@ define(
$reCaptcha.attr('id', this.getReCaptchaId());
$parentForm = $wrapper.parents('form');
- me = this;
parameters = _.extend(
{
'callback': function (token) { // jscs:ignore jsDoc
- me.reCaptchaCallback(token);
- me.validateReCaptcha(true);
- },
+ this.reCaptchaCallback(token);
+ this.validateReCaptcha(true);
+ }.bind(this),
'expired-callback': function () {
- me.validateReCaptcha(false);
- }
+ this.validateReCaptcha(false);
+ }.bind(this)
},
this.settings.rendering
);
+ if (parameters.size === 'invisible' && parameters.badge !== 'inline') {
+ nonInlineReCaptchaRenderer.add($reCaptcha, parameters);
+ }
+
// eslint-disable-next-line no-undef
widgetId = grecaptcha.render(this.getReCaptchaId(), parameters);
this.initParentForm($parentForm, widgetId);
@@ -134,18 +138,17 @@ define(
* @param {String} widgetId
*/
initParentForm: function (parentForm, widgetId) {
- var me = this,
- listeners;
+ var listeners;
if (this.getIsInvisibleRecaptcha() && parentForm.length > 0) {
parentForm.submit(function (event) {
- if (!me.tokenField.value) {
+ if (!this.tokenField.value) {
// eslint-disable-next-line no-undef
grecaptcha.execute(widgetId);
event.preventDefault(event);
event.stopImmediatePropagation();
}
- });
+ }.bind(this));
// Move our (last) handler topmost. We need this to avoid submit bindings with ko.
listeners = $._data(parentForm[0], 'events').submit;
@@ -160,6 +163,11 @@ define(
}
},
+ /**
+ * Validates reCAPTCHA
+ * @param {*} state
+ * @returns {jQuery}
+ */
validateReCaptcha: function (state) {
if (!this.getIsInvisibleRecaptcha()) {
return $(document).find('input[type=checkbox].required-captcha').prop('checked', state);
@@ -170,14 +178,12 @@ define(
* Render reCAPTCHA
*/
renderReCaptcha: function () {
- var me = this;
-
if (window.grecaptcha && window.grecaptcha.render) { // Check if reCAPTCHA is already loaded
- me.initCaptcha();
+ this.initCaptcha();
} else { // Wait for reCAPTCHA to be loaded
$(window).on('recaptchaapiready', function () {
- me.initCaptcha();
- });
+ this.initCaptcha();
+ }.bind(this));
}
},
@@ -189,5 +195,4 @@ define(
return this.reCaptchaId;
}
});
- }
-);
+ });
diff --git a/ReCaptchaNewsletter/etc/adminhtml/system.xml b/ReCaptchaNewsletter/etc/adminhtml/system.xml
index f70a2969..ca0109b6 100644
--- a/ReCaptchaNewsletter/etc/adminhtml/system.xml
+++ b/ReCaptchaNewsletter/etc/adminhtml/system.xml
@@ -12,7 +12,7 @@
-
+
If enabled, a badge will be displayed in every page.
Magento\ReCaptchaAdminUi\Model\OptionSource\Type
diff --git a/ReCaptchaNewsletter/view/frontend/layout/default.xml b/ReCaptchaNewsletter/view/frontend/layout/default.xml
index 82c684a2..992d56d7 100644
--- a/ReCaptchaNewsletter/view/frontend/layout/default.xml
+++ b/ReCaptchaNewsletter/view/frontend/layout/default.xml
@@ -17,13 +17,6 @@
ifconfig="recaptcha_frontend/type_for/newsletter">
newsletter
-
- - true
- -
-
- bottomright
- - invisible
-
-
-
-
diff --git a/ReCaptchaNewsletter/view/frontend/web/css/source/_module.less b/ReCaptchaNewsletter/view/frontend/web/css/source/_module.less
new file mode 100644
index 00000000..563319cd
--- /dev/null
+++ b/ReCaptchaNewsletter/view/frontend/web/css/source/_module.less
@@ -0,0 +1,16 @@
+/**
+ * Copyright © Magento, Inc. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+.block.newsletter {
+ .field-recaptcha {
+ .field {
+ .control {
+ &:before {
+ content: none;
+ }
+ }
+ }
+ }
+}
diff --git a/ReCaptchaVersion2Checkbox/etc/adminhtml/system.xml b/ReCaptchaVersion2Checkbox/etc/adminhtml/system.xml
index 6b89c488..f2c5124e 100644
--- a/ReCaptchaVersion2Checkbox/etc/adminhtml/system.xml
+++ b/ReCaptchaVersion2Checkbox/etc/adminhtml/system.xml
@@ -1,3 +1,10 @@
+
+
@@ -6,9 +13,10 @@
showInStore="0">
-
+ Magento\Config\Model\Config\Backend\Encrypted
-
+ Magento\Config\Model\Config\Backend\Encrypted
+
normal
light
@@ -19,6 +20,7 @@
+
normal
light
diff --git a/ReCaptchaVersion2Invisible/etc/adminhtml/system.xml b/ReCaptchaVersion2Invisible/etc/adminhtml/system.xml
index 470ba0bf..66ca5d88 100644
--- a/ReCaptchaVersion2Invisible/etc/adminhtml/system.xml
+++ b/ReCaptchaVersion2Invisible/etc/adminhtml/system.xml
@@ -1,3 +1,10 @@
+
+
@@ -6,9 +13,10 @@
showInStore="0">
-
+ Magento\Config\Model\Config\Backend\Encrypted
-
+ Magento\Config\Model\Config\Backend\Encrypted
+
inline
light
@@ -19,6 +20,7 @@
+
inline
light
diff --git a/ReCaptchaVersion3Invisible/etc/adminhtml/system.xml b/ReCaptchaVersion3Invisible/etc/adminhtml/system.xml
index 379b6156..eaa206bd 100644
--- a/ReCaptchaVersion3Invisible/etc/adminhtml/system.xml
+++ b/ReCaptchaVersion3Invisible/etc/adminhtml/system.xml
@@ -1,3 +1,10 @@
+
+
@@ -6,9 +13,10 @@
showInStore="0">
-
+ Magento\Config\Model\Config\Backend\Encrypted
-
+ Magento\Config\Model\Config\Backend\Encrypted
+
0.5
inline
@@ -20,6 +21,7 @@
+
0.5
inline
diff --git a/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php b/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php
index c314f8ca..e9e499d2 100644
--- a/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php
+++ b/TwoFactorAuth/Model/Provider/Engine/U2fKey/WebAuthn.php
@@ -298,7 +298,9 @@ public function getPublicKeyFromRegistrationData(array $data): array
return [
'key' => $attestationObject['attestationData']['keyBytes'],
'id' => $credentialData['id'],
- 'aaguid' => $attestationObject['attestationData']['aaguid'] ?? null
+ 'aaguid' => empty($attestationObject['attestationData']['aaguid'])
+ ? null
+ : base64_encode($attestationObject['attestationData']['aaguid'])
];
}