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 @@ + +