Skip to content

Commit cc61800

Browse files
authored
Merge pull request #107 from magento-l3/PR-2023-02-24
Pr 2023 02 24 - Security
2 parents e7cabf0 + db75442 commit cc61800

File tree

18 files changed

+1388
-21
lines changed

18 files changed

+1388
-21
lines changed

ReCaptchaCheckout/Block/LayoutProcessor/Checkout/Onepage.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ public function process($jsLayout)
7575
$key = 'place_order';
7676
if ($this->isCaptchaEnabled->isCaptchaEnabledFor($key)) {
7777
$jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
78-
['payment']['children']['beforeMethods']['children']['place-order-recaptcha-container']['children']
78+
['payment']['children']['payments-list']['children']['before-place-order']['children']
7979
['place-order-recaptcha']['settings'] = $this->captchaUiConfigResolver->get($key);
8080
} else {
8181
if (isset($jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
82-
['payment']['children']['beforeMethods']['children']['place-order-recaptcha-container']['children']
82+
['payment']['children']['payments-list']['children']['before-place-order']['children']
8383
['place-order-recaptcha'])) {
8484
unset($jsLayout['components']['checkout']['children']['steps']['children']['billing-step']['children']
85-
['payment']['children']['beforeMethods']['children']['place-order-recaptcha-container']
85+
['payment']['children']['payments-list']['children']['before-place-order']
8686
['children']['place-order-recaptcha']);
8787
}
8888
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ReCaptchaCheckout\Test\Unit\Block\LayoutProcessor\Checkout;
9+
10+
use Magento\Framework\DataObject;
11+
use Magento\ReCaptchaCheckout\Block\LayoutProcessor\Checkout\Onepage;
12+
use Magento\ReCaptchaUi\Model\IsCaptchaEnabledInterface;
13+
use Magento\ReCaptchaUi\Model\UiConfigResolverInterface;
14+
use PHPUnit\Framework\MockObject\MockObject;
15+
use PHPUnit\Framework\TestCase;
16+
17+
class OnepageTest extends TestCase
18+
{
19+
/**
20+
* @var UiConfigResolverInterface|MockObject
21+
*/
22+
private $uiConfigResolver;
23+
24+
/**
25+
* @var IsCaptchaEnabledInterface|MockObject
26+
*/
27+
private $isCaptchEnabled;
28+
29+
/**
30+
* @var Onepage
31+
*/
32+
private $model;
33+
34+
/**
35+
* @var array
36+
*/
37+
private $jsLayout = [
38+
'components' => [
39+
'checkout' => [
40+
'children' => [
41+
'steps' => [
42+
'children' => [
43+
'shipping-step' => [
44+
'children' => [
45+
'shippingAddress' => [
46+
'children' => [
47+
'customer-email' => [
48+
'children' => [
49+
'recaptcha' => []
50+
]
51+
]
52+
]
53+
]
54+
]
55+
],
56+
'billing-step' => [
57+
'children' => [
58+
'payment' => [
59+
'children' => [
60+
'customer-email' => [
61+
'children' => [
62+
'recaptcha' => []
63+
]
64+
],
65+
'payments-list' => [
66+
'children' => [
67+
'before-place-order' => [
68+
'children' => [
69+
'place-order-recaptcha' => []
70+
]
71+
]
72+
]
73+
]
74+
]
75+
]
76+
]
77+
]
78+
]
79+
],
80+
'authentication' => [
81+
'children' => [
82+
'recaptcha' => []
83+
]
84+
]
85+
]
86+
]
87+
]
88+
];
89+
90+
/**
91+
* @inheritdoc
92+
*/
93+
protected function setUp(): void
94+
{
95+
parent::setUp();
96+
$this->uiConfigResolver = $this->getMockForAbstractClass(UiConfigResolverInterface::class);
97+
$this->isCaptchEnabled = $this->getMockForAbstractClass(IsCaptchaEnabledInterface::class);
98+
$this->model = new Onepage(
99+
$this->uiConfigResolver,
100+
$this->isCaptchEnabled
101+
);
102+
}
103+
104+
/**
105+
* @dataProvider processDataProvider
106+
*/
107+
public function testProcess(array $mocks, array $expected): void
108+
{
109+
$this->uiConfigResolver->method('get')
110+
->willReturnMap($mocks['uiConfigResolver']);
111+
$this->isCaptchEnabled->method('isCaptchaEnabledFor')
112+
->willReturnMap($mocks['isCaptchaEnabled']);
113+
$prefix = 'components/checkout/children/';
114+
$config = new DataObject($this->model->process($this->jsLayout));
115+
$actual = [];
116+
foreach (array_keys($expected) as $path) {
117+
$actual[$path] = $config->getDataByPath($prefix.$path);
118+
}
119+
$this->assertSame($expected, $actual);
120+
}
121+
122+
public function processDataProvider(): array
123+
{
124+
return [
125+
[
126+
[
127+
'isCaptchaEnabled' => [
128+
['customer_login', false],
129+
['place_order', false],
130+
],
131+
'uiConfigResolver' => [
132+
['customer_login', ['type' => 'invisible']],
133+
['place_order', ['type' => 'robot']],
134+
],
135+
],
136+
[
137+
'steps/children/shipping-step/children/shippingAddress/children/customer-email/children' => [],
138+
'steps/children/billing-step/children/payment/children/customer-email/children' => [],
139+
'authentication/children' => [],
140+
'steps/children/billing-step/children/payment/children/payments-list/children/before-place-order/' .
141+
'children' => [],
142+
]
143+
],
144+
[
145+
[
146+
'isCaptchaEnabled' => [
147+
['customer_login', true],
148+
['place_order', true],
149+
],
150+
'uiConfigResolver' => [
151+
['customer_login', ['type' => 'invisible']],
152+
['place_order', ['type' => 'robot']],
153+
],
154+
],
155+
[
156+
'steps/children/shipping-step/children/shippingAddress/children/' .
157+
'customer-email/children' => ['recaptcha' => ['settings' => ['type' => 'invisible']]],
158+
'steps/children/billing-step/children/payment/children/' .
159+
'customer-email/children' => ['recaptcha' => ['settings' => ['type' => 'invisible']]],
160+
'authentication/children' => ['recaptcha' => ['settings' => ['type' => 'invisible']]],
161+
'steps/children/billing-step/children/payment/children/payments-list/children/before-place-order/' .
162+
'children' => ['place-order-recaptcha' => ['settings' => ['type' => 'robot']]],
163+
]
164+
]
165+
];
166+
}
167+
}

ReCaptchaCheckout/view/frontend/layout/checkout_index_index.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,18 @@
4949
</item>
5050
</item>
5151
</item>
52-
<item name="beforeMethods" xsi:type="array">
52+
<item name="payments-list" xsi:type="array">
5353
<item name="children" xsi:type="array">
54-
<item name="place-order-recaptcha-container" xsi:type="array">
55-
<item name="component" xsi:type="string">uiComponent</item>
56-
<item name="template" xsi:type="string">Magento_ReCaptchaCheckout/payment-recaptcha-container</item>
57-
<item name="displayArea" xsi:type="string">beforeMethods</item>
54+
<item name="before-place-order" xsi:type="array">
5855
<item name="children" xsi:type="array">
5956
<item name="place-order-recaptcha" xsi:type="array">
60-
<item name="component" xsi:type="string">Magento_ReCaptchaWebapiUi/js/webapiReCaptcha</item>
61-
<item name="displayArea" xsi:type="string">place-order-recaptcha</item>
57+
<item name="component" xsi:type="string">Magento_ReCaptchaCheckout/js/reCaptchaCheckout</item>
58+
<item name="displayArea" xsi:type="string">before-place-order</item>
6259
<item name="configSource" xsi:type="string">checkoutConfig</item>
6360
<item name="reCaptchaId" xsi:type="string">recaptcha-checkout-place-order</item>
61+
<item name="skipPayments" xsi:type="array">
62+
63+
</item>
6464
</item>
6565
</item>
6666
</item>

ReCaptchaCheckout/view/frontend/web/js/model/place-order-mixin.js

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ define([
1414

1515
return function (placeOrder) {
1616
return wrapper.wrap(placeOrder, function (originalAction, serviceUrl, payload, messageContainer) {
17-
var recaptchaDeferred;
17+
var recaptchaDeferred,
18+
reCaptchaId,
19+
$activeReCaptcha;
1820

19-
if (recaptchaRegistry.triggers.hasOwnProperty('recaptcha-checkout-place-order')) {
21+
$activeReCaptcha = $('.recaptcha-checkout-place-order:visible .g-recaptcha');
22+
23+
if ($activeReCaptcha.length > 0) {
24+
reCaptchaId = $activeReCaptcha.last().attr('id');
25+
}
26+
27+
if (reCaptchaId !== undefined && recaptchaRegistry.triggers.hasOwnProperty(reCaptchaId)) {
2028
//ReCaptcha is present for checkout
2129
recaptchaDeferred = $.Deferred();
22-
recaptchaRegistry.addListener('recaptcha-checkout-place-order', function (token) {
30+
recaptchaRegistry.addListener(reCaptchaId, function (token) {
2331
//Add reCaptcha value to place-order request and resolve deferred with the API call results
2432
payload.xReCaptchaValue = token;
2533
originalAction(serviceUrl, payload, messageContainer).done(function () {
@@ -29,14 +37,14 @@ define([
2937
});
3038
});
3139
//Trigger ReCaptcha validation
32-
recaptchaRegistry.triggers['recaptcha-checkout-place-order']();
40+
recaptchaRegistry.triggers[reCaptchaId]();
3341

3442
if (
35-
!recaptchaRegistry._isInvisibleType.hasOwnProperty('recaptcha-checkout-place-order') ||
36-
recaptchaRegistry._isInvisibleType['recaptcha-checkout-place-order'] === false
43+
!recaptchaRegistry._isInvisibleType.hasOwnProperty(reCaptchaId) ||
44+
recaptchaRegistry._isInvisibleType[reCaptchaId] === false
3745
) {
3846
//remove listener so that place order action is only triggered by the 'Place Order' button
39-
recaptchaRegistry.removeListener('recaptcha-checkout-place-order');
47+
recaptchaRegistry.removeListener(reCaptchaId);
4048
}
4149

4250
return recaptchaDeferred;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Copyright © Magento, Inc. All rights reserved.
3+
* See COPYING.txt for license details.
4+
*/
5+
6+
define(
7+
[
8+
'Magento_ReCaptchaWebapiUi/js/webapiReCaptcha',
9+
'jquery'
10+
],
11+
function (Component, $) {
12+
'use strict';
13+
14+
var reCaptchaIds = new WeakMap(),
15+
uuid = 0;
16+
17+
return Component.extend({
18+
defaults: {
19+
template: 'Magento_ReCaptchaCheckout/reCaptcha',
20+
skipPayments: [] // List of payment methods that do not require this reCaptcha
21+
},
22+
23+
/**
24+
* Render reCAPTCHA for payment method
25+
*
26+
* @param {Object} method
27+
*/
28+
renderReCaptchaFor: function (method) {
29+
var reCaptcha;
30+
31+
if (this.isCheckoutReCaptchaRequiredFor(method)) {
32+
reCaptcha = $.extend(true, {}, this, {reCaptchaId: this.getReCaptchaIdFor(method)});
33+
reCaptcha.renderReCaptcha();
34+
}
35+
},
36+
37+
/**
38+
* Get reCAPTCHA ID for payment method
39+
*
40+
* @param {Object} method
41+
* @returns {String}
42+
*/
43+
getReCaptchaIdFor: function (method) {
44+
if (!reCaptchaIds.has(method)) {
45+
reCaptchaIds.set(method, this.getReCaptchaId() + '-' + uuid++);
46+
}
47+
return reCaptchaIds.get(method);
48+
},
49+
50+
/**
51+
* Check whether checkout reCAPTCHA is required for payment method
52+
*
53+
* @param {Object} method
54+
* @returns {Boolean}
55+
*/
56+
isCheckoutReCaptchaRequiredFor: function (method) {
57+
return !this.skipPayments || !this.skipPayments.hasOwnProperty(method.getCode());
58+
},
59+
60+
/**
61+
* @inheritdoc
62+
*/
63+
initCaptcha: function () {
64+
var $wrapper,
65+
$recaptchaResponseInput;
66+
67+
this._super();
68+
// Since there will be multiple reCaptcha in the payment form,
69+
// they may override each other if the form data is serialized and submitted.
70+
// Instead, the reCaptcha response will be collected in the callback: reCaptchaCallback()
71+
// and sent in the request header X-ReCaptcha
72+
$wrapper = $('#' + this.getReCaptchaId() + '-wrapper');
73+
$recaptchaResponseInput = $wrapper.find('[name=g-recaptcha-response]');
74+
if ($recaptchaResponseInput.length) {
75+
$recaptchaResponseInput.prop('disabled', true);
76+
}
77+
}
78+
});
79+
}
80+
);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!--
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
-->
7+
<!-- ko if: (isCheckoutReCaptchaRequiredFor($parents[1]))-->
8+
<div class="recaptcha-checkout-place-order" data-bind="{
9+
attr: {
10+
'id': getReCaptchaIdFor($parents[1]) + '-wrapper'
11+
},
12+
'afterRender': renderReCaptchaFor($parents[1])
13+
}">
14+
<div class="g-recaptcha"></div>
15+
<!-- ko if: (!getIsInvisibleRecaptcha()) -->
16+
<div class="field">
17+
<div class="control">
18+
<input type="checkbox"
19+
value=""
20+
class="required-captcha checkbox"
21+
name="recaptcha-validate-"
22+
data-validate="{required:true}"
23+
tabindex="-1">
24+
</div>
25+
</div>
26+
<!-- /ko -->
27+
</div>
28+
<!-- /ko -->

0 commit comments

Comments
 (0)