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

commit da18bd64fe6aada8f7ed12f6386e4f6188642a06
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Nov 2 08:32:59 2026 +0100

    WW-4164 fix(core): ignore default-action-ref when claiming declared 
extensions
    
    A name that resolves only through a namespace's default-action-ref no longer
    counts as a match: the mapper leaves such a URL to the container (static
    files keep being served) and the outbound resolver leaves the extension
    untouched. RuntimeConfiguration gains getMatchedActionConfig, which walks 
the
    same lookup as getActionConfig but drops a default-action-ref result.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../struts2/config/RuntimeConfiguration.java       | 13 +++++
 .../struts2/config/impl/DefaultConfiguration.java  | 60 +++++++++++++++++-----
 .../dispatcher/mapper/ActionExtensionResolver.java |  5 +-
 .../dispatcher/mapper/DefaultActionMapper.java     |  5 +-
 .../dispatcher/ActionExtensionEndToEndTest.java    |  7 +++
 .../mapper/ActionExtensionResolverTest.java        |  9 ++++
 .../dispatcher/mapper/DefaultActionMapperTest.java | 26 ++++++++++
 .../providers/struts-extension-default-action.xml} | 10 ++--
 .../dispatcher/struts-extension-one-package.xml    |  1 +
 9 files changed, 113 insertions(+), 23 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/config/RuntimeConfiguration.java 
b/core/src/main/java/org/apache/struts2/config/RuntimeConfiguration.java
index 57916423d..76806e4ba 100644
--- a/core/src/main/java/org/apache/struts2/config/RuntimeConfiguration.java
+++ b/core/src/main/java/org/apache/struts2/config/RuntimeConfiguration.java
@@ -42,6 +42,19 @@ public interface RuntimeConfiguration extends Serializable {
      */
     ActionConfig getActionConfig(String namespace, String name);
 
+    /**
+     * Same lookup as {@link #getActionConfig(String, String)}, but returns 
{@code null} when the result comes only from
+     * a namespace's {@code default-action-ref}, so only exact and wildcard 
matches of {@code name} are returned.
+     *
+     * @param namespace the namespace of the Action, {@code null} means the 
empty namespace
+     * @param name      the name of the Action
+     * @return the matched ActionConfig, or {@code null}
+     * @since 7.5.0
+     */
+    default ActionConfig getMatchedActionConfig(String namespace, String name) 
{
+        return getActionConfig(namespace, name);
+    }
+
     /**
      * returns a Map of all the registered ActionConfigs.  Again, these 
ActionConfigs are fully expanded so that any
      * inherited interceptors, results, etc. will be included
diff --git 
a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java 
b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
index a66f31076..6862a0ac4 100644
--- 
a/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
+++ 
b/core/src/main/java/org/apache/struts2/config/impl/DefaultConfiguration.java
@@ -640,36 +640,47 @@ public class DefaultConfiguration implements 
Configuration {
          */
         @Override
         public ActionConfig getActionConfig(String namespace, String name) {
-            ActionConfig config = findActionConfigInNamespace(namespace, name);
+            Lookup lookup = lookup(namespace, name);
+            return lookup == null ? null : lookup.config;
+        }
+
+        @Override
+        public ActionConfig getMatchedActionConfig(String namespace, String 
name) {
+            Lookup lookup = lookup(namespace, name);
+            return lookup == null || lookup.defaultAction ? null : 
lookup.config;
+        }
+
+        private Lookup lookup(String namespace, String name) {
+            Lookup lookup = lookupInNamespace(namespace, name);
 
             // try wildcarded namespaces
-            if (config == null) {
+            if (lookup == null) {
                 NamespaceMatch match = namespaceMatcher.match(namespace);
                 if (match != null) {
-                    config = findActionConfigInNamespace(match.getPattern(), 
name);
+                    lookup = lookupInNamespace(match.getPattern(), name);
 
                     // If config found, place all the matches found in the 
namespace processing in the action's parameters
-                    if (config != null) {
-                        config = new ActionConfig.Builder(config)
+                    if (lookup != null) {
+                        lookup = lookup.withConfig(new 
ActionConfig.Builder(lookup.config)
                                 .addParams(match.getVariables())
-                                .build();
+                                .build());
                     }
                 }
             }
 
             // fail over to empty namespace
-            if (config == null && shouldFallbackToEmptyNamespace(namespace)) {
-                config = findActionConfigInNamespace("", name);
+            if (lookup == null && shouldFallbackToEmptyNamespace(namespace)) {
+                lookup = lookupInNamespace("", name);
             }
 
-            return config;
+            return lookup;
         }
 
         private boolean shouldFallbackToEmptyNamespace(String namespace) {
             return StringUtils.isNotBlank(namespace) && ("/".equals(namespace) 
|| fallbackToEmptyNamespace);
         }
 
-        private ActionConfig findActionConfigInNamespace(String namespace, 
String name) {
+        private Lookup lookupInNamespace(String namespace, String name) {
             if (namespace == null) {
                 namespace = "";
             }
@@ -680,15 +691,16 @@ public class DefaultConfiguration implements 
Configuration {
 
             ActionConfig config = actions.get(name);
             if (config != null) {
-                return config;
+                return Lookup.matched(config);
             }
 
             config = namespaceActionConfigMatchers.get(namespace).match(name);
             if (config != null) {
-                return config;
+                return Lookup.matched(config);
             }
 
-            return findDefaultActionConfig(namespace, actions);
+            config = findDefaultActionConfig(namespace, actions);
+            return config == null ? null : Lookup.defaultAction(config);
         }
 
         private ActionConfig findDefaultActionConfig(String namespace, 
Map<String, ActionConfig> actions) {
@@ -729,6 +741,28 @@ public class DefaultConfiguration implements Configuration 
{
 
             return buff.toString();
         }
+
+        private static final class Lookup {
+            private final ActionConfig config;
+            private final boolean defaultAction;
+
+            private Lookup(ActionConfig config, boolean defaultAction) {
+                this.config = config;
+                this.defaultAction = defaultAction;
+            }
+
+            private static Lookup matched(ActionConfig config) {
+                return new Lookup(config, false);
+            }
+
+            private static Lookup defaultAction(ActionConfig config) {
+                return new Lookup(config, true);
+            }
+
+            private Lookup withConfig(ActionConfig other) {
+                return new Lookup(other, defaultAction);
+            }
+        }
     }
 
     class ContainerProperties extends LocatableProperties {
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
index 574fffbbe..aad463bb6 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
@@ -62,7 +62,8 @@ public class ActionExtensionResolver {
      * Sets the extension of {@code target} to the current request's extension 
when the target accepts it, otherwise
      * to the target's first accepted extension, which is also used when the 
current request has no extension, as in
      * {@link DefaultActionMapper#lookupExtension(String)}. Leaves {@code 
target} unchanged when it already has an extension,
-     * when no action in the configuration declares extensions, when the 
target action is unknown, or when neither the
+     * when no action in the configuration declares extensions, when the 
target action is unknown (a name reached only
+     * through the namespace's {@code default-action-ref} counts as unknown), 
or when neither the
      * target nor the global constant declares extensions.
      *
      * @param target the mapping of the action the URL points at
@@ -77,7 +78,7 @@ public class ActionExtensionResolver {
             return;
         }
         ActionConfig actionConfig = runtimeConfiguration
-            .getActionConfig(target.getNamespace(), 
StringUtils.substringBefore(target.getName(), "!"));
+            .getMatchedActionConfig(target.getNamespace(), 
StringUtils.substringBefore(target.getName(), "!"));
         if (actionConfig == null) {
             return;
         }
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
index b5a38921b..0c3640408 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java
@@ -476,7 +476,8 @@ public class DefaultActionMapper implements ActionMapper {
 
     /**
      * Strips an extension that only package or action {@code extension} 
attributes declare, but only when the
-     * remaining URI resolves to an action that itself declares extensions.
+     * remaining URI matches an action (exactly or by wildcard, never through 
a {@code default-action-ref}) that itself
+     * declares extensions.
      *
      * @param uri           the request URI
      * @param mapping       the action mapping to store the extension in
@@ -508,7 +509,7 @@ public class DefaultActionMapper implements ActionMapper {
         String name = probe.getName();
         int bang = name.indexOf('!');
         String actionName = bang == -1 ? name : name.substring(0, bang);
-        ActionConfig config = 
runtimeConfiguration.getActionConfig(probe.getNamespace(), actionName);
+        ActionConfig config = 
runtimeConfiguration.getMatchedActionConfig(probe.getNamespace(), actionName);
         return config != null && config.getExtensions() != null;
     }
 
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java
index 6039d2222..0086defcc 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/ActionExtensionEndToEndTest.java
@@ -87,6 +87,13 @@ public class ActionExtensionEndToEndTest extends 
StrutsInternalTestCase {
         assertThat(request.getAttribute(EXECUTED)).isNull();
     }
 
+    public void testStaticHtmlUnderDeclaringDefaultActionFallsThrough() throws 
Exception {
+        doFilter("/shop/manual.html");
+
+        assertThat(filterChain.getRequest()).isNotNull();
+        assertThat(request.getAttribute(EXECUTED)).isNull();
+    }
+
     public void testDeclaredActionServedOnItsExtension() throws Exception {
         doFilter("/shop/cart.html");
 
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
index c1626f46a..1e45a55a3 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
@@ -85,6 +85,15 @@ public class ActionExtensionResolverTest extends 
StrutsInternalTestCase {
         assertThat(resolve("/child", "inherits!foo")).isEqualTo("html");
     }
 
+    public void 
testUnknownNameResolvedOnlyThroughDeclaringDefaultActionUntouched() {
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider("org/apache/struts2/config/providers/struts-extension-default-action.xml"));
+        resolver.setConfiguration(configuration);
+        withCurrentExtension("action");
+
+        assertThat(resolve("", "manual")).isNull();
+        assertThat(resolve("", "index")).isEqualTo("html");
+    }
+
     public void testWithoutCurrentMappingUsesFirstAllowed() {
         assertThat(resolve("/child", "inherits")).isEqualTo("html");
     }
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
index 75683b488..e5ffd6e4a 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/DefaultActionMapperTest.java
@@ -48,6 +48,7 @@ public class DefaultActionMapperTest extends 
StrutsInternalTestCase {
 
     private static final String EXTENSION_CONFIG = 
"org/apache/struts2/config/providers/struts-extension.xml";
     private static final String EXTENSION_RELOAD_CONFIG = 
"org/apache/struts2/config/providers/struts-extension-reload.xml";
+    private static final String EXTENSION_DEFAULT_ACTION_CONFIG = 
"org/apache/struts2/config/providers/struts-extension-default-action.xml";
 
     private MockHttpServletRequest req;
     private ConfigurationManager configManager;
@@ -1099,6 +1100,31 @@ public class DefaultActionMapperTest extends 
StrutsInternalTestCase {
         assertThat(mapDeclared(mapper, 
"/child/inherits.json").getExtension()).isEqualTo("json");
     }
 
+    public void testDeclaringDefaultActionDoesNotClaimStaticFile() {
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(EXTENSION_DEFAULT_ACTION_CONFIG));
+
+        assertThat(mapDeclared(declaredMapper(), 
"/docs/manual.html")).isNull();
+        assertThat(mapDeclared(declaredMapper(), "/manual.html")).isNull();
+    }
+
+    public void testDeclaringDefaultActionClaimsItsOwnName() {
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(EXTENSION_DEFAULT_ACTION_CONFIG));
+
+        ActionMapping mapping = mapDeclared(declaredMapper(), "/index.html");
+
+        assertThat(mapping.getName()).isEqualTo("index");
+        assertThat(mapping.getExtension()).isEqualTo("html");
+    }
+
+    public void testWildcardBesideDeclaringDefaultActionStillClaimed() {
+        loadConfigurationProviders(new 
StrutsXmlConfigurationProvider(EXTENSION_DEFAULT_ACTION_CONFIG));
+
+        ActionMapping mapping = mapDeclared(declaredMapper(), 
"/edit_user.html");
+
+        assertThat(mapping.getName()).isEqualTo("edit_user");
+        assertThat(mapping.getExtension()).isEqualTo("html");
+    }
+
     private DefaultActionMapper declaredMapper() {
         DefaultActionMapper mapper = new DefaultActionMapper();
         mapper.setExtensions("action,");
diff --git 
a/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
 
b/core/src/test/resources/org/apache/struts2/config/providers/struts-extension-default-action.xml
similarity index 72%
copy from 
core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
copy to 
core/src/test/resources/org/apache/struts2/config/providers/struts-extension-default-action.xml
index 54346be82..8a1389b7b 100644
--- 
a/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
+++ 
b/core/src/test/resources/org/apache/struts2/config/providers/struts-extension-default-action.xml
@@ -23,11 +23,9 @@
     "-//Apache Software Foundation//DTD Struts Configuration 7.5//EN"
     "https://struts.apache.org/dtds/struts-7.5.dtd";>
 <struts>
-    <package name="shop" extends="struts-default" namespace="/shop" 
extension="html">
-        <action name="cart" 
class="org.apache.struts2.dispatcher.ActionExtensionEndToEndTest$RecordingAction"/>
-    </package>
-
-    <package name="root" extends="struts-default" namespace="/">
-        <action name="home" 
class="org.apache.struts2.dispatcher.ActionExtensionEndToEndTest$RecordingAction"/>
+    <package name="site" namespace="" extension="html,">
+        <default-action-ref name="index"/>
+        <action name="index" class="org.apache.struts2.TestAction"/>
+        <action name="edit_*" class="org.apache.struts2.TestAction"/>
     </package>
 </struts>
diff --git 
a/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
 
b/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
index 54346be82..183af98be 100644
--- 
a/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
+++ 
b/core/src/test/resources/org/apache/struts2/dispatcher/struts-extension-one-package.xml
@@ -24,6 +24,7 @@
     "https://struts.apache.org/dtds/struts-7.5.dtd";>
 <struts>
     <package name="shop" extends="struts-default" namespace="/shop" 
extension="html">
+        <default-action-ref name="cart"/>
         <action name="cart" 
class="org.apache.struts2.dispatcher.ActionExtensionEndToEndTest$RecordingAction"/>
     </package>
 

Reply via email to