This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch support/struts-6-x-x
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/support/struts-6-x-x by this
push:
new 9fbaf3bb4 WW-5823 fix(javatemplates): escape dynamic attributes and
evaluate them only for lazy components
9fbaf3bb4 is described below
commit 9fbaf3bb497686050f9bab7a011e698b7b32dbfb
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 31 07:31:55 2026 +0100
WW-5823 fix(javatemplates): escape dynamic attributes and evaluate them
only for lazy components
DynamicAttributesHandler stored dynamic attribute values with a raw put
while every other handler and the FreeMarker dynamic-attributes.ftl
escape them, and it re-evaluated values that UIBean.setDynamicAttributes
had already translated. Values now go through Attributes.add, and the
handler evaluates an expression only when the component reports lazy
evaluation, which UIBean exposes as the lazyEvaluation parameter. Radio
and CheckboxList keep their per-item evaluation.
Co-Authored-By: Claude Mythos 5.1 <[email protected]>
(cherry picked from commit cdb638d53061956b4f6b7887713dfcc336bf4dcb)
---
.../java/org/apache/struts2/components/UIBean.java | 1 +
.../java/simple/DynamicAttributesHandler.java | 13 +--
.../java/simple/DynamicAttributesHandlerTest.java | 120 +++++++++++++++++++++
3 files changed, 128 insertions(+), 6 deletions(-)
diff --git a/core/src/main/java/org/apache/struts2/components/UIBean.java
b/core/src/main/java/org/apache/struts2/components/UIBean.java
index 710e395c3..9b77afe24 100644
--- a/core/src/main/java/org/apache/struts2/components/UIBean.java
+++ b/core/src/main/java/org/apache/struts2/components/UIBean.java
@@ -673,6 +673,7 @@ public abstract class UIBean extends Component {
addParameter(THEME, gotTheme);
addParameter("template", template != null ? findString(template) :
getDefaultTemplate());
addParameter("dynamicAttributes", dynamicAttributes);
+ addParameter("lazyEvaluation", lazyEvaluation());
addParameter("themeExpansionToken", uiThemeExpansionToken);
addParameter("expandTheme", uiThemeExpansionToken + gotTheme);
diff --git
a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DynamicAttributesHandler.java
b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DynamicAttributesHandler.java
index 150b4126f..746781973 100644
---
a/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DynamicAttributesHandler.java
+++
b/plugins/javatemplates/src/main/java/org/apache/struts2/views/java/simple/DynamicAttributesHandler.java
@@ -40,14 +40,15 @@ public class DynamicAttributesHandler extends
AbstractTagHandler {
}
protected void processDynamicAttributes(Attributes a) {
- Map<String, String> dynamicAttributes = (Map<String, String>)
context.getParameters().get("dynamicAttributes");
+ Map<String, Object> params = context.getParameters();
+ Map<String, String> dynamicAttributes = (Map<String, String>)
params.get("dynamicAttributes");
+ boolean lazyEvaluation =
Boolean.TRUE.equals(params.get("lazyEvaluation"));
for (Map.Entry<String, String> entry : dynamicAttributes.entrySet()) {
- if (ComponentUtils.isExpression(entry.getValue())) {
- String value =
ObjectUtils.defaultIfNull(findString(entry.getValue()), entry.getValue());
- a.put(entry.getKey(), value);
- } else {
- a.put(entry.getKey(), entry.getValue());
+ String value = entry.getValue();
+ if (lazyEvaluation && ComponentUtils.isExpression(value)) {
+ value = ObjectUtils.defaultIfNull(findString(value), value);
}
+ a.add(entry.getKey(), value);
}
}
diff --git
a/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DynamicAttributesHandlerTest.java
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DynamicAttributesHandlerTest.java
new file mode 100644
index 000000000..bf123b312
--- /dev/null
+++
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DynamicAttributesHandlerTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.struts2.views.java.simple;
+
+import org.apache.struts2.components.Radio;
+import org.apache.struts2.components.TextField;
+import org.apache.struts2.components.UIBean;
+import org.apache.struts2.interceptor.csp.CspNonceSource;
+import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.easymock.EasyMock.expect;
+
+public class DynamicAttributesHandlerTest extends AbstractTest {
+
+ private TextField textField;
+ private Radio radio;
+
+ public void testResolvedValueIsHtmlEscaped() throws Exception {
+ textField.setName("q");
+ textField.setTitle("%{quoted}");
+ textField.setDynamicAttributes(dynamic("data-hint", "%{quoted}"));
+
+ String output = render(textField, "textfield");
+
+ assertEquals(s("<input type='text' name='q' id='q' title='a
"b" <c>' data-hint='a "b" <c>'></input>"),
output);
+ }
+
+ public void testLiteralValueIsHtmlEscaped() throws Exception {
+ textField.setName("q");
+ textField.setDynamicAttributes(dynamic("data-hint", "a \"b\""));
+
+ String output = render(textField, "textfield");
+
+ assertEquals(s("<input type='text' name='q' id='q' data-hint='a
"b"'></input>"), output);
+ }
+
+ public void testResolvedValueInExpressionFormIsNotEvaluatedAgain() throws
Exception {
+ textField.setName("q");
+ textField.setDynamicAttributes(dynamic("data-hint",
"%{expressionLike}"));
+
+ String output = render(textField, "textfield");
+
+ assertEquals(s("<input type='text' name='q' id='q'
data-hint='%{1+1}'></input>"), output);
+ }
+
+ public void testLazyComponentStillEvaluatesPerItem() throws Exception {
+ radio.setName("r");
+ radio.setId("r");
+ radio.setList(Arrays.asList("one", "two"));
+ radio.setDynamicAttributes(dynamic("data-item", "%{top}"));
+
+ String output = render(radio, "radiomap");
+
+ assertEquals(s("<input type='radio' name='r' value='x' id='r0'
data-item='x'></input>"
+ + "<label for='r0' data-item='x'>x</label>"
+ + "<input type='radio' name='r' value='x' id='r1'
data-item='x'></input>"
+ + "<label for='r1' data-item='x'>x</label>"), output);
+ }
+
+ private String render(UIBean bean, String tagName) throws Exception {
+ bean.evaluateParams();
+ map.putAll(bean.getAttributes());
+ theme.renderTag(tagName, context);
+ return writer.getBuffer().toString();
+ }
+
+ private static Map<String, String> dynamic(String name, String value) {
+ Map<String, String> dynamic = new LinkedHashMap<>();
+ dynamic.put(name, value);
+ return dynamic;
+ }
+
+ @Override
+ protected void setUpStack() {
+ expect(stack.findValue("quoted", String.class)).andReturn("a \"b\"
<c>").anyTimes();
+ expect(stack.findValue("expressionLike",
String.class)).andReturn("%{1+1}").anyTimes();
+ expect(stack.findValue("1+1", String.class)).andReturn("2").anyTimes();
+ expect(stack.findValue("top", String.class)).andReturn("x").anyTimes();
+ expect(stack.findValue("top")).andReturn("x").anyTimes();
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ textField = new TextField(stack, request, response);
+ textField.setCspNonceReader(new
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+ radio = new Radio(stack, request, response);
+ radio.setCspNonceReader(new
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+ }
+
+ @Override
+ protected UIBean getUIBean() {
+ return textField;
+ }
+
+ @Override
+ protected String getTagName() {
+ return "textfield";
+ }
+}