This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-5823-javatemplates-dynamic-attributes
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 528a502864bf80589a3869719a89e1825fbc9617
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]>
---
 .../java/org/apache/struts2/components/UIBean.java |   1 +
 .../java/simple/DynamicAttributesHandler.java      |  13 ++-
 .../java/simple/DynamicAttributesHandlerTest.java  | 122 +++++++++++++++++++++
 3 files changed, 130 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 06d7f3f29..c2a909849 100644
--- a/core/src/main/java/org/apache/struts2/components/UIBean.java
+++ b/core/src/main/java/org/apache/struts2/components/UIBean.java
@@ -711,6 +711,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..69b34671f
--- /dev/null
+++ 
b/plugins/javatemplates/src/test/java/org/apache/struts2/views/java/simple/DynamicAttributesHandlerTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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 
&quot;b&quot; &lt;c&gt;' data-hint='a &quot;b&quot; &lt;c&gt;'></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 
&quot;b&quot;'></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()));
+        textField.setScopedConstantProvider(name -> null);
+        radio = new Radio(stack, request, response);
+        radio.setCspNonceReader(new 
StrutsCspNonceReader(CspNonceSource.SESSION.name()));
+        radio.setScopedConstantProvider(name -> null);
+    }
+
+    @Override
+    protected UIBean getUIBean() {
+        return textField;
+    }
+
+    @Override
+    protected String getTagName() {
+        return "textfield";
+    }
+}

Reply via email to