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']) ]; }