Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.aemds.guide.model.ReCaptchaConfigurationModel;
import com.adobe.aemds.guide.service.CloudConfigurationProvider;
Expand All @@ -50,6 +52,8 @@
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class RecaptchaImpl extends AbstractCaptchaImpl implements Captcha {

private static final Logger LOGGER = LoggerFactory.getLogger(RecaptchaImpl.class);

@Inject
private ResourceResolver resourceResolver;

Expand Down Expand Up @@ -123,4 +127,28 @@ public Map<String, Object> getCaptchaProperties() throws GuideException {
return customCaptchaProperties;

}

@Override
public String getCaptchaDisplayMode() {
// reCAPTCHA v3 has no visible challenge (badge-only), so it is treated as invisible. This
// lets the runtime's built-in submitForm auto-fetch the token before submit, the same way
// it already does for invisible turnstile and enterprise-score keys.
CaptchaDisplayMode captchaDisplayMode = CaptchaDisplayMode.VISIBLE;
String version = null;
resource = resourceResolver.getResource(this.getPath());
if (resource != null && cloudConfigurationProvider != null) {
try {
reCaptchaConfiguration = cloudConfigurationProvider.getRecaptchaCloudConfiguration(resource);
if (reCaptchaConfiguration != null) {
version = reCaptchaConfiguration.version();
}
} catch (GuideException e) {
LOGGER.error("[AF] [Captcha] [RECAPTCHA] Error while fetching cloud configuration to resolve display mode.", e);
}
}
if ("v3".equals(version)) {
captchaDisplayMode = CaptchaDisplayMode.INVISIBLE;
}
return captchaDisplayMode.getValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,31 @@ void getEnterpriseUrl() {
assertEquals("https://www.recaptcha.net/recaptcha/enterprise.js", enterpriseUrl);
}

@Test
void getV3Url() {
// reCAPTCHA v3 uses the classic (non-enterprise) endpoint, same as v2.
when(reCaptchaConfiguration.version()).thenReturn("v3");
Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA, Captcha.class, context);
Map<String, Object> captchaProps = recaptcha.getCaptchaProperties();
assertEquals("https://www.recaptcha.net/recaptcha/api.js", captchaProps.get("uri"));
assertEquals("v3", captchaProps.get("version"));
}

@Test
void getCaptchaDisplayModeForV3() {
// v3 has no visible challenge, so it must report as invisible for auto token fetch on submit.
when(reCaptchaConfiguration.version()).thenReturn("v3");
Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA, Captcha.class, context);
assertEquals("invisible", recaptcha.getCaptchaDisplayMode());
}

@Test
void getCaptchaDisplayModeForNonV3() {
when(reCaptchaConfiguration.version()).thenReturn("enterprise");
Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA, Captcha.class, context);
assertEquals("visible", recaptcha.getCaptchaDisplayMode());
}

@Test
void testNoFieldType() {
Captcha recaptcha = Utils.getComponentUnderTest(PATH_RECAPTCHA_WITHOUT_FEILDTYPE, Captcha.class, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
},
"customProp": "customPropValue"
},
"captchaDisplayMode": "visible",
":type": "core/fd/components/form/recaptcha/v1/recaptcha"
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"FT_FORMS-17107",
"FT_FORMS-24087",
"FT_FORMS-24343",
"FT_FORMS-25252"
"FT_FORMS-25252",
"FT_FORMS-27648"
],
"disabledToggles": [
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="cq:Page">
<jcr:content
cq:lastModified="{Date}2024-05-21T17:20:26.859+05:30"
cq:lastModifiedBy="admin"
cq:template="/libs/fd/af/cloudservices/recaptcha/template"
jcr:lastModified="{Date}2024-05-21T17:20:26.842+05:30"
jcr:lastModifiedBy="admin"
jcr:primaryType="cq:PageContent"
jcr:title="v3Config"
sling:resourceType="fd/af/cloudservices/recaptcha/page"
name="v3"
secretKey="placeholder-v3-secret-key"
siteKey="6LdV1LUtAAAAAM4xLHqg4bxuxl1LKWvWoIcG9Zqd"
version="v3"/>
</jcr:root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:fd="http://www.adobe.com/aemfd/fd/1.0"
jcr:primaryType="cq:Page">
<jcr:content
cq:conf="/conf/core-components-it/samples/recaptcha/basic"
cq:deviceGroups="[/etc/mobile/groups/responsive]"
cq:lastModified="{Date}2024-05-24T17:33:40.560+05:30"
cq:lastModifiedBy="admin"
cq:template="/conf/core-components-examples/settings/wcm/templates/af-blank-v2"
jcr:language="en"
jcr:primaryType="cq:PageContent"
jcr:title="v3"
sling:configRef="/conf/forms/core-components-it/samples/recaptcha/v3/"
sling:resourceType="forms-components-examples/components/page">
<guideContainer
fd:version="2.1"
jcr:lastModified="{Date}2024-05-24T17:33:40.543+05:30"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
sling:resourceType="forms-components-examples/components/form/container"
actionType="forms-core-components-it/customsubmission/logsubmit"
dorType="none"
fieldType="form"
schemaType="none"
specVersion="0.13.0"
textIsRich="true"
thankYouMessage="Thank you for submitting the form."
thankYouOption="page"
themeRef="/libs/fd/af/themes/canvas"
title="v3">
<recaptcha
jcr:lastModified="{Date}2024-05-24T17:33:17.916+05:30"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
jcr:title="CAPTCHA"
sling:resourceType="forms-core-components-it/form/recaptcha"
cloudServicePath="v2checkbox"
enabled="{Boolean}true"
fieldType="captcha"
hideTitle="false"
name="recaptcha-test"
rcCloudServicePath="v3"
readOnly="{Boolean}false"
size="normal"
textIsRich="true"
unboundFormElement="{Boolean}false"
required="{Boolean}true"
visible="{Boolean}true"/>
<submit
jcr:created="{Date}2024-05-24T17:33:23.737+05:30"
jcr:createdBy="admin"
jcr:lastModified="{Date}2024-05-24T17:33:23.737+05:30"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
jcr:title="Submit"
sling:resourceType="forms-components-examples/components/form/actions/submit"
buttonType="submit"
dorExclusion="true"
fieldType="button"
name="submit1716552204262">
<fd:events
jcr:primaryType="nt:unstructured"
click="[submitForm()]"/>
</submit>
</guideContainer>
</jcr:content>
</jcr:root>
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
this._model.subscribe((action) => {
let state = action.target.getState();
// execute the handler only if there are no rules configured on submitSuccess event.
if (!state.events.submitSuccess || state.events.submitSuccess.length === 0) {
// getState() omits `events` in the XFA runtime, so guard the access (optional
// chaining) to avoid a TypeError; a missing events object means no rule is configured.
if (!state.events?.submitSuccess || state.events.submitSuccess.length === 0) {
const globals = {
form: self.getModel().getRuleNode(),
event: {
Expand All @@ -48,7 +50,7 @@
this._model.subscribe((action) => {
let state = action.target.getState();
// execute the handler only if there are no rules configured on submitError event.
if (!state.events.submitError || state.events.submitError.length === 0) {
if (!state.events?.submitError || state.events.submitError.length === 0) {
let defaultSubmissionError = FormView.LanguageUtils.getTranslatedString(self.getLang(), "InternalFormSubmissionError");
const globals = {
form: self.getModel().getRuleNode(),
Expand All @@ -64,15 +66,15 @@
this._model.subscribe((action) => {
let state = action.target.getState();
// execute the handler only if there are no rules configured on custom:saveSuccess event.
if (!state.events['custom:saveSuccess'] || state.events['custom:saveSuccess'].length === 0) {
if (!state.events?.['custom:saveSuccess'] || state.events['custom:saveSuccess'].length === 0) {
console.log("Draft id = " + action?.payload?.body?.draftId);
window.alert(FormView.LanguageUtils.getTranslatedString(self.getLang(), "saveDraftSuccessMessage"));
}
}, "saveSuccess");
this._model.subscribe((action) => {
let state = action.target.getState();
// execute the handler only if there are no rules configured on custom:saveError event.
if (!state.events['custom:saveError'] || state.events['custom:saveError'].length === 0) {
if (!state.events?.['custom:saveError'] || state.events['custom:saveError'].length === 0) {
window.alert(FormView.LanguageUtils.getTranslatedString(self.getLang(), "saveDraftErrorMessage"));
}
}, "saveError");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Adaptive Form Recaptcha field component written in HTL.
* Supports the following recaptcha versions:
* v2
* Only visible behaviour is supported
* v3
* Only invisible behaviour is supported (badge-only, no visible challenge); the token is
auto-fetched at submit. Requires the FT_FORMS-27648 feature toggle to be enabled.
* Enterprise
* Checkbox keys (Visible Behaviour)
* Score-based keys (Invisible behaviour)
Expand All @@ -45,7 +48,7 @@ The following properties are written to JCR for this Form Recaptcha component an
7. `./requiredMessage` - defines the message displayed as tooltip when submitting the form if the value is left empty
8. `./readOnly` - if set to `true`, the filed will be read only
9. `./rcCloudServicePath` - defines the path of cloud configuration resource for reCAPTCHA
10. `./recaptchaSize` - defines the size attribute of Google reCAPTCHA. This property is only valid for reCAPTCHA v2 and the checkbox keys of reCAPTCHA enterprise. This property will be disabled for reCAPTCHA Enteprise score-based keys.
10. `./recaptchaSize` - defines the size attribute of Google reCAPTCHA. This property is only valid for reCAPTCHA v2 and the checkbox keys of reCAPTCHA enterprise. This property will be disabled for reCAPTCHA Enterprise score-based keys and for reCAPTCHA v3 (both invisible).

## Client Libraries
The component provides a `core.forms.components.recaptcha.v1.runtime` client library category that contains the Javascript runtime for the component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
const selectedVersion = selectedConfig.getAttribute("data-version");
const selectedKeyType = selectedConfig.getAttribute("data-key.type");
const inputs = recaptchaSize.querySelectorAll('input');
if (selectedVersion === "enterprise" && selectedKeyType === "score") {
// v3 has no keyType concept - it is always invisible/score-based, same as
// Enterprise's score-based key case.
if ((selectedVersion === "enterprise" && selectedKeyType === "score") || selectedVersion === "v3") {
inputs.forEach(input => input.setAttribute("disabled", true));
} else {
inputs.forEach(input => input.removeAttribute("disabled"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ if (typeof window.RecaptchaWidget === 'undefined') {

var self = this;
var recaptchaConfigData = this.#options;
// reCAPTCHA v3 has no "invisible" size setting like classic v2 - it is always
// badge-only/invisible, so it needs the same treatment as an explicit invisible size.
var isRecaptchaV3 = recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.version === "v3";
element.innerHTML = '<div class="g-recaptcha"></div>';
var gcontainer = document.getElementsByClassName("g-recaptcha")[0];
var widgetId;
var url = recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.uri;
if (recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.size == "invisible") {
if (recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.size == "invisible" || isRecaptchaV3) {
gcontainer.classList.add('g-recaptcha-invisible');
recaptchaConfigData.required = false;
}
Expand Down Expand Up @@ -94,7 +97,7 @@ if (typeof window.RecaptchaWidget === 'undefined') {
var runtimeLocale = this.#lang;

var scr = document.createElement('script');
let queryParams = isScoreBasedKey() ? "?render=" + recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.siteKey
let queryParams = (isScoreBasedKey() || isRecaptchaV3) ? "?render=" + recaptchaConfigData.properties[RecaptchaWidget.FD_CAPTCHA].config.siteKey
: "?onload=onloadRecaptchaCallback&render=explicit";
queryParams += "&hl=" + runtimeLocale;
scr.src = url + queryParams;
Expand Down
14 changes: 7 additions & 7 deletions ui.frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui.frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
"dependencies": {
"@aemforms/af-core": "^1.0.4",
"@aemforms/af-formatters": "^1.0.4",
"@aemforms/af-custom-functions": "1.0.17"
"@aemforms/af-custom-functions": "1.0.18"
}
}
2 changes: 1 addition & 1 deletion ui.frontend/src/customFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const customFunctions = {
/**
* Fetches the captcha token for the form.
*
* This function uses the Google reCAPTCHA Enterprise service to fetch the captcha token.
* Supports turnstile, reCAPTCHA Enterprise, and reCAPTCHA v3 via @aemforms/af-custom-functions.
*
* @async
* @param {object} globals - An object containing read-only form instance, read-only target field instance and methods for form modifications.
Expand Down
2 changes: 2 additions & 0 deletions ui.tests/test-module/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Local Cypress secrets/overrides — must never be committed
cypress.env.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ describe('Page - Authoring', function () {
cy.get("input[name='./recaptchaSize'][value='normal']").should("be.disabled");
cy.get("input[name='./recaptchaSize'][value='compact']").should("be.disabled");

// reCAPTCHA v3 is badge-only (no visible challenge), so size must be disabled like enterprise-score.
cy.get(".cmp-adaptiveform-recaptcha__configuration").click().then(() => {
cy.get("coral-selectlist-item[value='v3']").click();
cy.get("input[name='./recaptchaSize'][value='normal']").should("be.disabled");
cy.get("input[name='./recaptchaSize'][value='compact']").should("be.disabled");
})

cy.get(".cmp-adaptiveform-recaptcha__configuration").click().then(() => {
cy.get("coral-selectlist-item[value='v2checkbox']").click();
Expand Down
Loading
Loading