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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,12 @@ private FeatureToggleConstants() {}
* {@code ToggleMonitorSystemPropertyFactory} or for direct -D JVM override.
*/
public static final String FT_ALLOW_MULTIPLE_FIELDS_IN_WHEN = "FT_FORMS-12053";
/**
* When enabled, the Server-Side Validation (SSV) cloud configuration dropdown is shown in the
* form container dialog, allowing authors to select a validator endpoint to be called before
* form submission. When disabled, the SSV option is hidden and no server-side validation occurs.
* <p>
* System property: same name ({@code FT_FORMS-25252}); set to {@code "true"} to enable.
*/
public static final String FT_SERVER_SIDE_VALIDATION = "FT_FORMS-25252";
}
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ private ReservedProperties() {
public static final String PN_SUBMIT_AEP_DATASET_ID = "datasetId";
public static final String PN_SUBMIT_AEP_SANDBOX_NAME = "sandboxName";

public static final String PN_ENABLE_SERVER_SIDE_VALIDATION = "fd:enableServerSideValidation";
public static final String PN_SSV_CLOUD_SERVICE_PATH = "fd:ssvCloudServicePath";
// End: Form submission related properties
private static final Set<String> reservedProperties = aggregateReservedProperties();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,10 @@ private Map<String, Object> getSubmitProperties() {
submitProps.computeIfAbsent(SS_AEP, k -> new LinkedHashMap<String, Object>());
((Map<String, Object>) submitProps.get(SS_AEP)).put(entry.getKey(), entry.getValue());
}
// SSV properties (enableServerSideValidation, ssvCloudServicePath) are intentionally

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: comment not required here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

// excluded from the form JSON. The submit servlet reads them directly from JCR.
// ssvCloudServicePath in particular must not be sent to clients as it exposes
// internal JCR paths.
}
}
return submitProps;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ public enum FormMetaDataType {
SUBMIT_ACTION("submitAction"),
PREFILL_ACTION("prefillServiceProvider"),
LANG("lang"),
FORMATTERS("formatters");
FORMATTERS("formatters"),
SSV_CLOUD_CONFIG("ssvCloudServiceConfiguration");

private String value;

Expand Down Expand Up @@ -174,6 +175,9 @@ private Boolean isLangPolicy(FormMetaDataType type, Map.Entry<String, Object> en
private List<Resource> getDataSourceResources(SlingHttpServletRequest request, ResourceResolver resourceResolver, FormMetaDataType type,
String dataModel, Config config) {
List<Resource> resources = new ArrayList<>();
if (type == FormMetaDataType.SSV_CLOUD_CONFIG) {
return getCloudConfigsByGroup(request, resourceResolver);
}
FormMetaData formMetaData = resourceResolver.adaptTo(FormMetaData.class);
if (formMetaData != null) {
Iterator<FormsManager.ComponentDescription> metaDataList = null;
Expand Down Expand Up @@ -223,6 +227,64 @@ private List<Resource> getDataSourceResources(SlingHttpServletRequest request, R
resources.add(getResourceForDropdownDisplay(resourceResolver, i18n.get("None"), ""));
resources.addAll(this.getResourceListFromComponentDescription(metaDataList, resourceResolver));
break;
default:
break;
}
}
return resources;
}

private List<Resource> getCloudConfigsByGroup(SlingHttpServletRequest request, ResourceResolver resourceResolver) {
List<Resource> resources = new ArrayList<>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect, we have helpers in cq-guides (addon) for such use-cases, you should re-use that

resources.add(getResourceForDropdownDisplay(resourceResolver, "None", ""));
String contentPath = (String) request.getAttribute(Value.CONTENTPATH_ATTRIBUTE);
Resource formResource = null;
if (StringUtils.isNotBlank(contentPath)) {
formResource = resourceResolver.getResource(contentPath);
}
if (formResource == null) {
formResource = request.getRequestPathInfo().getSuffixResource();
}
String resolvedConfPath = null;
if (formResource != null) {
Resource r = formResource;
while (r != null) {
String confPath = r.getValueMap().get("cq:conf", String.class);
if (StringUtils.isNotBlank(confPath)) {
resolvedConfPath = confPath;
break;
}
// cq:conf lives on jcr:content for cq:Page and folder nodes
Resource pageContent = r.getChild(JcrConstants.JCR_CONTENT);
if (pageContent != null) {
confPath = pageContent.getValueMap().get("cq:conf", String.class);
if (StringUtils.isNotBlank(confPath)) {
resolvedConfPath = confPath;
break;
}
}
r = r.getParent();
}
}
// Fall back to /conf/global when no cq:conf is found in the hierarchy —
// this mirrors AEM's own context-aware configuration resolution behaviour.
if (resolvedConfPath == null) {
resolvedConfPath = "/conf/global";
}
// Query for any jcr:content node under cloudconfigs/ that carries serviceEndPoint,
// regardless of nesting depth (handles both flat and service-type-subfolder layouts).
String cloudConfigsBase = resolvedConfPath + "/settings/cloudconfigs";
String query = "SELECT * FROM [nt:base] AS s WHERE ISDESCENDANTNODE(s, '"
+ cloudConfigsBase + "') AND s.[serviceEndPoint] IS NOT NULL";
Iterator<Resource> results = resourceResolver.findResources(query, "JCR-SQL2");
while (results.hasNext()) {
Resource configContent = results.next();
// configContent is the node with serviceEndPoint — its parent is the config root node
Resource configNode = configContent.getParent();
if (configNode != null) {
String title = configContent.getValueMap().get("jcr:title",
configNode.getValueMap().get("jcr:title", configNode.getName()));
resources.add(getResourceForDropdownDisplay(resourceResolver, title, configNode.getPath()));
}
}
return resources;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public class FormContainerImplTest {
private static final String PATH_FORM_WITHOUT_FIELDTYPE = CONTENT_ROOT + "/formcontainerv2-without-fieldtype";
private static final String PATH_FORM_WITH_AUTO_SAVE = CONTENT_ROOT + "/formcontainerv2WithAutoSave";
private static final String PATH_FORM_WITH_CHANGE_EVENT = CONTENT_ROOT + "/formcontainerv2ChangeEventBehaviour";
private static final String PATH_FORM_WITH_SSV = CONTENT_ROOT + "/formcontainerv2-with-ssv";
private static final String PATH_FORM_SSV_DISABLED = CONTENT_ROOT + "/formcontainerv2-ssv-disabled";
private static final String PATH_FORM_1_WITHOUT_REDIRECT = CONTENT_ROOT + "/formcontainerv2WithoutRedirect";
private static final String CONTENT_FORM_WITHOUT_PREFILL_ROOT = "/content/forms/af/formWithoutPrefill";
private static final String PATH_FORM_WITHOUT_PREFILL = CONTENT_FORM_WITHOUT_PREFILL_ROOT + "/formcontainerv2WithoutPrefill";
Expand Down Expand Up @@ -954,4 +956,51 @@ void testSetLang() throws Exception {
formContainer.setLang(null);
assertEquals(formContainer.getLang(), "en");
}

// ─── SSV submit-properties tests ──────────────────────────────────────────

@SuppressWarnings("unchecked")
@Test
void testSsvPropertiesNotExposedInFormJson() throws Exception {
// SSV config (enableServerSideValidation, ssvCloudServicePath) must never appear in
// the exported form JSON. The submit servlet reads them directly from JCR. Exposing
// ssvCloudServicePath to clients leaks an internal JCR path.
context.request().setAttribute(FormConstants.X_ADOBE_FORM_DEFINITION, FormConstants.FORM_DEFINITION_SUBMISSION);
FormContainerImpl formContainer = Utils.getComponentUnderTest(PATH_FORM_WITH_SSV, FormContainerImpl.class, context);

Map<String, Object> props = formContainer.getProperties();
assertNotNull("fd:submit must be present in submission view", props.get(ReservedProperties.FD_SUBMIT_PROPERTIES));

Map<String, Object> submit = (Map<String, Object>) props.get(ReservedProperties.FD_SUBMIT_PROPERTIES);
assertNull("serverSideValidation block must NOT be in the form JSON to avoid leaking JCR paths",
submit.get("serverSideValidation"));
assertFalse("enableServerSideValidation must not be a top-level submit property",
submit.containsKey(ReservedProperties.PN_ENABLE_SERVER_SIDE_VALIDATION));
assertFalse("ssvCloudServicePath must not be a top-level submit property",
submit.containsKey(ReservedProperties.PN_SSV_CLOUD_SERVICE_PATH));
}

@SuppressWarnings("unchecked")
@Test
void testSubmitPropertiesHasNoSsvBlockWhenDisabled() throws Exception {
context.request().setAttribute(FormConstants.X_ADOBE_FORM_DEFINITION, FormConstants.FORM_DEFINITION_SUBMISSION);
FormContainerImpl formContainer = Utils.getComponentUnderTest(PATH_FORM_SSV_DISABLED, FormContainerImpl.class, context);

Map<String, Object> props = formContainer.getProperties();
assertNotNull("fd:submit must be present in submission view", props.get(ReservedProperties.FD_SUBMIT_PROPERTIES));

Map<String, Object> submit = (Map<String, Object>) props.get(ReservedProperties.FD_SUBMIT_PROPERTIES);
assertNull("serverSideValidation block must be absent when SSV is not configured", submit.get("serverSideValidation"));
}

@SuppressWarnings("unchecked")
@Test
void testSubmitPropertiesAbsentWithoutSubmissionViewHeader() throws Exception {
// Without the submission view request attribute, fd:submit must not be exported
FormContainer formContainer = Utils.getComponentUnderTest(PATH_FORM_WITH_SSV, FormContainer.class, context);

Map<String, Object> props = formContainer.getProperties();
assertNull("fd:submit must NOT be present in regular (non-submission-view) rendering",
props.get(ReservedProperties.FD_SUBMIT_PROPERTIES));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,22 @@
"customProp": "customPropValue"
}
},
"formcontainerv2-with-ssv": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "core/fd/components/form/container/v2/container",
"fieldType": "form",
"title": "SSV Form",
"fd:enableServerSideValidation": true,
"fd:ssvCloudServicePath": "/conf/global/settings/cloudconfigs/ssv-config",
"actionName": "rest"
},
"formcontainerv2-ssv-disabled": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "core/fd/components/form/container/v2/container",
"fieldType": "form",
"title": "No-SSV Form",
"actionName": "rest"
},
"printfragment": {
"jcr:primaryType": "nt:unstructured",
"jcr:title": "AF Fragment (v2)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"FT_FORMS-13519",
"FT_FORMS-17107",
"FT_FORMS-24087",
"FT_FORMS-24343"
"FT_FORMS-24343",
"FT_FORMS-25252"
],
"disabledToggles": [
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,21 @@
</cui>
</uiSettings>
</thankYouMessage>
<ssvCloudServicePath
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Server Side Validation"
fieldDescription="Choose a validation configuration to apply server-side validation during form submission."
name="./fd:ssvCloudServicePath">
<datasource
jcr:primaryType="nt:unstructured"
sling:resourceType="core/fd/components/form/container/v1/datasource"
type="ssvCloudServiceConfiguration"/>
<granite:rendercondition
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/renderconditions/featuretoggle"
toggleName="FT_FORMS-25252"/>
</ssvCloudServicePath>
<submitActionType
jcr:primaryType="nt:unstructured"
granite:class="cmp-adaptiveform-container__submitaction"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
let defaultSubmissionError = FormView.LanguageUtils.getTranslatedString(self.getLang(), "InternalFormSubmissionError");
const globals = {
form: self.getModel().getRuleNode(),
formModel: self.getModel(),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this required ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its not required. Removed it.

event: {
type: action.type,
payload: action.payload,
Expand Down
Loading