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

lukaszlenart pushed a commit to branch WW-5659-lazy-params-request-scoping
in repository https://gitbox.apache.org/repos/asf/struts.git

commit ac4676db5daf7f453f993331b90dcc3e8c88846b
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Jul 27 11:59:41 2026 +0200

    WW-5659 chore(core): harden the policy sets and tidy the lazy params 
dispatch
    
    UploadPolicy handed out the mutable HashSet built by 
commaDelimitedStringToSet,
    which the copy constructor shares by reference with the configured policy, 
so a
    subclass overriding the protected acceptFile could have rewritten 
process-wide
    config from a request thread. The sets are unmodifiable now.
    
    Also document that unresolved() records `disabled` like any other param, so 
an
    unresolvable disabled expression leaves the interceptor enabled and rejects
    every upload; and in DefaultActionInvocation use normal imports for the 
params
    types, make the interceptor local final, word the three skip logs 
consistently
    and identify the interceptor by its mapping name, and note that the 
name-based
    param merge is inherited behaviour whose duplicate-ref handling is 
questionable.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 .../apache/struts2/DefaultActionInvocation.java    | 38 +++++++++++++++++-----
 .../apache/struts2/interceptor/UploadPolicy.java   | 14 +++++++-
 2 files changed, 43 insertions(+), 9 deletions(-)

diff --git a/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java 
b/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java
index 913590410..72a35afe5 100644
--- a/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java
+++ b/core/src/main/java/org/apache/struts2/DefaultActionInvocation.java
@@ -30,7 +30,9 @@ import org.apache.struts2.config.entities.ResultConfig;
 import org.apache.struts2.inject.Container;
 import org.apache.struts2.inject.Inject;
 import org.apache.struts2.interceptor.ConditionalInterceptor;
+import org.apache.struts2.interceptor.DisableParams;
 import org.apache.struts2.interceptor.Interceptor;
+import org.apache.struts2.interceptor.InterceptorParams;
 import org.apache.struts2.interceptor.PreResultListener;
 import org.apache.struts2.interceptor.WithLazyParams;
 import org.apache.struts2.ognl.OgnlUtil;
@@ -258,11 +260,11 @@ public class DefaultActionInvocation implements 
ActionInvocation {
         if (asyncManager == null || !asyncManager.hasAsyncActionResult()) {
             if (interceptors.hasNext()) {
                 final InterceptorMapping interceptorMapping = 
interceptors.next();
-                Interceptor interceptor = interceptorMapping.getInterceptor();
+                final Interceptor interceptor = 
interceptorMapping.getInterceptor();
                 if (interceptor instanceof WithLazyParams<?> lazyInterceptor) {
                     resultCode = invokeWithLazyParams(lazyInterceptor, 
interceptorMapping);
                 } else if (interceptor instanceof ConditionalInterceptor 
conditionalInterceptor) {
-                    resultCode = executeConditional(conditionalInterceptor);
+                    resultCode = executeConditional(conditionalInterceptor, 
interceptorMapping.getName());
                 } else {
                     LOG.debug("Executing normal interceptor: {}", 
interceptorMapping.getName());
                     resultCode = interceptor.intercept(this);
@@ -313,18 +315,18 @@ public class DefaultActionInvocation implements 
ActionInvocation {
      * lazily resolved {@code disabled} flag and any custom {@code 
shouldIntercept} must be honoured
      * here, because the single-argument {@code intercept} is not the entry 
point on this path.
      */
-    private <P extends org.apache.struts2.interceptor.InterceptorParams> 
String invokeWithLazyParams(
+    private <P extends InterceptorParams> String invokeWithLazyParams(
             WithLazyParams<P> lazyInterceptor, InterceptorMapping 
interceptorMapping) throws Exception {
         P lazyParams = lazyParamInjector.resolveInto(
                 lazyInterceptor.newLazyParams(), 
mergedParams(interceptorMapping), invocationContext);
 
-        if (lazyParams instanceof org.apache.struts2.interceptor.DisableParams 
disableParams && disableParams.isDisabled()) {
-            LOG.debug("Interceptor: {} is disabled for this invocation, 
skipping to next", interceptorMapping.getName());
+        if (lazyParams instanceof DisableParams disableParams && 
disableParams.isDisabled()) {
+            LOG.debug("Interceptor: {} is disabled by its lazily resolved 
params, skipping to next", interceptorMapping.getName());
             return this.invoke();
         }
         if (lazyInterceptor instanceof ConditionalInterceptor 
conditionalInterceptor
                 && !conditionalInterceptor.shouldIntercept(this)) {
-            LOG.debug("Interceptor: {} is disabled, skipping to next", 
interceptorMapping.getName());
+            LOG.debug("Interceptor: {} declined by shouldIntercept() on the 
lazy params path, skipping to next", interceptorMapping.getName());
             return this.invoke();
         }
         LOG.debug("Executing lazy params interceptor: {}", 
interceptorMapping.getName());
@@ -332,6 +334,13 @@ public class DefaultActionInvocation implements 
ActionInvocation {
     }
 
     /**
+     * Merges the params declared on the interceptor-ref with those of the 
mapping being invoked.
+     * <p>
+     * The name-based lookup is inherited behaviour, kept as-is: the mapping 
is normally the very one
+     * found by name, so the merge is a no-op, and when a stack references the 
same interceptor name
+     * twice with different params it merges the first mapping's params over 
the current one, which
+     * is questionable. Changing it is out of scope here.
+     *
      * @return a fresh map; the mapping's own param map is shared across 
requests and must not be mutated
      */
     private Map<String, String> mergedParams(InterceptorMapping 
interceptorMapping) {
@@ -343,12 +352,25 @@ public class DefaultActionInvocation implements 
ActionInvocation {
         return merged;
     }
 
+    /**
+     * @deprecated since 7.3.0, use {@link 
#executeConditional(ConditionalInterceptor, String)} so the
+     * interceptor is identified by its mapping name in the logs.
+     */
+    @Deprecated
     protected String executeConditional(ConditionalInterceptor 
conditionalInterceptor) throws Exception {
+        return executeConditional(conditionalInterceptor, 
conditionalInterceptor.getClass().getSimpleName());
+    }
+
+    /**
+     * @param interceptorName the name of the interceptor mapping being 
invoked, used for logging
+     * @since 7.3.0
+     */
+    protected String executeConditional(ConditionalInterceptor 
conditionalInterceptor, String interceptorName) throws Exception {
         if (conditionalInterceptor.shouldIntercept(this)) {
-            LOG.debug("Executing conditional interceptor: {}", 
conditionalInterceptor.getClass().getSimpleName());
+            LOG.debug("Executing conditional interceptor: {}", 
interceptorName);
             return conditionalInterceptor.intercept(this);
         } else {
-            LOG.debug("Interceptor: {} is disabled, skipping to next", 
conditionalInterceptor.getClass().getSimpleName());
+            LOG.debug("Interceptor: {} declined by shouldIntercept(), skipping 
to next", interceptorName);
             return this.invoke();
         }
     }
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java 
b/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java
index 08702dee4..5911a401e 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/UploadPolicy.java
@@ -87,6 +87,13 @@ public class UploadPolicy extends DisableParams {
      * A parameter that could not be resolved makes this policy unusable: the 
upload is rejected
      * rather than validated against a partially-resolved policy, so a broken 
expression cannot
      * silently relax validation.
+     * <p>
+     * <strong>Any</strong> param name is recorded, {@code disabled} included. 
An unresolvable
+     * {@code <param name="disabled">${...}</param>} therefore leaves the 
interceptor enabled (the
+     * flag keeps its configured value) <em>and</em> marks this policy 
unusable, so every upload in
+     * that invocation is rejected. That is deliberate — a broken dispatch 
flag is a broken
+     * configuration, and refusing the upload is the safe reading — but it 
means a typo in
+     * {@code disabled} takes out upload validation rather than only the 
disabling.
      */
     @Override
     public void unresolved(String paramName) {
@@ -108,9 +115,14 @@ public class UploadPolicy extends DisableParams {
         return new UploadPolicy(this);
     }
 
+    /**
+     * The resulting set is unmodifiable: the copy constructor shares it by 
reference with the
+     * configured policy, so every per-invocation policy that does not 
override the param would
+     * otherwise hand out a live handle on process-wide configuration.
+     */
     private static Set<String> toSet(String commaDelimited) {
         return commaDelimited == null
                 ? Collections.emptySet()
-                : TextParseUtil.commaDelimitedStringToSet(commaDelimited);
+                : 
Collections.unmodifiableSet(TextParseUtil.commaDelimitedStringToSet(commaDelimited));
     }
 }

Reply via email to