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>
