Skip to content

Commit 7ebe0c6

Browse files
authored
LYNX-941: [AC-2.4.9] [CE] Implement ReCaptcha for missing GraphQl mutations
1 parent a34aaa3 commit 7ebe0c6

File tree

6 files changed

+290
-15
lines changed

6 files changed

+290
-15
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ReCaptchaContact\Model;
9+
10+
use Magento\ContactGraphQl\Model\Resolver\ContactUs;
11+
use Magento\ReCaptchaUi\Model\IsCaptchaEnabledInterface;
12+
use Magento\ReCaptchaUi\Model\ValidationConfigResolverInterface;
13+
use Magento\ReCaptchaValidationApi\Api\Data\ValidationConfigInterface;
14+
use Magento\ReCaptchaWebapiApi\Api\Data\EndpointInterface;
15+
use Magento\ReCaptchaWebapiApi\Api\WebapiValidationConfigProviderInterface;
16+
17+
class WebapiConfigProvider implements WebapiValidationConfigProviderInterface
18+
{
19+
private const CAPTCHA_ID = 'contact';
20+
21+
/**
22+
* WebapiConfigProvider constructor
23+
*
24+
* @param IsCaptchaEnabledInterface $isEnabled
25+
* @param ValidationConfigResolverInterface $configResolver
26+
*/
27+
public function __construct(
28+
private readonly IsCaptchaEnabledInterface $isEnabled,
29+
private readonly ValidationConfigResolverInterface $configResolver
30+
) {
31+
}
32+
33+
/**
34+
* @inheritDoc
35+
*/
36+
public function getConfigFor(EndpointInterface $endpoint): ?ValidationConfigInterface
37+
{
38+
if ($endpoint->getServiceMethod() === 'resolve'
39+
&& $endpoint->getServiceClass() === ContactUs::class
40+
&& $this->isEnabled->isCaptchaEnabledFor(self::CAPTCHA_ID)
41+
) {
42+
return $this->configResolver->get(self::CAPTCHA_ID);
43+
}
44+
45+
return null;
46+
}
47+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ReCaptchaContact\Test\Api\GraphQl\Contact;
9+
10+
use Magento\TestFramework\Fixture\Config as ConfigFixture;
11+
use Magento\TestFramework\TestCase\GraphQlAbstract;
12+
13+
/**
14+
* GraphQl test for contact us functionality with ReCaptcha enabled.
15+
*/
16+
class ContactUsTest extends GraphQlAbstract
17+
{
18+
#[
19+
ConfigFixture('recaptcha_frontend/type_invisible/public_key', 'test_public_key'),
20+
ConfigFixture('recaptcha_frontend/type_invisible/private_key', 'test_private_key'),
21+
ConfigFixture('recaptcha_frontend/type_for/contact', 'invisible')
22+
]
23+
public function testContactUsReCaptchaValidationFailed(): void
24+
{
25+
$this->expectExceptionMessage('ReCaptcha validation failed, please try again');
26+
$this->graphQlMutation($this->getContactUsMutation());
27+
}
28+
29+
/**
30+
* Get contact us graphql mutation query
31+
*
32+
* @return string
33+
*/
34+
private function getContactUsMutation(): string
35+
{
36+
return <<<MUTATION
37+
mutation {
38+
contactUs(input: {
39+
comment:"Test Contact Us",
40+
email:"test@adobe.com",
41+
name:"John Doe",
42+
telephone:"1111111111"
43+
})
44+
{
45+
status
46+
}
47+
}
48+
MUTATION;
49+
}
50+
}

ReCaptchaContact/composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
"require": {
55
"php": "~8.2.0||~8.3.0||~8.4.0",
66
"magento/framework": "*",
7-
"magento/module-re-captcha-ui": "*"
7+
"magento/module-contact-graph-ql": "*",
8+
"magento/module-re-captcha-ui": "*",
9+
"magento/module-re-captcha-validation-api": "*",
10+
"magento/module-re-captcha-webapi-api": "*"
811
},
912
"type": "magento2-module",
1013
"license": "OSL-3.0",

ReCaptchaContact/etc/di.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
/**
4+
* Copyright 2025 Adobe
5+
* All Rights Reserved.
6+
*/
7+
-->
8+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
9+
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
10+
<type name="Magento\ReCaptchaWebapiApi\Model\CompositeWebapiValidationConfigProvider">
11+
<arguments>
12+
<argument name="providers" xsi:type="array">
13+
<item name="recaptcha_on_contact_form" xsi:type="object">Magento\ReCaptchaContact\Model\WebapiConfigProvider</item>
14+
</argument>
15+
</arguments>
16+
</type>
17+
</config>

ReCaptchaCustomer/Model/WebapiConfigProvider.php

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88

99
namespace Magento\ReCaptchaCustomer\Model;
1010

11+
use Magento\CustomerGraphQl\Model\Resolver\ChangePassword;
12+
use Magento\CustomerGraphQl\Model\Resolver\RequestPasswordResetEmail;
13+
use Magento\CustomerGraphQl\Model\Resolver\ResetPassword;
14+
use Magento\CustomerGraphQl\Model\Resolver\UpdateCustomer;
15+
use Magento\Framework\Exception\InputException;
1116
use Magento\ReCaptchaUi\Model\IsCaptchaEnabledInterface;
1217
use Magento\ReCaptchaUi\Model\ValidationConfigResolverInterface;
1318
use Magento\ReCaptchaValidationApi\Api\Data\ValidationConfigInterface;
@@ -21,7 +26,7 @@ class WebapiConfigProvider implements WebapiValidationConfigProviderInterface
2126
{
2227
private const RESET_PASSWORD_CAPTCHA_ID = 'customer_forgot_password';
2328

24-
private const CHANGE_PASSWORD_CAPTCHA_ID = 'customer_edit';
29+
private const CUSTOMER_EDIT_CAPTCHA_ID = 'customer_edit';
2530

2631
private const LOGIN_CAPTCHA_ID = 'customer_login';
2732

@@ -53,27 +58,49 @@ public function __construct(IsCaptchaEnabledInterface $isEnabled, ValidationConf
5358
* @param string $serviceMethod
5459
* @param string $serviceClass
5560
* @return ValidationConfigInterface|null
61+
* @throws InputException
5662
*/
5763
private function validateChangePasswordCaptcha($serviceMethod, $serviceClass): ?ValidationConfigInterface
5864
{
59-
//phpcs:disable Magento2.PHP.LiteralNamespaces
60-
if ($serviceMethod === 'resetPassword' || $serviceMethod === 'initiatePasswordReset'
61-
|| $serviceClass === 'Magento\CustomerGraphQl\Model\Resolver\ResetPassword'
62-
|| $serviceClass === 'Magento\CustomerGraphQl\Model\Resolver\RequestPasswordResetEmail'
63-
) {
64-
return $this->isEnabled->isCaptchaEnabledFor(self::RESET_PASSWORD_CAPTCHA_ID) ?
65-
$this->configResolver->get(self::RESET_PASSWORD_CAPTCHA_ID) : null;
66-
} elseif ($serviceMethod === 'changePasswordById'
67-
|| $serviceClass === 'Magento\CustomerGraphQl\Model\Resolver\ChangePassword'
68-
) {
69-
return $this->isEnabled->isCaptchaEnabledFor(self::CHANGE_PASSWORD_CAPTCHA_ID) ?
70-
$this->configResolver->get(self::CHANGE_PASSWORD_CAPTCHA_ID) : null;
65+
if ($this->isResetPasswordCase($serviceMethod, $serviceClass)) {
66+
$captchaId = self::RESET_PASSWORD_CAPTCHA_ID;
67+
} elseif ($this->isChangePasswordCase($serviceMethod, $serviceClass)) {
68+
$captchaId = self::CUSTOMER_EDIT_CAPTCHA_ID;
69+
}
70+
71+
if (isset($captchaId) && $this->isEnabled->isCaptchaEnabledFor($captchaId)) {
72+
return $this->configResolver->get($captchaId);
7173
}
72-
//phpcs:enable Magento2.PHP.LiteralNamespaces
7374

7475
return null;
7576
}
7677

78+
/**
79+
* Check if the request is related to reset password
80+
*
81+
* @param string $serviceMethod
82+
* @param string $serviceClass
83+
* @return bool
84+
*/
85+
private function isResetPasswordCase(string $serviceMethod, string $serviceClass): bool
86+
{
87+
return in_array($serviceMethod, ['resetPassword', 'initiatePasswordReset'], true)
88+
|| in_array($serviceClass, [ResetPassword::class, RequestPasswordResetEmail::class], true);
89+
}
90+
91+
/**
92+
* Check if the request is related to change password
93+
*
94+
* @param string $serviceMethod
95+
* @param string $serviceClass
96+
* @return bool
97+
*/
98+
private function isChangePasswordCase(string $serviceMethod, string $serviceClass): bool
99+
{
100+
return $serviceMethod === 'changePasswordById'
101+
|| in_array($serviceClass, [ChangePassword::class, UpdateCustomer::class], true);
102+
}
103+
77104
/**
78105
* Validates ifLoginCaptchaEnabled using captcha_id
79106
*
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\ReCaptchaCustomer\Test\Api\GraphQl\Customer;
9+
10+
use Magento\Customer\Api\Data\CustomerInterface;
11+
use Magento\Customer\Test\Fixture\Customer;
12+
use Magento\Framework\Exception\AuthenticationException;
13+
use Magento\Integration\Api\CustomerTokenServiceInterface;
14+
use Magento\TestFramework\Fixture\Config as ConfigFixture;
15+
use Magento\TestFramework\Fixture\DataFixture;
16+
use Magento\TestFramework\Fixture\DataFixtureStorageManager;
17+
use Magento\TestFramework\Helper\Bootstrap;
18+
use Magento\TestFramework\TestCase\GraphQlAbstract;
19+
20+
/**
21+
* GraphQl test for update customer functionality with ReCaptcha enabled.
22+
*/
23+
class UpdateCustomerTest extends GraphQlAbstract
24+
{
25+
/**
26+
* @var CustomerTokenServiceInterface
27+
*/
28+
private $customerTokenService;
29+
30+
/**
31+
* @var CustomerInterface
32+
*/
33+
private $customer;
34+
35+
protected function setUp(): void
36+
{
37+
$this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class);
38+
$this->customer = DataFixtureStorageManager::getStorage()->get('customer');
39+
}
40+
41+
#[
42+
DataFixture(Customer::class, as: 'customer'),
43+
ConfigFixture('recaptcha_frontend/type_invisible/public_key', 'test_public_key'),
44+
ConfigFixture('recaptcha_frontend/type_invisible/private_key', 'test_private_key'),
45+
ConfigFixture('recaptcha_frontend/type_for/customer_edit', 'invisible')
46+
]
47+
public function testUpdateCustomerV2ReCaptchaValidationFailed(): void
48+
{
49+
$this->expectExceptionMessage('ReCaptcha validation failed, please try again');
50+
$this->graphQlMutation(
51+
$this->getUpdateCustomerV2Mutation(),
52+
[],
53+
'',
54+
$this->getCustomerAuthHeaders($this->customer->getEmail())
55+
);
56+
}
57+
58+
#[
59+
DataFixture(Customer::class, as: 'customer'),
60+
ConfigFixture('recaptcha_frontend/type_invisible/public_key', 'test_public_key'),
61+
ConfigFixture('recaptcha_frontend/type_invisible/private_key', 'test_private_key'),
62+
ConfigFixture('recaptcha_frontend/type_for/customer_edit', 'invisible')
63+
]
64+
public function testUpdateCustomerReCaptchaValidationFailed(): void
65+
{
66+
$this->expectExceptionMessage('ReCaptcha validation failed, please try again');
67+
$this->graphQlMutation(
68+
$this->getUpdateCustomerMutation(),
69+
[],
70+
'',
71+
$this->getCustomerAuthHeaders($this->customer->getEmail())
72+
);
73+
}
74+
75+
/**
76+
* Get update customer graphql mutation
77+
*
78+
* @return string
79+
*/
80+
private function getUpdateCustomerMutation(): string
81+
{
82+
return <<<MUTATION
83+
mutation {
84+
updateCustomer(
85+
input: {
86+
firstname: "Test User"
87+
}
88+
) {
89+
customer {
90+
firstname
91+
}
92+
}
93+
}
94+
MUTATION;
95+
}
96+
97+
/**
98+
* Get update customer V2 graphql mutation
99+
*
100+
* @return string
101+
*/
102+
private function getUpdateCustomerV2Mutation(): string
103+
{
104+
return <<<MUTATION
105+
mutation {
106+
updateCustomerV2(
107+
input: {
108+
firstname: "Test User"
109+
}
110+
) {
111+
customer {
112+
firstname
113+
}
114+
}
115+
}
116+
MUTATION;
117+
}
118+
119+
/**
120+
* Get customer auth headers
121+
*
122+
* @param string $email
123+
* @return array
124+
* @throws AuthenticationException
125+
*/
126+
private function getCustomerAuthHeaders(string $email): array
127+
{
128+
$customerToken = $this->customerTokenService->createCustomerAccessToken($email, 'password');
129+
return ['Authorization' => 'Bearer ' . $customerToken];
130+
}
131+
}

0 commit comments

Comments
 (0)