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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 8f481e740 WW-5765 fix(interceptor): re-store only the token validated 
in the request in execAndWait (#2022)
8f481e740 is described below

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

    WW-5765 fix(interceptor): re-store only the token validated in the request 
in execAndWait (#2022)
    
    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 6fe8ed584..5a073147f 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
@@ -310,10 +310,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 8bc349e91..e2ead6fc5 100644
--- a/core/src/main/java/org/apache/struts2/util/TokenHelper.java
+++ b/core/src/main/java/org/apache/struts2/util/TokenHelper.java
@@ -54,6 +54,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";
 
 
     /**
@@ -203,11 +204,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 7af1e6cc4..7701173eb 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
@@ -34,6 +34,7 @@ import org.apache.struts2.config.entities.ResultConfig;
 import org.apache.struts2.inject.ContainerBuilder;
 import org.apache.struts2.mock.MockResult;
 import org.apache.struts2.ognl.OgnlUtil;
+import org.apache.struts2.util.TokenHelper;
 import org.apache.struts2.util.location.LocatableProperties;
 import jakarta.servlet.http.HttpSession;
 import org.apache.struts2.StrutsInternalTestCase;
@@ -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 85fcf3dcc..5308cf04b 100644
--- a/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
+++ b/core/src/test/java/org/apache/struts2/util/TokenHelperTest.java
@@ -115,6 +115,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