diff --git a/.github/workflows/exporter-validate-pr.yml b/.github/workflows/exporter-validate-pr.yml
index 8f3ac2c5b8..0907dd8f64 100644
--- a/.github/workflows/exporter-validate-pr.yml
+++ b/.github/workflows/exporter-validate-pr.yml
@@ -29,34 +29,39 @@ jobs:
# Loop through each changed file
for file in $changed_files; do
- # Fetch the base and head versions of the file
- base_file=$(git show ${{ github.base_ref }}:$file)
- head_file=$(git show ${{ github.head_ref }}:$file)
+ # Check if the file exists in both branches
+ if git cat-file -e origin/${{ github.base_ref }}:$file 2>/dev/null && git cat-file -e origin/${{ github.head_ref }}:$file 2>/dev/null; then
+ # Fetch the base and head versions of the file
+ base_file=$(git show origin/${{ github.base_ref }}:$file)
+ head_file=$(git show origin/${{ github.head_ref }}:$file)
- # Compare the JSON keys
- base_keys=$(echo "$base_file" | jq -r 'paths | map(tostring) | join(".")' | sed 's/\./\\./g')
- head_keys=$(echo "$head_file" | jq -r 'paths | map(tostring) | join(".")' | sed 's/\./\\./g')
+ # Compare the JSON keys
+ base_keys=$(echo "$base_file" | jq -r 'paths | map(tostring) | join(".")' | sed 's/\./\\./g')
+ head_keys=$(echo "$head_file" | jq -r 'paths | map(tostring) | join(".")' | sed 's/\./\\./g')
- # Check for removed keys
- removed_keys=$(comm -23 <(echo "$base_keys" | sort) <(echo "$head_keys" | sort))
+ # Check for removed keys
+ removed_keys=$(comm -23 <(echo "$base_keys" | sort) <(echo "$head_keys" | sort))
- if [ -n "$removed_keys" ]; then
- echo "Backward incompatibility change detected in $file. The following keys were removed:"
- echo "$removed_keys"
- exit 1
- fi
+ if [ -n "$removed_keys" ]; then
+ echo "Backward incompatibility change detected in $file. The following keys were removed:"
+ echo "$removed_keys"
+ exit 1
+ fi
# Check for changed values
for key in $base_keys; do
base_value=$(echo "$base_file" | jq -r --arg key "$key" '.[$key]')
head_value=$(echo "$head_file" | jq -r --arg key "$key" '.[$key]')
- if [ "$base_value" != "$head_value" ]; then
- echo "Backward incompatibility change detected in $file. The value of key '$key' was changed from '$base_value' to '$head_value'."
- exit 1
- fi
- done
+ if [ "$base_value" != "$head_value" ]; then
+ echo "Backward incompatibility change detected in $file. The value of key '$key' was changed from '$base_value' to '$head_value'."
+ exit 1
+ fi
+ done
+ else
+ echo "Skipping file $file as it exists in one branch but not the other."
+ fi
done
echo "All exporter JSON files have only additions. No backward incompatibility changes detected."
- shell: bash
\ No newline at end of file
+ shell: bash
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java
index 09986f6e91..2944ee3f6c 100644
--- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java
@@ -74,7 +74,10 @@ private FormConstants() {
/** The resource type for check box group v1 */
public static final String RT_FD_FORM_CHECKBOX_GROUP_V1 = RT_FD_FORM_PREFIX + "checkboxgroup/v1/checkboxgroup";
- /** The resource type for reCaptcha v1 */
+ /** The resource type for turnstile v1 */
+ public static final String RT_FD_FORM_TURNSTILE_V1 = RT_FD_FORM_PREFIX + "turnstile/v1/turnstile";
+
+ /** The resource type for hCaptcha v1 */
public static final String RT_FD_FORM_HCAPTCHA_V1 = RT_FD_FORM_PREFIX + "hcaptcha/v1/hcaptcha";
/** The resource type for reCaptcha v1 */
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/HCaptchaImpl.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/HCaptchaImpl.java
index 82bc77d889..ad27b1d95f 100644
--- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/HCaptchaImpl.java
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/HCaptchaImpl.java
@@ -42,7 +42,6 @@
import com.adobe.cq.forms.core.components.internal.form.ReservedProperties;
import com.adobe.cq.forms.core.components.models.form.HCaptcha;
import com.adobe.cq.forms.core.components.util.AbstractCaptchaImpl;
-import com.fasterxml.jackson.annotation.JsonIgnore;
@Model(
adaptables = { SlingHttpServletRequest.class, Resource.class },
@@ -68,26 +67,23 @@ public class HCaptchaImpl extends AbstractCaptchaImpl implements HCaptcha {
private CloudConfigurationProvider cloudConfigurationProvider;
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
- @JsonIgnore
@Named(ReservedProperties.PN_CLOUD_SERVICE_PATH)
protected String cloudServicePath;
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
- @JsonIgnore
@Named(ReservedProperties.PN_SIZE)
protected String size;
- private static final String SITE_KEY = "siteKey";
- private static final String URI = "uri";
- private static final String SIZE = "size";
- private static final String THEME = "theme";
- private static final String TYPE = "type";
-
@Override
public String getCloudServicePath() {
return cloudServicePath;
}
+ @Override
+ public String getSize() {
+ return size;
+ }
+
@Override
public String getProvider() {
return "hcaptcha";
@@ -113,11 +109,11 @@ public Map getCaptchaProperties() throws GuideException {
} catch (GuideException e) {
LOGGER.error("[AF] [Captcha] [HCAPTCHA] Error while fetching cloud configuration, upgrade to latest release to use hCaptcha.");
}
- customCaptchaProperties.put(SITE_KEY, siteKey);
- customCaptchaProperties.put(URI, uri);
- customCaptchaProperties.put(SIZE, this.size);
- customCaptchaProperties.put(THEME, "light");
- customCaptchaProperties.put(TYPE, "image");
+ customCaptchaProperties.put(CAPTCHA_SITE_KEY, siteKey);
+ customCaptchaProperties.put(CAPTCHA_URI, uri);
+ customCaptchaProperties.put(CAPTCHA_SIZE, getSize());
+ customCaptchaProperties.put(CAPTCHA_THEME, "light");
+ customCaptchaProperties.put(CAPTCHA_TYPE, "image");
return customCaptchaProperties;
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java
index 1127021c1d..3f93b6f050 100644
--- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/RecaptchaImpl.java
@@ -41,7 +41,6 @@
import com.adobe.cq.forms.core.components.internal.form.ReservedProperties;
import com.adobe.cq.forms.core.components.models.form.Captcha;
import com.adobe.cq.forms.core.components.util.AbstractCaptchaImpl;
-import com.fasterxml.jackson.annotation.JsonIgnore;
@Model(
adaptables = { SlingHttpServletRequest.class, Resource.class },
@@ -65,33 +64,25 @@ public class RecaptchaImpl extends AbstractCaptchaImpl implements Captcha {
private CloudConfigurationProvider cloudConfigurationProvider;
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
- @JsonIgnore
@Named(ReservedProperties.PN_RECAPTCHA_CLOUD_SERVICE_PATH)
protected String cloudServicePath;
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
- @JsonIgnore
@Named(ReservedProperties.PN_RECAPTCHA_SIZE)
protected String size;
public static final String RECAPTCHA_DEFAULT_DOMAIN = "https://www.recaptcha.net/";
public static final String RECAPTCHA_DEFAULT_URL = RECAPTCHA_DEFAULT_DOMAIN + "recaptcha/api.js";
public static final String RECAPTCHA_ENTERPRISE_DEFAULT_URL = RECAPTCHA_DEFAULT_DOMAIN + "recaptcha/enterprise.js";
- private static final String RECAPTCHA_SITE_KEY = "siteKey";
- private static final String RECAPTCHA_URI = "uri";
- private static final String RECAPTCHA_SIZE = "size";
- private static final String RECAPTCHA_THEME = "theme";
- private static final String RECAPTCHA_TYPE = "type";
- private static final String RECAPTCHA_VERSION = "version";
- private static final String RECAPTCHA_KEYTYPE = "keyType";
+ public static final String RECAPTCHA_VERSION = "version";
+ public static final String RECAPTCHA_KEYTYPE = "keyType";
@Override
- @JsonIgnore
public String getCloudServicePath() {
return cloudServicePath;
}
- @JsonIgnore
+ @Override
public String getSize() {
return size;
}
@@ -101,7 +92,6 @@ public String getProvider() {
return "recaptcha";
}
- @JsonIgnore
@Override
public Map getCaptchaProperties() throws GuideException {
@@ -118,20 +108,19 @@ public Map getCaptchaProperties() throws GuideException {
keyType = reCaptchaConfiguration.keyType();
}
}
- customCaptchaProperties.put(RECAPTCHA_SITE_KEY, siteKey);
+ customCaptchaProperties.put(CAPTCHA_SITE_KEY, siteKey);
if (StringUtils.isNotEmpty(version) && version.equals("enterprise")) {
- customCaptchaProperties.put(RECAPTCHA_URI, RECAPTCHA_ENTERPRISE_DEFAULT_URL);
+ customCaptchaProperties.put(CAPTCHA_URI, RECAPTCHA_ENTERPRISE_DEFAULT_URL);
} else {
- customCaptchaProperties.put(RECAPTCHA_URI, RECAPTCHA_DEFAULT_URL);
+ customCaptchaProperties.put(CAPTCHA_URI, RECAPTCHA_DEFAULT_URL);
}
- customCaptchaProperties.put(RECAPTCHA_SIZE, getSize());
- customCaptchaProperties.put(RECAPTCHA_THEME, "light");
- customCaptchaProperties.put(RECAPTCHA_TYPE, "image");
+ customCaptchaProperties.put(CAPTCHA_SIZE, getSize());
+ customCaptchaProperties.put(CAPTCHA_THEME, "light");
+ customCaptchaProperties.put(CAPTCHA_TYPE, "image");
customCaptchaProperties.put(RECAPTCHA_VERSION, version);
customCaptchaProperties.put(RECAPTCHA_KEYTYPE, keyType);
return customCaptchaProperties;
}
-
}
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/TurnstileImpl.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/TurnstileImpl.java
new file mode 100644
index 0000000000..d1f3991b9e
--- /dev/null
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/TurnstileImpl.java
@@ -0,0 +1,156 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Copyright 2024 Adobe
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package com.adobe.cq.forms.core.components.internal.models.v1.form;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.models.annotations.Exporter;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
+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.TurnstileConfiguration;
+import com.adobe.aemds.guide.service.CloudConfigurationProvider;
+import com.adobe.aemds.guide.service.GuideException;
+import com.adobe.cq.export.json.ComponentExporter;
+import com.adobe.cq.export.json.ExporterConstants;
+import com.adobe.cq.forms.core.components.internal.form.FormConstants;
+import com.adobe.cq.forms.core.components.models.form.Turnstile;
+import com.adobe.cq.forms.core.components.util.AbstractCaptchaImplV2;
+
+@Model(
+ adaptables = { SlingHttpServletRequest.class, Resource.class },
+ adapters = { Turnstile.class,
+ ComponentExporter.class },
+ resourceType = { FormConstants.RT_FD_FORM_TURNSTILE_V1 })
+@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
+public class TurnstileImpl extends AbstractCaptchaImplV2 implements Turnstile {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TurnstileImpl.class);
+
+ @Inject
+ private ResourceResolver resourceResolver;
+
+ private Resource resource;
+ private String captchaSiteKey;
+
+ @Reference
+ @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
+ private TurnstileConfiguration turnstileConfiguration;
+
+ @OSGiService
+ @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
+ private CloudConfigurationProvider cloudConfigurationProvider;
+
+ @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
+ @Named("cloudServicePath")
+ protected String cloudServicePath;
+
+ @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
+ @Named("size")
+ protected String size;
+
+ @Override
+ public String getCloudServicePath() {
+ return cloudServicePath;
+ }
+
+ @Override
+ public String getProvider() {
+ return "turnstile";
+ }
+
+ @Override
+ public String getSize() {
+ return size;
+ }
+
+ /**
+ * Set the turnstileConfiguration, by fetching it from the cloud configurations.
+ * Also sets the captchaSiteKey.
+ */
+ private void setTurnstileConfiguration() {
+ LOGGER.debug("[AF] [Captcha] [TURNSTILE] Fetching cloud configuration for turnstile.");
+ if (cloudConfigurationProvider != null) {
+ try {
+ resource = resourceResolver.getResource(this.getPath());
+ turnstileConfiguration = cloudConfigurationProvider.getTurnstileCloudConfiguration(resource);
+ if (turnstileConfiguration != null) {
+ captchaSiteKey = turnstileConfiguration.getSiteKey();
+ } else {
+ LOGGER.debug("[AF] [Captcha] [TURNSTILE] Cloud configuration for turnstile is not available for " + this.getPath());
+ }
+ } catch (GuideException e) {
+ LOGGER.error(
+ "[AF] [Captcha] [TURNSTILE] Error while fetching cloud configuration, upgrade to latest release to use turnstile.", e);
+ }
+ } else {
+ LOGGER.error(
+ "[AF] [Captcha] [TURNSTILE] Error while fetching cloud configuration, upgrade to latest release to use turnstile.");
+ }
+ }
+
+ @PostConstruct
+ @Override
+ public Map getCaptchaProperties() {
+ Map customCaptchaProperties = new LinkedHashMap<>();
+ String siteKey = null, uri = null, widgetType = null;
+ if (turnstileConfiguration == null) {
+ setTurnstileConfiguration();
+ }
+ if (turnstileConfiguration != null) {
+ customCaptchaProperties.put(CAPTCHA_URI, turnstileConfiguration.getClientSideJsUrl());
+ customCaptchaProperties.put(CAPTCHA_WIDGET_TYPE, turnstileConfiguration.getWidgetType());
+ }
+ customCaptchaProperties.put(CAPTCHA_SIZE, getSize());
+ customCaptchaProperties.put(CAPTCHA_THEME, CAPTCHA_THEME_LIGHT);
+ return customCaptchaProperties;
+ }
+
+ @PostConstruct
+ @Override
+ public String getCaptchaDisplayMode() {
+ CaptchaDisplayMode captchaDisplayMode = CaptchaDisplayMode.VISIBLE;
+ if (turnstileConfiguration == null) {
+ setTurnstileConfiguration();
+ }
+ if (turnstileConfiguration != null && CaptchaDisplayMode.INVISIBLE.getValue().equals(turnstileConfiguration.getWidgetType())) {
+ captchaDisplayMode = CaptchaDisplayMode.INVISIBLE;
+ }
+ return captchaDisplayMode.getValue();
+ }
+
+ @PostConstruct
+ @Override
+ public String getCaptchaSiteKey() {
+ if (turnstileConfiguration == null) {
+ setTurnstileConfiguration();
+ }
+ return this.captchaSiteKey;
+ }
+}
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Captcha.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Captcha.java
index 3fcc130f5a..baf2e33847 100644
--- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Captcha.java
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Captcha.java
@@ -30,15 +30,63 @@
@ConsumerType
public interface Captcha extends Field {
+ /**
+ * Defines the display mode for captcha.
+ * Possible values: {@code visible}, {@code invisible}
+ *
+ * @since com.adobe.cq.forms.core.components.models.form 5.10.0
+ */
+ enum CaptchaDisplayMode {
+ VISIBLE("visible"),
+ INVISIBLE("invisible");
+
+ private String displayMode;
+
+ CaptchaDisplayMode(String displayMode) {
+ this.displayMode = displayMode;
+ }
+
+ /**
+ * Returns the string value of this enum constant.
+ *
+ * @return the string value of this enum constant
+ * @since com.adobe.cq.forms.core.components.models.form 5.10.0
+ */
+ public String getValue() {
+ return displayMode;
+ }
+ }
+
@JsonIgnore
default String getCloudServicePath() {
return null;
}
@JsonIgnore
+ String getSize();
+
String getProvider();
@JsonIgnore
Map getCaptchaProperties() throws GuideException;
+ /**
+ * Returns the display mode of the captcha component.
+ *
+ * @return the string value of the one of the {@link CaptchaDisplayMode} enum
+ * @since com.adobe.cq.forms.core.components.models.form 5.10.0
+ */
+ default String getCaptchaDisplayMode() {
+ return null;
+ }
+
+ /**
+ * Returns the site key of the captcha component.
+ *
+ * @return the site key
+ * @since com.adobe.cq.forms.core.components.models.form 5.10.0
+ */
+ default String getCaptchaSiteKey() {
+ return null;
+ }
}
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Turnstile.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Turnstile.java
new file mode 100644
index 0000000000..10d55eeb51
--- /dev/null
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Turnstile.java
@@ -0,0 +1,28 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Copyright 2024 Adobe
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+
+package com.adobe.cq.forms.core.components.models.form;
+
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * Defines the form {@code Turnstile} Sling Model used for the {@code /apps/core/fd/components/form/turnstile/v1/turnstile}
+ * component.
+ *
+ * @since com.adobe.cq.forms.core.components.models.form 5.10.0
+ */
+@ConsumerType
+public interface Turnstile extends Captcha {}
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java
index e143716fa4..a659eb1155 100644
--- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java
@@ -35,7 +35,7 @@
*
*/
-@Version("5.9.6")
+@Version("5.10.0")
package com.adobe.cq.forms.core.components.models.form;
import org.osgi.annotation.versioning.Version;
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractCaptchaImpl.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractCaptchaImpl.java
index 0a34746ea6..4b82de8c63 100644
--- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractCaptchaImpl.java
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractCaptchaImpl.java
@@ -28,7 +28,17 @@
*/
public abstract class AbstractCaptchaImpl extends AbstractFieldImpl implements Captcha {
public static final String CUSTOM_RECAPTCHA_PROPERTY_WRAPPER = "fd:captcha";
+ protected static final String CAPTCHA_CONFIG = "config";
+ protected static final String CAPTCHA_SITE_KEY = "siteKey";
+ protected static final String CAPTCHA_URI = "uri";
+ protected static final String CAPTCHA_SIZE = "size";
+ protected static final String CAPTCHA_THEME = "theme";
+ protected static final String CAPTCHA_THEME_LIGHT = "light";
+ protected static final String CAPTCHA_TYPE = "type";
+ protected static final String CAPTCHA_TYPE_IMAGE = "image";
+ protected static final String CAPTCHA_WIDGET_TYPE = "widgetType";
+ @Override
@JsonIgnore
public abstract String getProvider();
@@ -37,6 +47,7 @@ public String getFieldType() {
return super.getFieldType(FieldType.CAPTCHA);
}
+ @JsonIgnore
public abstract Map getCaptchaProperties();
public Map getProperties() {
diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractCaptchaImplV2.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractCaptchaImplV2.java
new file mode 100644
index 0000000000..cc1e17c282
--- /dev/null
+++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractCaptchaImplV2.java
@@ -0,0 +1,73 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Copyright 2024 Adobe
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package com.adobe.cq.forms.core.components.util;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.PostConstruct;
+
+import com.adobe.cq.forms.core.components.models.form.Captcha;
+import com.adobe.cq.forms.core.components.models.form.FieldType;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * AbstractCaptchaImplV2 is an updated implementation for handling captcha field types.
+ *
+ * This class represents an evolution in the captcha JSON structure where captchaProvider
+ * is promoted to a top-level property, improving JSON clarity and eliminating redundancy.
+ *
+ * Background:
+ * Previous Implementation (AbstractCaptchaImpl):
+ * - Captcha provider information was embedded within the fd:captcha custom property
+ * - This led to redundant data and a less clean JSON structure with the updated forms spec
+ *
+ * Current Implementation (AbstractCaptchaImplV2):
+ * - CaptchaProvider is now a first-class citizen at the root level of the JSON
+ * - This change results in a cleaner and more efficient JSON structure
+ *
+ * Note: AbstractCaptchaImpl is not deprecated yet, as it is still used by
+ * recaptcha/hcaptcha v1 implementations in core components. Once these are migrated
+ * to AbstractCaptchaImplV2, the V1 implementation will be deprecated.
+ */
+public abstract class AbstractCaptchaImplV2 extends AbstractCaptchaImpl implements Captcha {
+
+ @Override
+ @JsonProperty("captchaProvider")
+ @JsonIgnore(false)
+ public abstract String getProvider();
+
+ @Override
+ public String getFieldType() {
+ return super.getFieldType(FieldType.CAPTCHA);
+ }
+
+ public abstract Map getCaptchaProperties();
+
+ @PostConstruct
+ public Map getProperties() {
+ Map properties = super.getProperties();
+ Map captchaConfig = new HashMap<>();
+ Map captchaProperties = getCaptchaProperties();
+ if (captchaProperties != null && captchaProperties.size() > 0) {
+ captchaConfig.put(CAPTCHA_CONFIG, captchaProperties);
+ }
+ properties.put(CUSTOM_RECAPTCHA_PROPERTY_WRAPPER, captchaConfig);
+ return properties;
+ }
+
+}
diff --git a/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/TurnstileImplTest.java b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/TurnstileImplTest.java
new file mode 100644
index 0000000000..d54786385b
--- /dev/null
+++ b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/TurnstileImplTest.java
@@ -0,0 +1,155 @@
+/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Copyright 2024 Adobe
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
+package com.adobe.cq.forms.core.components.internal.models.v1.form;
+
+import org.apache.sling.api.resource.Resource;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+
+import com.adobe.aemds.guide.model.HCaptchaConfiguration;
+import com.adobe.aemds.guide.model.ReCaptchaConfigurationModel;
+import com.adobe.aemds.guide.model.TurnstileConfiguration;
+import com.adobe.aemds.guide.service.CloudConfigurationProvider;
+import com.adobe.aemds.guide.service.GuideException;
+import com.adobe.cq.forms.core.Utils;
+import com.adobe.cq.forms.core.components.internal.form.FormConstants;
+import com.adobe.cq.forms.core.components.models.form.Captcha;
+import com.adobe.cq.forms.core.components.models.form.FieldType;
+import com.adobe.cq.forms.core.components.models.form.Turnstile;
+import com.adobe.cq.forms.core.context.FormsCoreComponentTestContext;
+import io.wcm.testing.mock.aem.junit5.AemContext;
+import io.wcm.testing.mock.aem.junit5.AemContextExtension;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@ExtendWith(AemContextExtension.class)
+public class TurnstileImplTest {
+ private static final String BASE = "/form/turnstile";
+ private static final String CONTENT_ROOT = "/content";
+ private static final String PATH_TURNSTILE = CONTENT_ROOT + "/turnstile";
+
+ private final AemContext context = FormsCoreComponentTestContext.newAemContext();
+
+ TurnstileConfiguration turnstileConfig = Mockito.mock(TurnstileConfiguration.class);
+
+ CloudConfigurationProvider cloudConfigurationProvider = new CloudConfigurationProvider() {
+ @Override
+ public ReCaptchaConfigurationModel getRecaptchaCloudConfiguration(Resource resource) throws GuideException {
+ return null;
+ }
+
+ @Override
+ public String getCustomFunctionUrl(Resource resource) {
+ return null;
+ }
+
+ @Override
+ public HCaptchaConfiguration getHCaptchaCloudConfiguration(Resource resource) throws GuideException {
+ return null;
+ }
+
+ @Override
+ public TurnstileConfiguration getTurnstileCloudConfiguration(Resource resource) throws GuideException {
+ return turnstileConfig;
+ }
+
+ };
+
+ @BeforeEach
+ void setUp() throws GuideException {
+ context.load().json(BASE + FormsCoreComponentTestContext.TEST_CONTENT_JSON, CONTENT_ROOT);
+ context.registerService(CloudConfigurationProvider.class, cloudConfigurationProvider);
+ }
+
+ @Test
+ void testExportedType() {
+ Captcha turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertEquals(FormConstants.RT_FD_FORM_TURNSTILE_V1, turnstile.getExportedType());
+ Turnstile turnstileMock = Mockito.mock(Turnstile.class);
+ Mockito.when(turnstileMock.getExportedType()).thenCallRealMethod();
+ assertEquals("", turnstileMock.getExportedType());
+ }
+
+ @Test
+ void testFieldType() {
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertEquals(FieldType.CAPTCHA.getValue(), turnstile.getFieldType());
+ }
+
+ @Test
+ void testGetName() {
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertEquals("turnstile1715230058257", turnstile.getName());
+ Turnstile turnstileMock = Mockito.mock(Turnstile.class);
+ Mockito.when(turnstileMock.getName()).thenCallRealMethod();
+ assertEquals(null, turnstileMock.getName());
+ }
+
+ @Test
+ void testGetTurnstileProvider() {
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertEquals("turnstile", turnstile.getProvider());
+ Turnstile turnstileMock = Mockito.mock(Turnstile.class);
+ Mockito.when(turnstileMock.getName()).thenCallRealMethod();
+ assertEquals(null, turnstileMock.getName());
+ }
+
+ @Test
+ void testGetConfigurationPath() {
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertEquals("managed", turnstile.getCloudServicePath());
+ Turnstile turnstileMock = Mockito.mock(Turnstile.class);
+ Mockito.when(turnstileMock.getName()).thenCallRealMethod();
+ assertEquals(null, turnstileMock.getName());
+ }
+
+ @Test
+ void testIsVisible() {
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertEquals(true, turnstile.isVisible());
+ Turnstile turnstileMock = Mockito.mock(Turnstile.class);
+ Mockito.when(turnstileMock.isVisible()).thenCallRealMethod();
+ assertEquals(null, turnstileMock.isVisible());
+ }
+
+ @Test
+ void testIsEnabled() {
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertEquals(true, turnstile.isEnabled());
+ Turnstile turnstileMock = Mockito.mock(Turnstile.class);
+ Mockito.when(turnstileMock.isEnabled()).thenCallRealMethod();
+ assertEquals(null, turnstileMock.isEnabled());
+ }
+
+ @Test
+ void testJSONExport() throws Exception {
+ Mockito.when(turnstileConfig.getSiteKey()).thenReturn("siteKey");
+ Mockito.when(turnstileConfig.getWidgetType()).thenReturn("invisible");
+ Mockito.when(turnstileConfig.getClientSideJsUrl()).thenReturn("https://challenges.cloudflare.com/turnstile/v0/api.js");
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ Utils.testJSONExport(turnstile, Utils.getTestExporterJSONPath(BASE, PATH_TURNSTILE));
+ }
+
+ @Test
+ void turnstileConfigExceptionTest() throws GuideException {
+ Mockito.when(turnstileConfig.getSiteKey()).thenThrow(new GuideException("Error while fetching site key"));
+ Turnstile turnstile = Utils.getComponentUnderTest(PATH_TURNSTILE, Turnstile.class, context);
+ assertNotNull(turnstile.getCaptchaProperties());
+ }
+}
diff --git a/bundles/af-core/src/test/resources/form/turnstile/exporter-turnstile.json b/bundles/af-core/src/test/resources/form/turnstile/exporter-turnstile.json
new file mode 100644
index 0000000000..1afda00aa6
--- /dev/null
+++ b/bundles/af-core/src/test/resources/form/turnstile/exporter-turnstile.json
@@ -0,0 +1,37 @@
+{
+ "id": "turnstile-b4c0808e68",
+ "fieldType": "captcha",
+ "name": "turnstile1715230058257",
+ "visible": true,
+ "type": "string",
+ "required": true,
+ "enabled": true,
+ "readOnly": false,
+ "properties": {
+ "fd:dor": {
+ "dorExclusion": false
+ },
+ "fd:path": "/content/turnstile",
+ "fd:captcha": {
+ "config": {
+ "uri": "https://challenges.cloudflare.com/turnstile/v0/api.js",
+ "widgetType": "invisible",
+ "size": "normal",
+ "theme": "light"
+ }
+ }
+ },
+ "captchaProvider": "turnstile",
+ "captchaDisplayMode": "invisible",
+ "captchaSiteKey": "siteKey",
+ "label": {
+ "visible": true,
+ "value": "TURNSTILE"
+ },
+ "events": {
+ "custom:setProperty": [
+ "$event.payload"
+ ]
+ },
+ ":type": "core/fd/components/form/turnstile/v1/turnstile"
+}
diff --git a/bundles/af-core/src/test/resources/form/turnstile/test-content.json b/bundles/af-core/src/test/resources/form/turnstile/test-content.json
new file mode 100644
index 0000000000..17aa78a94e
--- /dev/null
+++ b/bundles/af-core/src/test/resources/form/turnstile/test-content.json
@@ -0,0 +1,22 @@
+{
+ "turnstile": {
+ "jcr:primaryType": "nt:unstructured",
+ "jcr:createdBy": "admin",
+ "jcr:title": "TURNSTILE",
+ "enabled": true,
+ "jcr:lastModifiedBy": "admin",
+ "readOnly": false,
+ "required": true,
+ "jcr:created": "Thu May 09 2024 10:17:37 GMT+0530",
+ "name": "turnstile1715230058257",
+ "size": "normal",
+ "cloudServicePath": "managed",
+ "visible": true,
+ "hideTitle": "false",
+ "jcr:lastModified": "Thu May 09 2024 10:18:00 GMT+0530",
+ "sling:resourceType": "core/fd/components/form/turnstile/v1/turnstile",
+ "fieldType": "captcha",
+ "textIsRich": "true",
+ "unboundFormElement": false
+ }
+}
diff --git a/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/adaptive-form/turnstile/.content.xml b/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/adaptive-form/turnstile/.content.xml
new file mode 100644
index 0000000000..333978ce8e
--- /dev/null
+++ b/examples/ui.content/src/main/content/jcr_root/content/core-components-examples/library/adaptive-form/turnstile/.content.xml
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml
index c862537b53..1b446738e2 100644
--- a/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml
+++ b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/clientlibs/custom-forms-components-runtime-all/.content.xml
@@ -5,4 +5,4 @@
cssProcessor="[default:none,min:none]"
jsProcessor="[default:none,min:none]"
categories="[core.forms.components.it.runtime.all]"
- embed="[core.forms.components.runtime.base,core.forms.components.it.container.v1.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v2.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.switch.v1.runtime,core.forms.components.termsandconditions.v1.runtime, core.forms.components.it.textinput.v1.runtime, core.forms.components.hcaptcha.v1.runtime]"/>
+ embed="[core.forms.components.runtime.base,core.forms.components.it.container.v1.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v2.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.switch.v1.runtime,core.forms.components.termsandconditions.v1.runtime, core.forms.components.it.textinput.v1.runtime, core.forms.components.hcaptcha.v1.runtime, core.forms.components.turnstile.v1.runtime]"/>
diff --git a/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/form/turnstile/.content.xml b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/form/turnstile/.content.xml
new file mode 100644
index 0000000000..3431ffc5e5
--- /dev/null
+++ b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/form/turnstile/.content.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/form/turnstile/_cq_template.xml b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/form/turnstile/_cq_template.xml
new file mode 100644
index 0000000000..c55ac31fef
--- /dev/null
+++ b/it/apps/src/main/content/jcr_root/apps/forms-core-components-it/form/turnstile/_cq_template.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entcheckbox/.content.xml b/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entcheckbox/.content.xml
index 3e2b39c5c2..05aa120ee5 100644
--- a/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entcheckbox/.content.xml
+++ b/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entcheckbox/.content.xml
@@ -12,7 +12,7 @@
sling:resourceType="fd/af/cloudservices/recaptcha/page"
enterpriseVerifyUrl="https://recaptchaenterprise.googleapis.com/"
keyType="checkbox"
- name="entCheckbox"
+ name="entcheckbox"
projectId="aem-forms-internal"
secretKey="\{ee8e90ea55c4fbc7675fb8e9ffd7cea858e5536f0789198aabc40a3295f278f59f30e4325aa5ad93124380c962198c4fbf6fe3f86ac979fa64da7d99f47b24fb}"
siteKey="6LfaMOkpAAAAAK9ooBLOcnDO84EE7UTTgBKA6e3d"
diff --git a/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entscore/.content.xml b/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entscore/.content.xml
index cdc833c4cf..f7a36e04b0 100644
--- a/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entscore/.content.xml
+++ b/it/content/src/main/content/jcr_root/conf/core-components-it/samples/recaptcha/basic/settings/cloudconfigs/recaptcha/entscore/.content.xml
@@ -12,7 +12,7 @@
sling:resourceType="fd/af/cloudservices/recaptcha/page"
enterpriseVerifyUrl="https://recaptchaenterprise.googleapis.com/"
keyType="score"
- name="entScore"
+ name="entscore"
projectId="aem-forms-internal"
secretKey="\{ee8e90ea55c4fbc7675fb8e9ffd7cea858e5536f0789198aabc40a3295f278f59f30e4325aa5ad93124380c962198c4fbf6fe3f86ac979fa64da7d99f47b24fb}"
siteKey="6LfKIukpAAAAAFabx1W7ve1hWDKXBD92oHtGb9j6"
diff --git a/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/.content.xml b/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/.content.xml
new file mode 100644
index 0000000000..a0ac99e384
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/.content.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/invisible/.content.xml b/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/invisible/.content.xml
new file mode 100644
index 0000000000..53c61ce32a
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/invisible/.content.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/managed/.content.xml b/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/managed/.content.xml
new file mode 100644
index 0000000000..14e72741af
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/conf/core-components-it/settings/cloudconfigs/turnstile/managed/.content.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/.content.xml b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/.content.xml
new file mode 100644
index 0000000000..ea2e55bfa7
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/.content.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/basic/.content.xml b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/basic/.content.xml
new file mode 100644
index 0000000000..f87b064c12
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/basic/.content.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/invisible/.content.xml b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/invisible/.content.xml
new file mode 100644
index 0000000000..8af952bdaf
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/invisible/.content.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/managed/.content.xml b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/managed/.content.xml
new file mode 100644
index 0000000000..5b6b4741b8
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/dam/formsanddocuments/core-components-it/samples/turnstile/managed/.content.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/.content.xml b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/.content.xml
new file mode 100644
index 0000000000..3edadd6c63
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/.content.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/basic/.content.xml b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/basic/.content.xml
new file mode 100644
index 0000000000..8999bca137
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/basic/.content.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/invisible/.content.xml b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/invisible/.content.xml
new file mode 100644
index 0000000000..6318dc1f19
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/invisible/.content.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/managed/.content.xml b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/managed/.content.xml
new file mode 100644
index 0000000000..1196a59e27
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/turnstile/managed/.content.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/forms/sites/core-components-it/site-with-captcha-inline-form/.content.xml b/it/content/src/main/content/jcr_root/content/forms/sites/core-components-it/site-with-captcha-inline-form/.content.xml
new file mode 100644
index 0000000000..a225e5b6d8
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/forms/sites/core-components-it/site-with-captcha-inline-form/.content.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/it/content/src/main/content/jcr_root/content/forms/sites/core-components-it/site-with-turnstile-afv2-form/.content.xml b/it/content/src/main/content/jcr_root/content/forms/sites/core-components-it/site-with-turnstile-afv2-form/.content.xml
new file mode 100644
index 0000000000..b4205fa962
--- /dev/null
+++ b/it/content/src/main/content/jcr_root/content/forms/sites/core-components-it/site-with-turnstile-afv2-form/.content.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml
index 4c0c770f99..0ef50cf14f 100644
--- a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/af-clientlibs/core-forms-components-runtime-all/.content.xml
@@ -5,4 +5,4 @@
cssProcessor="[default:none,min:none]"
jsProcessor="[default:none,min:none]"
categories="[core.forms.components.runtime.all]"
- embed="[core.forms.components.runtime.base,core.forms.components.container.v2.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v3.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.switch.v1.runtime,core.forms.components.termsandconditions.v1.runtime, core.forms.components.hcaptcha.v1.runtime,core.forms.components.review.v1.runtime]"/>
+ embed="[core.forms.components.runtime.base,core.forms.components.container.v2.runtime,core.forms.components.datePicker.v1.runtime,core.forms.components.textinput.v1.runtime,core.forms.components.numberinput.v1.runtime,core.forms.components.panelcontainer.v1.runtime,core.forms.components.radiobutton.v1.runtime,core.forms.components.text.v1.runtime,core.forms.components.checkboxgroup.v1.runtime,core.forms.components.button.v1.runtime,core.forms.components.image.v1.runtime,core.forms.components.dropdown.v1.runtime,core.forms.components.fileinput.v3.runtime,core.forms.components.accordion.v1.runtime,core.forms.components.tabs.v1.runtime,core.forms.components.wizard.v1.runtime,core.forms.components.verticaltabs.v1.runtime,core.forms.components.recaptcha.v1.runtime,core.forms.components.checkbox.v1.runtime,core.forms.components.fragment.v1.runtime,core.forms.components.switch.v1.runtime,core.forms.components.termsandconditions.v1.runtime, core.forms.components.hcaptcha.v1.runtime,core.forms.components.review.v1.runtime, core.forms.components.turnstile.v1.runtime]"/>
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/.content.xml
new file mode 100644
index 0000000000..491392d539
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/.content.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/.content.xml
new file mode 100644
index 0000000000..5e25fbe65e
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/.content.xml
@@ -0,0 +1,4 @@
+
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/.content.xml
new file mode 100644
index 0000000000..9af5bb2a03
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/.content.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/README.md b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/README.md
new file mode 100644
index 0000000000..e5fe2ab00f
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/README.md
@@ -0,0 +1,71 @@
+
+Adaptive Form Turnstile (v1)
+====
+Adaptive Form Cloudflare Turnstile field component written in HTL.
+
+## Features
+
+* Provides the following type of input:
+ * Turnstile
+* The following widgets are supported for Turnstile:
+ * Managed
+ * Non-Interactive
+ * Invisible
+* Styles
+* Custom constraint messages for the above types
+
+### Use Object
+The Form Text component uses the `com.adobe.cq.forms.core.components.models.form.Turnstile` Sling Model for its Use-object.
+
+### Edit Dialog Properties
+The following properties are written to JCR for this Form Recaptcha component and are expected to be available as `Resource` properties:
+
+1. `./jcr:title` - defines the label to use for this field
+2. `./hideTitle` - if set to `true`, the label of this field will be hidden
+3. `./name` - defines the name of the field, which will be submitted with the form data
+4. `./mandatoryMessage` - defines the message displayed as tooltip when submitting the form if the value is left empty
+5. `./cloudServicePath` - defines the path of cloud configuration resource for Turnstile
+6. `./size` - defines the size attribute of Turnstile
+
+## Client Libraries
+The component provides a `core.forms.components.turnstile.v1.runtime` client library category that contains the Javascript runtime for the component.
+It should be added to a relevant site client library using the `embed` property.
+
+
+## BEM Description
+```
+BLOCK cmp-adaptiveform-turnstile
+ ELEMENT cmp-adaptiveform-turnstile__label
+ ELEMENT cmp-adaptiveform-turnstile__widget
+ ELEMENT cmp-adaptiveform-turnstile__errormessage
+```
+
+## JavaScript Data Attribute Bindings
+
+The following attributes must be added for the initialization of the hCaptcha component in the form view:
+1. `data-cmp-is="adaptiveFormTurnstile"`
+2. `data-cmp-adaptiveformcontainer-path="${formstructparser.formContainerPath}"`
+
+
+
+The following are optional attributes that can be added to the component in the form view:
+1. `data-cmp-valid` having a boolean value to indicate whether the field is currently valid or not
+2. `data-cmp-required` having a boolean value to indicate whether the field is currently required or not
+3. `data-cmp-readonly` having a boolean value to indicate whether the field is currently readonly or not
+4. `data-cmp-active` having a boolean value to indicate whether the field is currently active or not
+5. `data-cmp-visible` having a boolean value to indicate whether the field is currently visible or not
+6. `data-cmp-enabled` having a boolean value to indicate whether the field is currently enabled or not
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/_cq_dialog/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/_cq_dialog/.content.xml
new file mode 100644
index 0000000000..5f51f816fa
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/_cq_dialog/.content.xml
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/_cq_template.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/_cq_template.xml
new file mode 100644
index 0000000000..6e03b7d2d3
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/_cq_template.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/.content.xml
new file mode 100644
index 0000000000..491392d539
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/.content.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/.content.xml
new file mode 100644
index 0000000000..9370b2d52c
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/.content.xml
@@ -0,0 +1,5 @@
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/js.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/js.txt
new file mode 100644
index 0000000000..dd642eaed7
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/js.txt
@@ -0,0 +1,18 @@
+###############################################################################
+# Copyright 2022 Adobe
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+###############################################################################
+
+#base=js
+editDialog.js
\ No newline at end of file
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/js/editDialog.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/js/editDialog.js
new file mode 100644
index 0000000000..8c2817de3d
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/editor/js/editDialog.js
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright 2024 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+(function($) {
+ "use strict";
+
+ var EDIT_DIALOG = ".cmp-adaptiveform-turnstile__editdialog",
+ TURNSTILE_CONFIG = EDIT_DIALOG + " .cmp-adaptiveform-turnstile__configuration",
+ TURNSTILE_SIZE = EDIT_DIALOG + " .cmp-adaptiveform-turnstile__size",
+ Utils = window.CQ.FormsCoreComponents.Utils.v1;
+
+
+ function addListenerForCaptchaConfigChange(dialog) {
+ const turnstileConfigElement = dialog.find(TURNSTILE_CONFIG)[0];
+ turnstileConfigElement.addEventListener("change", function() {
+ handleCaptchaConfigChange(turnstileConfigElement, dialog);
+ });
+ handleCaptchaConfigChange(turnstileConfigElement, dialog);
+ }
+
+ function handleCaptchaConfigChange(turnstileConfigElement, dialog) {
+ const turnstileSizeElement = dialog.find(TURNSTILE_SIZE)[0];
+ const selectedConfig = turnstileConfigElement.querySelector("coral-select-item[selected]");
+ const selectedWidgetType = selectedConfig.getAttribute("data-widget.type");
+ const inputs = turnstileSizeElement.querySelectorAll('input');
+ if (selectedWidgetType === "invisible") {
+ inputs.forEach(input => input.setAttribute("disabled", true));
+ } else {
+ inputs.forEach(input => input.removeAttribute("disabled"));
+ }
+ }
+
+ Utils.initializeEditDialog(EDIT_DIALOG)(addListenerForCaptchaConfigChange);
+
+})(jQuery);
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/.content.xml b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/.content.xml
new file mode 100644
index 0000000000..0ace1e4531
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/.content.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/css.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/css.txt
new file mode 100644
index 0000000000..b2a208dac5
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/css.txt
@@ -0,0 +1,18 @@
+###############################################################################
+# Copyright 2024 Adobe
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+###############################################################################
+
+#base=css
+turnstileview.css
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/css/turnstileview.css b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/css/turnstileview.css
new file mode 100644
index 0000000000..941298eba2
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/css/turnstileview.css
@@ -0,0 +1,19 @@
+/**
+## BEM Description
+BLOCK cmp-adaptiveform-turnstile
+ ELEMENT cmp-adaptiveform-turnstile__label
+ ELEMENT cmp-adaptiveform-turnstile__widget
+ ELEMENT cmp-adaptiveform-turnstile__errormessage
+*/
+
+.cmp-adaptiveform-turnstile {
+
+}
+
+.cmp-adaptiveform-turnstile__widget {
+
+}
+
+.cmp-adaptiveform-turnstile__label {
+
+}
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js.txt b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js.txt
new file mode 100644
index 0000000000..fff80d07dd
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js.txt
@@ -0,0 +1,19 @@
+###############################################################################
+# Copyright 2024 Adobe
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+###############################################################################
+
+#base=js
+turnstileview.js
+turnstilewidget.js
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js/turnstileview.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js/turnstileview.js
new file mode 100644
index 0000000000..87d65a0d1c
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js/turnstileview.js
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ * Copyright 2024 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+(function() {
+
+ "use strict";
+ class Turnstile extends FormView.FormFieldBase {
+
+ static NS = FormView.Constants.NS;
+ static IS = "adaptiveFormTurnstile";
+ static bemBlock = 'cmp-adaptiveform-turnstile';
+ static selectors = {
+ self: "[data-" + this.NS + '-is="' + this.IS + '"]',
+ widget: `.${Turnstile.bemBlock}__widget`,
+ label: `.${Turnstile.bemBlock}__label`,
+ errorDiv: `.${Turnstile.bemBlock}__errormessage`
+ };
+
+ constructor(params) {
+ super(params);
+ }
+
+ getWidget() {
+ return this.element.querySelector(Turnstile.selectors.widget);
+ }
+
+ getDescription() {
+ return null;
+ }
+
+ getLabel() {
+ return this.element.querySelector(Turnstile.selectors.label);
+ }
+
+ getTooltipDiv() {
+ return null;
+ }
+
+ getErrorDiv() {
+ return this.element.querySelector(Turnstile.selectors.errorDiv);
+ }
+
+ getQuestionMarkDiv() {
+ return null;
+ }
+
+ initializeWidget() {
+ this.widgetObject = new TurnstileWidget(this, this._model, this.getWidget());
+ this.getWidget().addEventListener('blur', (e) => {
+ if(this.element) {
+ this.setInactive();
+ }
+ });
+
+ }
+
+ setModel(model) {
+ super.setModel(model);
+ if (this.widgetObject == null) {
+ this.initializeWidget();
+ } else {
+ if (this.widget.value !== '') {
+ this._model.dispatch(new FormView.Actions.UIChange({'value': this.widget.value}));
+ }
+ }
+ }
+ }
+
+ FormView.Utils.setupField(({element, formContainer}) => {
+ return new Turnstile({element, formContainer})
+ }, Turnstile.selectors.self);
+
+})();
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js/turnstilewidget.js b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js/turnstilewidget.js
new file mode 100644
index 0000000000..d9ccf2ce0e
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/clientlibs/site/js/turnstilewidget.js
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright 2024 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+
+/**
+ * This class is responsible for interacting with the turnstile widget. It displays turnstile challenge.
+ */
+
+if (typeof window.TurnstileWidget === 'undefined') {
+ window.TurnstileWidget = class {
+ #widget = null
+ #model = null // passed by reference
+ #options = null
+ #lang = 'en'
+ static FD_CAPTCHA = "fd:captcha";
+
+ constructor(view, model, widget) {
+ // initialize the widget and model
+ this.#widget = widget;
+ this.#model = model;
+ this.#widget = document.createElement("div");
+ this.#widget.classList.add("cmp-adaptiveform-turnstile__widget");
+ this.#lang = view.formContainer.getModel().lang;
+
+ //Always inserting it in body
+ document.body.appendChild(this.#widget);
+ this.#options = Object.assign({}, this.#model._jsonModel);
+
+ this.#renderTurnstile(widget);
+ }
+
+ #renderTurnstile(turnstileContainer) {
+
+ const self = this;
+ const turnstileConfigData = this.#options;
+ let widgetId;
+ const url = turnstileConfigData.properties[TurnstileWidget.FD_CAPTCHA].config.uri;
+
+ const successCallback = function(response) {
+ self.setCaptchaModel(response);
+ };
+
+ const expiredCallback = function() {
+ turnstile.reset(widgetId);
+ self.setCaptchaModel("");
+ };
+
+ const onloadCallbackInternal = function() {
+ widgetId = turnstile.render(
+ turnstileContainer,
+ turnstileParameters
+ );
+ return widgetId;
+ };
+
+ const turnstileParameters = {
+ 'sitekey': this.#model.captchaSiteKey,
+ 'size': turnstileConfigData.properties[TurnstileWidget.FD_CAPTCHA].config.size,
+ 'theme': turnstileConfigData.properties[TurnstileWidget.FD_CAPTCHA].config.theme || 'light',
+ 'callback': successCallback,
+ 'expired-callback': expiredCallback,
+ 'language': this.#lang
+ };
+
+ window.onloadTurnstileCallback = onloadCallbackInternal;
+ const scr = document.createElement('script');
+ const queryParams = (this.#model.captchaDisplayMode === 'invisible')
+ ? "?render=explicit"
+ : "?onload=onloadTurnstileCallback&render=explicit";
+ scr.src = url + queryParams;
+ scr.async = true;
+ turnstileContainer.appendChild(scr);
+ }
+
+ setCaptchaModel(response) {
+ this.#model.dispatch(new FormView.Actions.UIChange({'value': response}));
+ }
+ }
+}
diff --git a/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/turnstile.html b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/turnstile.html
new file mode 100644
index 0000000000..af12e42f7e
--- /dev/null
+++ b/ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/turnstile/v1/turnstile/turnstile.html
@@ -0,0 +1,42 @@
+
+
+
+
diff --git a/ui.frontend/package-lock.json b/ui.frontend/package-lock.json
index 225f96ffc5..bfe27a044c 100644
--- a/ui.frontend/package-lock.json
+++ b/ui.frontend/package-lock.json
@@ -9,8 +9,8 @@
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
- "@aemforms/af-core": "^0.22.111",
- "@aemforms/af-custom-functions": "1.0.12",
+ "@aemforms/af-core": "^0.22.112",
+ "@aemforms/af-custom-functions": "1.0.13",
"@aemforms/af-formatters": "^0.22.109"
},
"devDependencies": {
@@ -26,6 +26,52 @@
"webpack-merge": "^5.8.0"
}
},
+ "../../../repos/af-custom-functions": {
+ "name": "@aemforms/af-custom-functions",
+ "version": "1.0.12",
+ "extraneous": true,
+ "license": "MIT License, Copyright 2024 Adobe Systems Incorporated",
+ "devDependencies": {
+ "jest": "^29.7.0",
+ "jest-environment-jsdom": "^29.7.0"
+ }
+ },
+ "../../../repos/af2-web-runtime/packages/forms-next-core": {
+ "name": "@aemforms/af-core",
+ "version": "0.22.111",
+ "extraneous": true,
+ "license": "Adobe Proprietary",
+ "dependencies": {
+ "@adobe/json-formula": "0.1.50",
+ "@aemforms/af-formatters": "^0.22.111"
+ },
+ "devDependencies": {
+ "@babel/preset-env": "^7.20.2",
+ "@types/jest": "29.2.4",
+ "@types/lodash": "^4.14.171",
+ "@typescript-eslint/eslint-plugin": "^4.28.2",
+ "@typescript-eslint/parser": "^4.28.2",
+ "babel-jest": "^29.4.1",
+ "blob-polyfill": "^7.0.20220408",
+ "eslint": "^7.30.0",
+ "eslint-config-standard": "^16.0.3",
+ "eslint-plugin-import": "^2.23.4",
+ "eslint-plugin-jest": "^24.3.6",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-promise": "^5.1.0",
+ "form-data": "^4.0.0",
+ "jest": "29.3",
+ "jest-environment-jsdom": "^29.3.1",
+ "jest-junit": "^12.2.0",
+ "nock": "^13.1.3",
+ "node-fetch": "^2.6.1",
+ "parse-multipart-data": "^1.5.0",
+ "ts-jest": "29.0",
+ "typedoc": "0.22.11",
+ "typedoc-plugin-markdown": "3.11.13",
+ "typescript": "^4.3.5"
+ }
+ },
"../../af2-web-runtime/packages/forms-next-formatters": {
"name": "@aemforms/af-formatters",
"version": "0.22.75",
@@ -61,23 +107,23 @@
}
},
"node_modules/@aemforms/af-core": {
- "version": "0.22.111",
- "resolved": "https://registry.npmjs.org/@aemforms/af-core/-/af-core-0.22.111.tgz",
- "integrity": "sha512-vJIA2R2pbrKt+c29c9u61HsK4tiCr+8/YWw4GpROoDXUBg1/1gAinGCOd2A3IVP+8eaO3uiiZWCO5YBVGIDjEg==",
+ "version": "0.22.112",
+ "resolved": "https://registry.npmjs.org/@aemforms/af-core/-/af-core-0.22.112.tgz",
+ "integrity": "sha512-cyUbPYu/l+w9elyTLEWhyQj2qLp/PlI2fXDX/2kpTcGfb+ckRqXRzpItbPqa66m4scjcNpK0RpwfThcSRb1S3w==",
"dependencies": {
"@adobe/json-formula": "0.1.50",
- "@aemforms/af-formatters": "^0.22.111"
+ "@aemforms/af-formatters": "^0.22.112"
}
},
"node_modules/@aemforms/af-custom-functions": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.12.tgz",
- "integrity": "sha512-O8ADuD8+6hOMj2EJ3Zaq3hCsmzhTTC9acgQtFvHMYOnII0dyKx1WOgNwDIQ++/CapVqLSmzkTyDvy4iLPtsrLA=="
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.13.tgz",
+ "integrity": "sha512-thtSn92qpkZg0uldYMaiN81MGxyttv+kyO2h34YA1Li8V79DYFp3JJWE3duhCgo+tslCLGamqryVwJbMdSJV6Q=="
},
"node_modules/@aemforms/af-formatters": {
- "version": "0.22.111",
- "resolved": "https://registry.npmjs.org/@aemforms/af-formatters/-/af-formatters-0.22.111.tgz",
- "integrity": "sha512-q4kdTbTpHKYo7m954SbXbz1y4Fg8GLqu6AS5yOf2ec+EXxmxhFdqzGn9kzrV13i9YJuG/ukdQcd0ek22I9+N3A=="
+ "version": "0.22.112",
+ "resolved": "https://registry.npmjs.org/@aemforms/af-formatters/-/af-formatters-0.22.112.tgz",
+ "integrity": "sha512-hnscUtTMxfEWNcBiatYY5lzGCCtRn1lbCdKemPTqMMRIzBCWFJXx1mxV84ysyktXXW7R+PS64P0A2K8IBVtVFw=="
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
@@ -11076,23 +11122,23 @@
"integrity": "sha512-dmlLYfbty8NPVIdxvI9cJ+ZdXsrRCFrCdmL1+aR2auEzXJ86rD0bm1qu+S4NOpFiZLKIyx0zvUTykms40vNjsA=="
},
"@aemforms/af-core": {
- "version": "0.22.111",
- "resolved": "https://registry.npmjs.org/@aemforms/af-core/-/af-core-0.22.111.tgz",
- "integrity": "sha512-vJIA2R2pbrKt+c29c9u61HsK4tiCr+8/YWw4GpROoDXUBg1/1gAinGCOd2A3IVP+8eaO3uiiZWCO5YBVGIDjEg==",
+ "version": "0.22.112",
+ "resolved": "https://registry.npmjs.org/@aemforms/af-core/-/af-core-0.22.112.tgz",
+ "integrity": "sha512-cyUbPYu/l+w9elyTLEWhyQj2qLp/PlI2fXDX/2kpTcGfb+ckRqXRzpItbPqa66m4scjcNpK0RpwfThcSRb1S3w==",
"requires": {
"@adobe/json-formula": "0.1.50",
- "@aemforms/af-formatters": "^0.22.111"
+ "@aemforms/af-formatters": "^0.22.112"
}
},
"@aemforms/af-custom-functions": {
- "version": "1.0.12",
- "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.12.tgz",
- "integrity": "sha512-O8ADuD8+6hOMj2EJ3Zaq3hCsmzhTTC9acgQtFvHMYOnII0dyKx1WOgNwDIQ++/CapVqLSmzkTyDvy4iLPtsrLA=="
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@aemforms/af-custom-functions/-/af-custom-functions-1.0.13.tgz",
+ "integrity": "sha512-thtSn92qpkZg0uldYMaiN81MGxyttv+kyO2h34YA1Li8V79DYFp3JJWE3duhCgo+tslCLGamqryVwJbMdSJV6Q=="
},
"@aemforms/af-formatters": {
- "version": "0.22.111",
- "resolved": "https://registry.npmjs.org/@aemforms/af-formatters/-/af-formatters-0.22.111.tgz",
- "integrity": "sha512-q4kdTbTpHKYo7m954SbXbz1y4Fg8GLqu6AS5yOf2ec+EXxmxhFdqzGn9kzrV13i9YJuG/ukdQcd0ek22I9+N3A=="
+ "version": "0.22.112",
+ "resolved": "https://registry.npmjs.org/@aemforms/af-formatters/-/af-formatters-0.22.112.tgz",
+ "integrity": "sha512-hnscUtTMxfEWNcBiatYY5lzGCCtRn1lbCdKemPTqMMRIzBCWFJXx1mxV84ysyktXXW7R+PS64P0A2K8IBVtVFw=="
},
"@ampproject/remapping": {
"version": "2.2.1",
diff --git a/ui.frontend/package.json b/ui.frontend/package.json
index c2f8ad33c6..dd5ed20f1e 100644
--- a/ui.frontend/package.json
+++ b/ui.frontend/package.json
@@ -23,8 +23,8 @@
"webpack-merge": "^5.8.0"
},
"dependencies": {
- "@aemforms/af-core": "^0.22.111",
+ "@aemforms/af-core": "^0.22.112",
"@aemforms/af-formatters": "^0.22.109",
- "@aemforms/af-custom-functions": "1.0.12"
+ "@aemforms/af-custom-functions": "1.0.13"
}
}
diff --git a/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js b/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js
index 6b4b92eb72..0d4422af2e 100644
--- a/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js
+++ b/ui.tests/test-module/specs/recaptcha/recaptcha.authoring.cy.js
@@ -64,7 +64,7 @@ describe('Page - Authoring', function () {
// Checking some dynamic behaviours
cy.get(".cmp-adaptiveform-recaptcha__configuration").click().then(() => {
- cy.get("coral-selectlist-item[value='entScore']").click();
+ cy.get("coral-selectlist-item[value='entscore']").click();
cy.get("input[name='./recaptchaSize'][value='normal']").should("be.disabled");
cy.get("input[name='./recaptchaSize'][value='compact']").should("be.disabled");
})
diff --git a/ui.tests/test-module/specs/sites/captchaEmbed.runtime.cy.js b/ui.tests/test-module/specs/sites/captchaEmbed.runtime.cy.js
index f5f511aacc..11d46e4986 100644
--- a/ui.tests/test-module/specs/sites/captchaEmbed.runtime.cy.js
+++ b/ui.tests/test-module/specs/sites/captchaEmbed.runtime.cy.js
@@ -16,6 +16,7 @@
describe("Captcha In Sites Runtime Test", () => {
const pagePath = "content/forms/sites/core-components-it/site-with-captcha-afv2-form.html";
const hCaptchaPagePath = "content/forms/sites/core-components-it/site-with-hcaptcha-afv2-form.html";
+ const turnstilePagePath = "content/forms/sites/core-components-it/site-with-turnstile-afv2-form.html";
const FT_HCAPTCHA = "FT_FORMS-12407";
let formContainer = null;
@@ -89,4 +90,20 @@ describe("Captcha In Sites Runtime Test", () => {
})
}
})
+
+ it("turnstile should render when form is embedded in site", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_HCAPTCHA)) {
+ cy.previewForm(turnstilePagePath).then(p => {
+ formContainer = p;
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ expect(formContainer._model.items.length, "model and view elements match").to.equal(Object.keys(formContainer._fields).length);
+ cy.get('.cmp-adaptiveform-turnstile__widget').should('exist').then($iframe => {
+ cy.wrap($iframe).then($iframe => {
+ cy.window().should('have.property', 'turnstile').and('not.be.undefined');
+ });
+ });
+ })
+ }
+ })
+
})
diff --git a/ui.tests/test-module/specs/sites/inlineFormInSites.runtime.cy.js b/ui.tests/test-module/specs/sites/inlineFormInSites.runtime.cy.js
new file mode 100644
index 0000000000..4fd7906efd
--- /dev/null
+++ b/ui.tests/test-module/specs/sites/inlineFormInSites.runtime.cy.js
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright 2024 Adobe
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ ******************************************************************************/
+import 'cypress-wait-until';
+
+describe("Captcha In Sites Runtime Test", () => {
+ const siteWithRecaptchaScore = "content/forms/sites/core-components-it/site-with-captcha-inline-form.html";
+
+ let formContainer = null;
+
+ function updateEnterpriseConfig(score) {
+ const secretKey = Cypress.env('RECAPTCHA_ENT_API_KEY');
+ cy.openPage("/mnt/overlay/fd/af/cloudservices/recaptcha/properties.html?item=%2Fconf%2Fcore-components-it%2Fsamples%2Frecaptcha%2Fbasic%2Fsettings%2Fcloudconfigs%2Frecaptcha%2Fentscore").then(x => {
+ cy.get('#recaptcha-cloudconfiguration-secret-key').clear().type(secretKey);
+ cy.get('#recaptcha-cloudconfiguration-threshold-score').clear().type(score);
+ cy.get("#shell-propertiespage-doneactivator").click();
+ })
+ }
+
+ it("submission should pass for enterprise score based captcha",() => {
+ if (cy.af.isLatestAddon()) {
+ updateEnterpriseConfig(0.5);
+ cy.previewForm(siteWithRecaptchaScore).then((p) => {
+ formContainer = p;
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ cy.get(`div.grecaptcha-badge`).should('exist').then(() => {
+ cy.intercept('POST', /\/adobe\/forms\/af\/submit\/.*/).as('submitForm');
+ const submitForm = () => {
+ cy.get(`.cmp-adaptiveform-button__widget`).click();
+
+ // Wait for the submitForm request
+ return cy.wait('@submitForm',{ timeout: 50000 }).then((interception) => {
+ if (interception.response.statusCode === 200) {
+ // Request succeeded
+ cy.log('Submit request succeeded');
+ return cy.wrap(true);
+ } else {
+ // Request failed
+ cy.log(`Submit request failed, retrying...`);
+ return cy.wrap(false);
+ }
+ });
+ };
+ // Need to submit multiple times until the form is submitted successfully
+ // Due to below error while validating recaptcha enterprise
+ // https://cloud.google.com/recaptcha-enterprise/docs/faq#returned_browser_error_when_creating_an_assessment_what_should_i_do_about_this
+ cy.waitUntil(() => submitForm(), {
+ errorMsg: 'Maximum retry limit reached, request did not succeed',
+ timeout: 50000, // Total timeout (10 seconds)
+ interval: 5000, // Interval between retries (1 second)
+ });
+ });
+ });
+ }
+ })
+
+})
diff --git a/ui.tests/test-module/specs/turnstile/turnstile.authoring.cy.js b/ui.tests/test-module/specs/turnstile/turnstile.authoring.cy.js
new file mode 100644
index 0000000000..86cc5bb3e4
--- /dev/null
+++ b/ui.tests/test-module/specs/turnstile/turnstile.authoring.cy.js
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2024 Adobe Systems Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const sitesSelectors = require('../../libs/commons/sitesSelectors'),
+ afConstants = require('../../libs/commons/formsConstants');
+
+/**
+ * Testing Turnstile with Forms and Sites Editor
+ */
+describe('Page - Authoring', function () {
+
+ const FT_TURNSTILE = "FT_FORMS-12407";
+ const formturnstile = "/apps/forms-core-components-it/form/turnstile";
+ let toggle_array = [];
+
+ before(() => {
+ cy.fetchFeatureToggles().then((response) => {
+ if (response.status === 200) {
+ toggle_array = response.body.enabled;
+ }
+ });
+ });
+
+ // we can use these values to log in
+
+ const dropTurnstileInContainer = function () {
+ const dataPath = "/content/forms/af/core-components-it/samples/turnstile/basic/jcr:content/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Cloudflare® Turnstile", formturnstile);
+ cy.get('body').click(0, 0);
+ }
+
+ const dropTurnstileInSites = function () {
+ const dataPath = "/content/core-components-examples/library/adaptive-form/turnstile/jcr:content/root/responsivegrid/demo/component/guideContainer/*",
+ responsiveGridDropZoneSelector = sitesSelectors.overlays.overlay.component + "[data-path='" + dataPath + "']";
+ cy.selectLayer("Edit");
+ cy.insertComponent(responsiveGridDropZoneSelector, "Adaptive Form Cloudflare® Turnstile", formturnstile);
+ cy.get('body').click(0, 0);
+ }
+
+ const testTurnstileBehaviour = function (turnstileEditPathSelector, turnstileDrop, isSites) {
+ if (isSites) {
+ dropTurnstileInSites();
+ } else {
+ dropTurnstileInContainer();
+ }
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + turnstileEditPathSelector);
+ cy.invokeEditableAction("[data-action='CONFIGURE']"); // this line is causing frame busting which is causing cypress to fail
+ // Check If Dialog Options Are Visible
+ cy.get("[name='./cloudServicePath']")
+ .should("exist");
+ cy.get("[name='./size']")
+ .should("exist");
+ cy.get("[name='./name']")
+ .should("exist");
+
+ // Switching to invisible widget type key should disable size options
+ cy.get(".cmp-adaptiveform-turnstile__configuration").click().then(() => {
+ cy.get("coral-selectlist-item[value='invisible']").click();
+ cy.get("input[name='./size'][value='normal']").should("be.disabled");
+ cy.get("input[name='./size'][value='compact']").should("be.disabled");
+ })
+ cy.get('.cq-dialog-submit').click();
+ cy.reload();
+ // If invisible widget type key is selected, size options should be disabled
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + turnstileEditPathSelector);
+ cy.invokeEditableAction("[data-action='CONFIGURE']");
+ cy.get("input[name='./size'][value='normal']").should("be.disabled");
+ cy.get("input[name='./size'][value='compact']").should("be.disabled");
+ // Switching to managed/non-interactive widget type key should enable size options
+ cy.get(".cmp-adaptiveform-turnstile__configuration").click().then(() => {
+ cy.get("coral-selectlist-item[value='managed']").click();
+ cy.get("input[name='./size'][value='normal']").should("be.enabled");
+ cy.get("input[name='./size'][value='compact']").should("be.enabled");
+ })
+ cy.get('.cq-dialog-submit').click();
+ cy.reload();
+ // If managed/non-interactive widget type key is selected, size options should be enabled
+ cy.openEditableToolbar(sitesSelectors.overlays.overlay.component + turnstileEditPathSelector);
+ cy.invokeEditableAction("[data-action='CONFIGURE']");
+ cy.get("input[name='./size'][value='normal']").should("be.enabled");
+ cy.get("input[name='./size'][value='compact']").should("be.enabled");
+ cy.get('.cq-dialog-cancel').click();
+
+ cy.deleteComponentByPath(turnstileDrop);
+ }
+
+ context('Open Forms Editor', function() {
+ const pagePath = "/content/forms/af/core-components-it/samples/turnstile/basic",
+ turnstileEditPath = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/turnstile",
+ turnstileEditPathSelector = "[data-path='" + turnstileEditPath + "']",
+ turnstileDrop = pagePath + afConstants.FORM_EDITOR_FORM_CONTAINER_SUFFIX + "/" + formturnstile.split("/").pop();
+ beforeEach(function () {
+ if (cy.af.isLatestAddon()) {
+ // this is done since cypress session results in 403 sometimes
+ cy.openAuthoring(pagePath);
+ }
+ });
+
+ it('insert turnstile in form container', function () {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ dropTurnstileInContainer();
+ cy.deleteComponentByPath(turnstileDrop);
+ }
+ });
+
+ it ('open edit dialog of turnstile',{ retries: 3 }, function(){
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ cy.cleanTest(turnstileDrop).then(function(){
+ testTurnstileBehaviour(turnstileEditPathSelector, turnstileDrop);
+ });
+ }
+ })
+ })
+
+ context('Open Sites Editor', function () {
+ const pagePath = "/content/core-components-examples/library/adaptive-form/turnstile",
+ turnstileEditPath = pagePath + afConstants.RESPONSIVE_GRID_DEMO_SUFFIX + "/guideContainer/turnstile",
+ turnstileEditPathSelector = "[data-path='" + turnstileEditPath + "']",
+ turnstileDrop = pagePath + afConstants.RESPONSIVE_GRID_DEMO_SUFFIX + '/guideContainer/' + formturnstile.split("/").pop();
+
+ beforeEach(function () {
+ if (cy.af.isLatestAddon()) {
+ // this is done since cypress session results in 403 sometimes
+ cy.openAuthoring(pagePath);
+ }
+ });
+
+ it('insert aem forms turnstile', function () {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ dropTurnstileInSites();
+ cy.deleteComponentByPath(turnstileDrop);
+ }
+ });
+
+ it('open edit dialog of aem forms turnstile', function() {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ testTurnstileBehaviour(turnstileEditPathSelector, turnstileDrop, true);
+ }
+ });
+ });
+
+});
diff --git a/ui.tests/test-module/specs/turnstile/turnstile.runtime.cy.js b/ui.tests/test-module/specs/turnstile/turnstile.runtime.cy.js
new file mode 100644
index 0000000000..3d6a79638a
--- /dev/null
+++ b/ui.tests/test-module/specs/turnstile/turnstile.runtime.cy.js
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2024 Adobe Systems Incorporated
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+describe("Form Runtime with Turnstile Input", () => {
+
+ const FT_TURNSTILE = "FT_FORMS-12407";
+ const pagePath = "content/forms/af/core-components-it/samples/turnstile/managed.html"
+ const invisiblePagePath = "content/forms/af/core-components-it/samples/turnstile/invisible.html"
+ const bemBlock = 'cmp-adaptiveform-turnstile'
+ // The secret keys are part of public documentation and are not sensitive :
+ // https://developers.cloudflare.com/turnstile/troubleshooting/testing/
+ const alwaysPassSecretKey = "1x0000000000000000000000000000000AA";
+ const alwaysFailSecretKey = "2x0000000000000000000000000000000AA";
+ let formContainer = null
+
+ let toggle_array = [];
+
+ before(() => {
+ cy.fetchFeatureToggles().then((response) => {
+ if (response.status === 200) {
+ toggle_array = response.body.enabled;
+ }
+ });
+ });
+
+ function updateTurnstileSecretKey(secretKey, configName) {
+ cy.openPage(`mnt/overlay/fd/af/cloudservices/turnstile/properties.html?item=%2Fconf%2Fcore-components-it%2Fsettings%2Fcloudconfigs%2Fturnstile%2F${configName}`).then(x => {
+ cy.get('#captcha-cloudconfiguration-secret-key').clear().type(secretKey).then(x => {
+ cy.get("#shell-propertiespage-doneactivator").click();
+ });
+ });
+ }
+
+ // render the form with captcha, we have whitelisted the "Missing required parameters: sitekey" error
+ beforeEach(() => {
+ if (cy.af.isLatestAddon()) {
+ cy.previewForm(pagePath).then((p) => {
+ formContainer = p;
+ });
+ }
+ });
+
+ const checkHTML = (id, state) => {
+ const visible = state.visible;
+ const passVisibleCheck = `${visible === true ? "" : "not."}be.visible`;
+ const passDisabledAttributeCheck = `${state.enabled === false || state.readOnly === true ? "" : "not."}have.attr`;
+ const value = state.value
+ cy.get(`#${id}`)
+ .should(passVisibleCheck)
+ .invoke('attr', 'data-cmp-visible')
+ .should('eq', visible.toString());
+ cy.get(`#${id}`)
+ .invoke('attr', 'data-cmp-enabled')
+ .should('eq', state.enabled.toString());
+ return cy.get(`#${id}`).within((root) => {
+ cy.get('*').should(passVisibleCheck)
+ })
+ }
+
+ it(" should get model and view initialized properly ", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ expect(formContainer._model.items.length, "model and view elements match").to.equal(Object.keys(formContainer._fields).length);
+ Object.entries(formContainer._fields).forEach(([id, field]) => {
+ expect(field.getId()).to.equal(id)
+ expect(formContainer._model.getElement(id), `model and view are in sync`).to.equal(field.getModel())
+ });
+ }
+ })
+
+ it(" model's changes are reflected in the html ", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ const [id, fieldView] = Object.entries(formContainer._fields)[0]
+ const model = formContainer._model.getElement(id)
+ cy.get('.cmp-adaptiveform-turnstile__widget').should('exist');
+
+ checkHTML(model.id, model.getState()).then(() => {
+ model.visible = false
+ return checkHTML(model.id, model.getState())
+ }).then(() => {
+ model.enable = false
+ return checkHTML(model.id, model.getState())
+ })
+ }
+ });
+
+ it(" html changes are reflected in model ", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ const [id, fieldView] = Object.entries(formContainer._fields)[0]
+ const model = formContainer._model.getElement(id)
+ cy.log(model.getState().value)
+ cy.get(`#${id}`).click().then(x => {
+ cy.log(model.getState().value)
+ expect(model.getState().value).to.not.be.null
+ })
+ }
+ });
+
+
+ it("decoration element should not have same class name", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ cy.wrap().then(() => {
+ const id = formContainer._model._children[0].id;
+ cy.get(`#${id}`).parent().should("not.have.class", bemBlock);
+ })
+ }
+ })
+
+ it("client side validation should fail if captcha is not filled", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ cy.get(`.cmp-adaptiveform-button__widget`).click().then(x => {
+ cy.get('.cmp-adaptiveform-turnstile__errormessage').should('exist').contains('Please fill in this field.');
+ });
+ }
+ })
+
+ it("submission should pass for mandatory captcha", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ updateTurnstileSecretKey(alwaysPassSecretKey, "managed");
+ cy.previewForm(pagePath).then((p) => {
+ formContainer = p;
+ });
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ cy.get('.cmp-adaptiveform-turnstile__widget').should('be.visible').then($iframe => {
+ cy.wrap($iframe).then($iframe => {
+ cy.window().should('have.property', 'turnstile').and('not.be.undefined')
+ .then((turnstile) => {
+ turnstile.execute();
+ return new Cypress.Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 3000);
+ });
+ }).then(() => {
+ cy.get(`.cmp-adaptiveform-button__widget`).click().then(x => {
+ cy.get('body').should('contain', "Thank you for submitting the form.\n")
+ });
+ })
+ });
+ });
+ }
+ })
+
+ it("submission should fail for mandatory captcha", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ updateTurnstileSecretKey(alwaysFailSecretKey, "managed");
+ cy.previewForm(pagePath).then((p) => {
+ formContainer = p;
+ });
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ cy.get('.cmp-adaptiveform-turnstile__widget').should('be.visible').then($iframe => {
+ cy.wrap($iframe).then($iframe => {
+ cy.window().should('have.property', 'turnstile').and('not.be.undefined')
+ .then((hcaptcha) => {
+ hcaptcha.execute();
+ return new Cypress.Promise(resolve => {
+ setTimeout(() => {
+ resolve();
+ }, 3000);
+ });
+ }).then(() => {
+ cy.on('window:alert', (message) => {
+ expect(message).to.equal('Encountered an internal error while submitting the form.');
+ });
+ cy.intercept('POST', /\/adobe\/forms\/af\/submit\/.*/).as('submitForm');
+ cy.get(`.cmp-adaptiveform-button__widget`).click();
+ cy.wait('@submitForm').then((interception) => {
+ expect(interception.response.statusCode).to.equal(400);
+ expect(interception.response.body).to.have.property('title', 'The CAPTCHA validation failed. Please try again.');
+ expect(interception.response.body).to.have.property('detail', 'com.adobe.aem.forms.af.rest.exception.CaptchaValidationException: Captcha validation failed.');
+ });
+ })
+ });
+ });
+ }
+ })
+
+ it("submission should pass for mandatory invisible captcha", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ updateTurnstileSecretKey(alwaysPassSecretKey, "invisible");
+ cy.previewForm(invisiblePagePath).then((p) => {
+ formContainer = p;
+ });
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ cy.get(`.cmp-adaptiveform-turnstile__widget`).should('exist').then(() => {
+ cy.intercept('POST', /\/adobe\/forms\/af\/submit\/.*/, (req) => {
+ req.reply((res) => {
+ expect(res.statusCode).to.equal(200);
+ });
+ });
+ cy.get(`.cmp-adaptiveform-button__widget`).click();
+ });
+ }
+ });
+
+ it("submission should return 400 if invisible captcha validation fails", () => {
+ if (cy.af.isLatestAddon() && toggle_array.includes(FT_TURNSTILE)) {
+ updateTurnstileSecretKey(alwaysFailSecretKey, "invisible");
+ cy.previewForm(invisiblePagePath).then((p) => {
+ formContainer = p;
+ });
+ expect(formContainer, "formcontainer is initialized").to.not.be.null;
+ cy.get(`.cmp-adaptiveform-turnstile__widget`).should('exist').then(() => {
+ cy.intercept('POST', /\/adobe\/forms\/af\/submit\/.*/, (req) => {
+ req.reply((res) => {
+ expect(res.statusCode).to.equal(400);
+ });
+ });
+ cy.get(`.cmp-adaptiveform-button__widget`).click();
+ });
+ }
+ });
+
+})