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 cf7d65714 WW-5765 fix(interceptor): re-store only the token validated 
in the request in execAndWait (#2023)
cf7d65714 is described below

commit cf7d65714be57d6f25d5306b2b0a8ccc9750089a
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Oct 16 07:01:31 2026 +0200

    WW-5765 fix(interceptor): re-store only the token validated in the request 
in execAndWait (#2023)
    
    ExecuteAndWaitInterceptor put a transaction token back into the session
    on every wait result, reading the value from the "token" request
    parameter and the name from "struts.token.name". It did so whether or
    not a token had been validated in the current request, including for
    execAndWait actions without a token check, and it read the value and
    the name under different names.
    
    TokenHelper.validToken() now records the token name and value it
    accepted in the current ActionContext, and the new
    TokenHelper.restoreValidatedToken() puts exactly that pair back into
    the session. ExecuteAndWaitInterceptor calls it instead of reading
    request parameters, so with no validated token it leaves the session
    token store unchanged. Actions combining token or tokenSession with
    execAndWait keep accepting wait-page refreshes as before (WW-1741).
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../interceptor/ExecuteAndWaitInterceptor.java     |  5 +-
 .../java/org/apache/struts2/util/TokenHelper.java  | 23 ++++++
 .../interceptor/ExecuteAndWaitInterceptorTest.java | 82 ++++++++++++++++++++++
 .../org/apache/struts2/util/TokenHelperTest.java   | 44 ++++++++++++
 4 files changed, 150 insertions(+), 4 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
index 8d487661d..3c2b897e0 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
@@ -281,10 +281,7 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
                 LOG.trace("BackgroundProcess instance is not done (wait 
processing) - executeAfterValidationPass: {}, secondTime: {}.", 
executeAfterValidationPass, secondTime);
                 actionInvocation.getStack().push(bp.getAction());
 
-                final String token = TokenHelper.getToken();
-                if (token != null) {
-                    TokenHelper.setSessionToken(TokenHelper.getTokenName(), 
token);
-                }
+                TokenHelper.restoreValidatedToken();
 
                 Map<String, ResultConfig> results = 
proxy.getConfig().getResults();
                 if (!results.containsKey(WAIT)) {
diff --git a/core/src/main/java/org/apache/struts2/util/TokenHelper.java 
b/core/src/main/java/org/apache/struts2/util/TokenHelper.java
index c03a13514..385271bf5 100644
--- a/core/src/main/java/org/apache/struts2/util/TokenHelper.java
+++ b/core/src/main/java/org/apache/struts2/util/TokenHelper.java
@@ -52,6 +52,7 @@ public class TokenHelper {
     public static final String TOKEN_NAME_FIELD = "struts.token.name";
     private static final Logger LOG = LogManager.getLogger(TokenHelper.class);
     private static final Random RANDOM = new SecureRandom();
+    private static final String VALIDATED_TOKEN_KEY = 
TokenHelper.class.getName() + ".validatedToken";
 
 
     /**
@@ -196,11 +197,33 @@ public class TokenHelper {
 
         // remove the token so it won't be used again
         session.remove(tokenSessionName);
+        ActionContext.getContext().put(VALIDATED_TOKEN_KEY, new 
ValidatedToken(tokenName, token));
 
         return true;
     }
 
+    /**
+     * Puts back into the session the token accepted by {@link #validToken()} 
earlier in the current request, so the
+     * same token stays valid for a follow-up request. Does nothing if no 
token was accepted in the current request.
+     */
+    public static void restoreValidatedToken() {
+        ValidatedToken validated = (ValidatedToken) 
ActionContext.getContext().get(VALIDATED_TOKEN_KEY);
+        if (validated != null) {
+            setSessionToken(validated.name, validated.value);
+        }
+    }
+
     public static String generateGUID() {
         return new BigInteger(165, RANDOM).toString(36).toUpperCase();
     }
+
+    private static final class ValidatedToken {
+        private final String name;
+        private final String value;
+
+        private ValidatedToken(String name, String value) {
+            this.name = name;
+            this.value = value;
+        }
+    }
 }
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
index 87595d9b5..acf75fdc2 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
@@ -36,6 +36,7 @@ import 
com.opensymphony.xwork2.interceptor.ParametersInterceptor;
 import com.opensymphony.xwork2.mock.MockResult;
 import com.opensymphony.xwork2.ognl.OgnlUtil;
 import com.opensymphony.xwork2.util.location.LocatableProperties;
+import org.apache.struts2.util.TokenHelper;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.dispatcher.HttpParameters;
 import org.apache.struts2.interceptor.exec.ExecutorProvider;
@@ -64,6 +65,8 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
     private Map<String, Object> session;
     private ExecuteAndWaitInterceptor waitInterceptor;
     private ParametersInterceptor parametersInterceptor;
+    private TokenInterceptor tokenInterceptor;
+    private TokenSessionStoreInterceptor tokenSessionInterceptor;
 
     public void testOneWait() throws Exception {
         waitInterceptor.setDelay(0);
@@ -219,6 +222,63 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
         assertEquals("success", result3);
     }
 
+    public void testWaitWithoutTokenCheckLeavesSessionTokensUnchanged() throws 
Exception {
+        waitInterceptor.setDelay(0);
+        waitInterceptor.setDelaySleepInterval(0);
+        String customTokenSessionName = 
TokenHelper.buildTokenSessionAttributeName("custom");
+        session.put(customTokenSessionName, "server-generated");
+
+        for (String tokenName : new String[]{TokenHelper.DEFAULT_TOKEN_NAME, 
"custom"}) {
+            Map<String, String[]> requestParams = new HashMap<>();
+            requestParams.put(TokenHelper.TOKEN_NAME_FIELD, new 
String[]{tokenName});
+            requestParams.put(TokenHelper.DEFAULT_TOKEN_NAME, new 
String[]{"request-supplied"});
+            requestParams.put("custom", new String[]{"request-supplied"});
+            withRequestParams(requestParams);
+
+            assertEquals("wait", buildProxy("plainWait").execute());
+
+            
assertNull(session.get(TokenHelper.buildTokenSessionAttributeName(TokenHelper.DEFAULT_TOKEN_NAME)));
+            assertEquals("server-generated", 
session.get(customTokenSessionName));
+        }
+    }
+
+    public void testTokenAndWaitAcceptsWaitPageRefreshWithValidatedToken() 
throws Exception {
+        assertWaitPageRefreshAccepted("tokenWait");
+    }
+
+    public void 
testTokenSessionAndWaitAcceptsWaitPageRefreshWithValidatedToken() throws 
Exception {
+        assertWaitPageRefreshAccepted("tokenSessionWait");
+    }
+
+    private void assertWaitPageRefreshAccepted(String actionName) throws 
Exception {
+        waitInterceptor.setDelay(0);
+        waitInterceptor.setDelaySleepInterval(0);
+        String tokenSessionName = 
TokenHelper.buildTokenSessionAttributeName(TokenHelper.DEFAULT_TOKEN_NAME);
+        session.put(tokenSessionName, "server-generated");
+
+        Map<String, String[]> requestParams = new HashMap<>();
+        requestParams.put(TokenHelper.TOKEN_NAME_FIELD, new 
String[]{TokenHelper.DEFAULT_TOKEN_NAME});
+        requestParams.put(TokenHelper.DEFAULT_TOKEN_NAME, new 
String[]{"server-generated"});
+        withRequestParams(requestParams);
+
+        assertEquals("wait", buildProxy(actionName).execute());
+        assertEquals("server-generated", session.get(tokenSessionName));
+
+        Thread.sleep(300);
+
+        assertEquals("wait", buildProxy(actionName).execute());
+        assertEquals("server-generated", session.get(tokenSessionName));
+
+        Thread.sleep(500);
+
+        assertEquals("success", buildProxy(actionName).execute());
+        assertNull(session.get(tokenSessionName));
+    }
+
+    private void withRequestParams(Map<String, String[]> requestParams) {
+        context = 
ActionContext.of(context).withParameters(HttpParameters.create(requestParams).build()).getContextMap();
+    }
+
     protected ActionProxy buildProxy(String actionName) throws Exception {
         return actionProxyFactory.createActionProxy("", actionName, null, 
context);
     }
@@ -244,9 +304,13 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
 
         container.inject(waitInterceptor);
         container.inject(parametersInterceptor);
+        container.inject(tokenInterceptor);
+        container.inject(tokenSessionInterceptor);
 
         waitInterceptor.init();
         parametersInterceptor.init();
+        tokenInterceptor.init();
+        tokenSessionInterceptor.init();
     }
 
     protected void tearDown() throws Exception {
@@ -273,6 +337,8 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
             // interceptors
             waitInterceptor = new ExecuteAndWaitInterceptor();
             parametersInterceptor = new ParametersInterceptor();
+            tokenInterceptor = new TokenInterceptor();
+            tokenSessionInterceptor = new TokenSessionStoreInterceptor();
             PackageConfig wait = new PackageConfig.Builder("")
                 .addActionConfig("action1", new ActionConfig.Builder("", 
"action1", ExecuteAndWaitDelayAction.class.getName())
                     .addResultConfig(new ResultConfig.Builder(Action.SUCCESS, 
MockResult.class.getName()).build())
@@ -280,10 +346,26 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
                     .addInterceptor(new InterceptorMapping("params", 
parametersInterceptor))
                     .addInterceptor(new InterceptorMapping("execAndWait", 
waitInterceptor))
                     .build())
+                .addActionConfig("plainWait", waitActionConfig("plainWait"))
+                .addActionConfig("tokenWait", waitActionConfig("tokenWait", 
new InterceptorMapping("token", tokenInterceptor)))
+                .addActionConfig("tokenSessionWait", 
waitActionConfig("tokenSessionWait", new InterceptorMapping("tokenSession", 
tokenSessionInterceptor)))
                 .build();
             configuration.addPackageConfig("", wait);
         }
 
+        private ActionConfig waitActionConfig(String name, 
InterceptorMapping... tokenInterceptors) {
+            ActionConfig.Builder builder = new ActionConfig.Builder("", name, 
ExecuteAndWaitDelayAction.class.getName())
+                .addResultConfig(new ResultConfig.Builder(Action.SUCCESS, 
MockResult.class.getName()).build())
+                .addResultConfig(new 
ResultConfig.Builder(ExecuteAndWaitInterceptor.WAIT, 
MockResult.class.getName()).build());
+            for (InterceptorMapping tokenInterceptor : tokenInterceptors) {
+                builder.addInterceptor(tokenInterceptor);
+            }
+            return builder
+                .addInterceptor(new InterceptorMapping("params", 
parametersInterceptor))
+                .addInterceptor(new InterceptorMapping("execAndWait", 
waitInterceptor))
+                .build();
+        }
+
         public void register(ContainerBuilder builder, LocatableProperties 
props) throws ConfigurationException {
             builder.factory(ObjectFactory.class);
             builder.factory(ActionProxyFactory.class, 
DefaultActionProxyFactory.class);
diff --git a/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java 
b/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
index 4eba8f1b8..f660e497e 100644
--- a/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
+++ b/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
@@ -76,6 +76,50 @@ public class TokenHelperTest extends TestCase {
         assertTrue(TokenHelper.validToken());
     }
 
+    public void testRestoreValidatedTokenPutsBackTheValidatedToken() {
+        String tokenName = "restoreTokenTest";
+        String token = TokenHelper.setToken(tokenName);
+        final String sessionTokenName = 
TokenHelper.buildTokenSessionAttributeName(tokenName);
+        withTokenParams(tokenName, token);
+
+        assertTrue(TokenHelper.validToken());
+        assertNull(session.get(sessionTokenName));
+
+        TokenHelper.restoreValidatedToken();
+
+        assertEquals(token, session.get(sessionTokenName));
+    }
+
+    public void 
testRestoreValidatedTokenAfterRejectedTokenLeavesSessionUnchanged() {
+        String tokenName = "rejectedTokenTest";
+        String token = TokenHelper.setToken(tokenName);
+        final String sessionTokenName = 
TokenHelper.buildTokenSessionAttributeName(tokenName);
+        Map<String, String[]> params = new HashMap<>();
+        params.put(TokenHelper.TOKEN_NAME_FIELD, new String[]{tokenName});
+        
ActionContext.getContext().withParameters(HttpParameters.create(params).build());
+
+        assertFalse(TokenHelper.validToken());
+        TokenHelper.restoreValidatedToken();
+
+        assertEquals(token, session.get(sessionTokenName));
+    }
+
+    public void 
testRestoreValidatedTokenWithoutValidationLeavesSessionUnchanged() {
+        String tokenName = "unvalidatedTokenTest";
+        withTokenParams(tokenName, "request-supplied");
+
+        TokenHelper.restoreValidatedToken();
+
+        assertTrue(session.isEmpty());
+    }
+
+    private void withTokenParams(String tokenName, String token) {
+        Map<String, String[]> params = new HashMap<>();
+        params.put(TokenHelper.TOKEN_NAME_FIELD, new String[]{tokenName});
+        params.put(tokenName, new String[]{token});
+        
ActionContext.getContext().withParameters(HttpParameters.create(params).build());
+    }
+
     public void testGetTokenDoesNotNpe() {
         String token = TokenHelper.getToken(null);
         assertNull(token);

Reply via email to