Commit Graph

3927 Commits

Author SHA1 Message Date
Lukasz Lenart 789dbf3cd2 WW-5640 Add WebJars support to Struts core (#1765)
* WW-5640 docs: design for WebJars support in Struts core

Adds first-class WebJars support so client-side libraries can be
referenced by a version-less logical path and served through the
existing static-content pipeline. Grounded against 7.2.x source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 docs: implementation plan for WebJars support

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 build: add webjars-locator-lite dependency

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 feat: add webjars config constants and defaults

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 docs: correct plan test framework to JUnit 4

core uses JUnit 4 + AssertJ + Mockito, not JUnit 5 Jupiter (no
Jupiter engine on the classpath). Test tasks translate accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 feat: add WebJarUrlProvider resolution seam

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 feat: register WebJarUrlProvider bean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 feat: extend static content-type map for webjar assets

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 feat: serve webjar assets via static content loader

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 feat: add <s:webjar> tag and <@s.webjar> macro

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 docs: add generated tag reference for <s:webjar>

Annotation-processor-generated tag reference (attributes + description),
tracked like every other tag's docs under core/src/site/resources/tags/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 fix: address final review (log level, resolveUrl traversal test, javadoc)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5640 refactor: address SonarCloud code smells

- getContentType: replace long if/else chain with a static extension->
  MIME map (S3776 cognitive complexity)
- DefaultWebJarUrlProvider.split: return Optional<String[]> instead of a
  null sentinel (S1168; Optional fits the reject semantics, empty-array
  would not)
- serving tests: rename local 'loader' -> 'webJarLoader' to stop hiding
  the ContentTypeProbe field (S1117)
- WebJarTest: use assertThat(writer).hasToString(...) (S5838)

S110 (WebJarTag inheritance depth) is inherent to the Struts tag base
class hierarchy shared by every tag; left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:03:05 +02:00
Lukasz Lenart 50c16e93b1 Release 7.2.1-RC1 (#1744)
* [maven-release-plugin] prepare release STRUTS_7_2_1

* [maven-release-plugin] prepare for next development iteration
2026-06-26 07:18:26 +02:00
Lukasz Lenart 59e342488d WW-5256 Decouple FreeMarker whitespace stripping from devMode (#1743)
* WW-5256 docs: design to decouple FreeMarker whitespace stripping from devMode

Fixes s:textarea rendering blank lines and HTML whitespace bloat in devMode
by honoring struts.freemarker.whitespaceStripping unconditionally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5256 docs: implementation plan to decouple whitespace stripping from devMode

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5256 test: prove whitespace stripping wrongly disabled in devMode

* WW-5256 fix(freemarker): honor whitespaceStripping regardless of devMode

* WW-5256 docs: drop devMode note from whitespaceStripping constant

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 12:22:51 +02:00
Lukasz Lenart 489445c78d Release 7.2.0-RC1 (#1742)
* [maven-release-plugin] prepare release STRUTS_7_2_0

* [maven-release-plugin] prepare for next development iteration
2026-06-15 12:07:02 +02:00
Arun cc3ebc3c1f WW-5635 Avoid logging sensitive token values in TokenHelper (#1738)
* Avoid logging sensitive token values in TokenHelper

Redact form and session token values from WARN-level log output
in TokenHelper.validToken() and update corresponding i18n message
properties. Detailed diagnostics moved to DEBUG level with
sanitized input.

* Update struts-messages.properties

* Update invalid token error message for clarity

* Update struts-messages_da.properties

* Update struts-messages_de.properties

* Update Polish translation for invalid token message

* Update invalid token message in Portuguese properties

* Improve token mismatch warning logging

Updated warning message to include the form token in the log.

* Update struts-messages.properties

* Update invalid token message format in properties file

* Update invalid token message for clarity

* Update struts-messages_de.properties

* Update struts-messages_pl.properties

* Update invalid token message format in properties file

* Update TokenHelper.java

* Refactor token mismatch logging for development mode
2026-06-14 17:08:39 +00:00
Arun 8f9b4b8a90 WW-5636 Harden redirect URL escaping in non-302 response body (#1737)
* Implement test for status code 200 with HTML escaping

* Escape HTML in ServletRedirectResult response

Escape HTML in the final location before writing to the response.
2026-06-14 18:44:43 +02:00
Lukasz Lenart dd830dca80 WW-5630 test: streamline ConfigParseUtilTest and convert to JUnit 4 (#1740)
Collapse 12 overlapping cache tests to 5 focused ones, replace the
~80-entry JDK class-name literal with a synthetic-name loop bounded by
the inner-cache limit, and drop reflection from the behavioral tests
(load-count assertions only). Reflection is retained solely in the two
size-bound tests, where Caffeine exposes no public seam.

Production ConfigParseUtil caching logic is unchanged.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 09:16:39 +00:00
Lukasz Lenart 4f3fd69aa6 WW-5632 Harden commons-fileupload2 dependency against milestone binary-incompatibility (#1735)
* WW-5632 docs: add commons-fileupload2 milestone-hardening design spec

Design for hardening the commons-fileupload2 dependency against
milestone binary-incompatibility (manage -core, activate a scoped
enforcer rule, add a runtime API guard in AbstractMultiPartRequest).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5632 docs: add implementation plan for fileupload2 milestone hardening

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5632 build(deps): manage commons-fileupload2-core alongside jakarta-servlet6

Pin both commons-fileupload2 artifacts to a single
commons-fileupload2.version property so the volatile -core API can no
longer skew from -jakarta-servlet6 in the reactor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5632 build: enforce a single commons-fileupload2 version

Activate maven-enforcer-plugin (previously dormant in pluginManagement)
with a fileupload-scoped bannedDependencies rule so any divergent
commons-fileupload2 version fails the build early.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5632 fix(fileupload): fail fast on incompatible commons-fileupload2 API

Verify once per JVM that the fileupload size-limit setters exist and
throw a clear StrutsException reporting the core/jakarta version skew,
replacing an opaque deep-stack NoSuchMethodError in downstream runtimes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* WW-5632 fix(fileupload): make API-verification guard static

Resolve Sonar java:S2696 (instance method writing a static field) by
making ensureFileUploadApiVerified() static; verification is JVM-global.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 08:28:47 +02:00
brianandle 210dc86b88 WW-5630 - Performance Issue SecurityMemberAccess (#1721)
* WW-5630 - Performance Issue SecurityMemberAccess
* Add size bound cache, 50, for Class lookup
* Add unit test

Code generated by Copilot

* WW-5630 - Add additional UT

* WW-5630 - Add UT for non-existent class

* WW-5630 - Review feedback changes
* Cache ClassLoader directly
* Use weakKeys and weakValues
* Comment on the ClassLookupException
* Additional Unit Tests

Assistance in coding using co-pilot

* WW-5630 - Additional review
* Limit outer, Classloader, to 25. Ensure memory bounding.
* Limit inner, Classes, to 50. Ensure memory bounding.
* Additional UTs

With co-pilot assitance
2026-06-12 17:12:26 +00:00
Lukasz Lenart 9011c32b38 WW-5631 Add opt-in @StrutsParameter enforcement to ChainingInterceptor (#1719)
* WW-5631 feat(chaining): add struts.chaining.requireAnnotations constant

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 feat(chaining): default struts.chaining.requireAnnotations=false

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 test(chaining): add annotated/unannotated chaining fixtures

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 test(chaining): add failing @StrutsParameter enforcement tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 feat(chaining): enforce @StrutsParameter on target when opted in

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 refactor(chaining): align requireAnnotations parsing with BooleanUtils

Use BooleanUtils.toBoolean for the chaining requireAnnotations flag so it
accepts the same values (yes/on/1) as the sibling
struts.parameters.requireAnnotations switch, and unify the enforcement WARN
message prefix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 test(chaining): cover includes interaction and proxied target

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 docs(chaining): document struts.chaining.requireAnnotations

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 test(chaining): cover fail-closed introspection; clarify target==action

Add a test asserting nothing is copied when the target action cannot be
introspected (fail-closed), and document why isAuthorized is called with
target == action for chaining (no ModelDriven exemption).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5631 fix(chaining): address SonarCloud findings

- Mark injected parameterAuthorizer/ognlUtil fields transient (S1948);
  they are re-injected by the container, not serialized.
- Extract per-object copy into copyObjectToAction so the copyStack loop
  uses no break/continue (S135); fail-closed path now returns from the
  helper instead of continuing the loop.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 13:19:43 +02:00
Lukasz Lenart 419fb1f5c6 WW-5535 test(core): cover wildcard-resolved unannotated methods via real proxy (#1692)
Closes the test gap noted in the WW-5535 research: no integration test
exercised HttpMethodInterceptor against a real DefaultActionProxy resolving
a wildcard action with an unannotated method.

Uses xwork-test-allowed-methods.xml's existing <action name="Wild-*"
method="{1}"> on HttpMethodsTestAction. URL "Wild-execute" resolves to
ActionSupport.execute() (no method-level HTTP annotation); the class-level
@AllowedHttpMethod(POST) must still reject GET end-to-end.

Together with the prior MockActionProxy regression tests, this locks in
both halves of the fix:
- DefaultActionProxy.resolveMethod() sets isMethodSpecified()=true for
  wildcard-resolved methods (WW-5535 / #1592)
- HttpMethodInterceptor falls back to class-level annotations when the
  resolved method is unannotated (#1690)
2026-05-19 08:26:49 +00:00
ⳕⲛτⲉⲅⲥⲉⳏτⲟⲅ 🕵🏻 213b83f64f fix(core): enforce class-level HTTP method annotations for wildcard-resolved unannotated methods (#1690)
The WW-5535 fix (commit 4d2eb93) corrected isMethodSpecified() for wildcard-resolved
methods but introduced a structural gap in HttpMethodInterceptor.intercept().

The if/else-if structure made the class-level annotation check unreachable whenever
isMethodSpecified()=true and the resolved method carries no method-level annotation:

  if (isMethodSpecified()) {
      if (isAnnotatedBy(method)) { ... }
      // falls through silently
  } else if (isAnnotatedBy(class)) { ... }  // never reached
  return invocation.invoke();               // no enforcement

Fix: convert else-if to standalone if so the class-level check is always evaluated
as a fallback when the method itself has no annotation. Method-level annotations
still take precedence (checked first).

Add two regression tests covering the wildcard-resolved unannotated method scenario.

Co-authored-by: g0w6y <g0w6y@users.noreply.github.com>
2026-05-19 08:05:21 +02:00
Lukasz Lenart e83487f0e1 WW-5627 Gate CookieInterceptor through ParameterAuthorizer (#1681)
* WW-5627 add ParameterAllowlister interface and STRUTS_PARAMETER_ALLOWLISTER constant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5627 add OgnlParameterAllowlister default implementation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5627 register ParameterAllowlister bean in struts-default DI

* WW-5627 delegate ParametersInterceptor OGNL allowlisting to OgnlParameterAllowlister

Also register ParameterAllowlister in DefaultConfiguration bootstrap
factories so it is available in test containers (parallel to how
ParameterAuthorizer was already registered there).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5627 test(cookie): failing test for unannotated setter skip

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5627 gate CookieInterceptor cookie injection through ParameterAuthorizer

Adds a 5-arg `populateCookieValueIntoStack(name, value, map, stack, action)` hook
that runs cookie writes through `ParameterAuthorizer.isAuthorized` and primes
`ThreadAllowlist` via `ParameterAllowlister` for nested paths, then delegates
to the legacy 4-arg form. The 4-arg form is `@Deprecated(since="7.2.0")` but
its body is unchanged, so existing subclass overrides automatically receive
only authorized cookies. Default-config behavior is preserved because the
authorizer short-circuits when `requireAnnotations=false`.

Existing `CookieInterceptorTest` instantiates `new CookieInterceptor()` rather
than going through the container, leaving the new injected fields null. Wires
explicit pass-through lambdas through a `disableAuthorizationGate(...)` helper
so those tests continue to exercise default-config behavior.

* WW-5627 cover CookieInterceptor authorization matrix in CookieInterceptorAnnotationTest

* WW-5627 docs(cookie): document new 5-arg extension hook and deprecation

* WW-5627 wire OgnlParameterAllowlister in StrutsParameterAnnotationTest fixture

* WW-5627 address SonarCloud findings on PR #1681

- S1948: mark transient on the new ParameterAuthorizer/ParameterAllowlister
  fields in CookieInterceptor and ParametersInterceptor (the host classes
  are Serializable; the injected services are not).
- S1874: suppress the deprecation warning on the new 5-arg
  populateCookieValueIntoStack — the delegation to the deprecated 4-arg
  form is the contract that lets existing subclass overrides participate.
- S3776: extract `allowlistViaPropertyDescriptor` and
  `allowlistViaPublicField` from `OgnlParameterAllowlister.allowlistAuthorizedPath`
  to drop cognitive complexity below the threshold.
- S1068: remove the unused `mapping` test fixture field.

* WW-5627 clarify ParameterAllowlister contract and tidy ParametersInterceptor

Rename `ParameterAllowlister#allowlistAuthorizedPath` to `primeAllowlistForPath`
to make the contract explicit: the SAM is a side-effect-only priming hook that
runs after `ParameterAuthorizer#isAuthorized` has already decided. A no-op
return means "no priming needed or possible", never "rejected". The interface
name stays channel-agnostic; only the impl class (`OgnlParameterAllowlister`)
binds the priming to OGNL's `ThreadAllowlist`.

Add a `LOG.debug` in `OgnlParameterAllowlister` for the case where authorization
passed but no `@StrutsParameter` could be located on the root property
(e.g. `ModelDriven` models without per-property annotations) so the
authorize-vs-prime gap is observable instead of surfacing later as an opaque
OGNL traversal failure.

Drop the dead `performOgnlAllowlisting` pass-through and its unused `paramDepth`
parameter from `ParametersInterceptor` — the depth check is already enforced
inside `OgnlParameterAllowlister.primeAllowlistForPath`, so the outer guard was
a redundant computation.

No behavior change.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 13:52:17 +02:00
Lukasz Lenart 09d03286f8 WW-5626 per-property authorization for Jackson REST handlers (#1674)
* WW-5626 spike: validate Jackson per-property authorization mechanism

Validates that the Approach C design is feasible before committing to a detailed
implementation plan. Wraps each SettableBeanProperty via BeanDeserializerModifier;
intercepts deserializeAndSet to authorize against a path built from a ThreadLocal
Deque; uses skipChildren() to discard unauthorized values; uses [0] suffix for
collection/map/array elements to match ParametersInterceptor depth semantics.

Findings:
- Delegating base class via 'protected delegate' field is the right pattern
- addOrReplaceProperty(prop, true) is the correct builder API
- Reject-at-parent skips all nested deserialization (better security than two-phase
  copy: setter side effects on unauthorized properties never fire)
- JavaType#isCollectionLikeType/isMapLikeType/isArrayType detects the indexed-path case

Spike is kept under .../spike/ as a learning artifact; it will be replaced by
production code + tests in subsequent commits.

* WW-5626 add ParameterAuthorizationContext for deserializer-level authorization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5626 address review feedback on ParameterAuthorizationContext

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5626 add AuthorizationAwareContentTypeHandler marker interface

* WW-5626 add AuthorizingSettableBeanProperty for Jackson per-property authorization

* WW-5626 add ParameterAuthorizingModule installing the property wrapper on Jackson mappers

* WW-5626 register ParameterAuthorizingModule on default Jackson REST handlers

* WW-5626 use AuthorizationAwareContentTypeHandler path when handler supports it

* WW-5626 add integration tests proving the new Jackson authorization path is used

* WW-5626 deprecate XStreamHandler in favor of JacksonXmlHandler

* WW-5626 remove Jackson auth spike; replaced by production tests

* WW-5626 make JuneauXmlHandler authorization-aware via post-parse walk

Implements AuthorizationAwareContentTypeHandler. When ParameterAuthorizationContext
is active (set by ContentTypeInterceptor when requireAnnotations=true), the handler
walks the parsed result tree and copies only authorized properties to the target,
descending into nested beans/collections/maps/arrays with indexed-path semantics
([0] suffix) for parity with ParametersInterceptor.

Note: Juneau parses the entire result tree before our walk runs, so setter side
effects on transient nested objects can fire even for unauthorized properties —
those transient objects are then discarded. This is functionally equivalent to the
legacy two-phase copy in ContentTypeInterceptor; only the Jackson handlers achieve
the stronger guarantee where unauthorized subtrees are never instantiated at all
(they use Jackson's BeanDeserializerModifier + skipChildren).

When no context is bound (default config), behavior is unchanged: parser.parse +
BeanUtils.copyProperties.

* WW-5626 add JuneauXmlHandler integration tests for @StrutsParameter authorization

* WW-5626 test(rest): cover JuneauXmlHandler post-parse walk for collections, maps, arrays

Sonar reported 51 uncovered new lines in JuneauXmlHandler (48.8% coverage on the
post-parse authorization walk — the security-critical code path the branch exists
to introduce). Add integration coverage for the previously-uncovered branches:

- collection-of-scalars (List<String> tags)
- collection-of-beans (List<Address> addresses)
- map-of-scalars (Map<String,String> attributes)
- array-of-scalars (String[] aliases)
- empty collection
- malformed XML wrapped as IOException

Also drop two unnecessary casts (Sonar S1905) on lines 243/252 — the unchecked
conversion happens at the return statement, the explicit casts were redundant
under the existing @SuppressWarnings("unchecked").

Add @Override on the inline AnyConstraintMatcher.matches override (Sonar S1161).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5626 test(rest): cover AuthorizingSettableBeanProperty builder-path deserialization

Sonar reported 11 uncovered new lines on AuthorizingSettableBeanProperty (66.7%
coverage). All 11 are in deserializeSetAndReturn — the alternate Jackson entry
point used for builder-pattern deserialization, never triggered by setter-based
fixtures like Person.

Add an @JsonDeserialize(builder=...) fixture (ImmutablePerson) that forces
Jackson to use BuilderBasedDeserializer, which dispatches property writes
through deserializeSetAndReturn. Three new tests exercise the path:
inactive-context pass-through, top-level authorization, and full rejection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* WW-5626 refactor(rest): extract helpers from ContentTypeInterceptor.intercept

Sonar S3776 flagged intercept() at cognitive complexity 16 (limit 15). Extract
the body-handling branches into named helpers:

- openBodyReader: encoding-aware reader from the request InputStream
- applyRequestBody: dispatcher between requireAnnotations on/off paths
- applyWithAuthorizationContext: bind + delegate + unbind for AuthorizationAware handlers
- applyTwoPhaseDeserialize: legacy fresh-instance + copyAuthorizedProperties path

intercept() drops to ~12 lines and reads as a flat sequence: resolve target,
delegate body application, invoke. Each helper carries the comment that
explains the security model for its branch.

Add @Override on the inline AnyConstraintMatcher.matches override (Sonar S1161).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 15:01:00 +02:00
Lukasz Lenart 690c4c2737 WW-5626 cleanup follow-ups for @StrutsParameter JSON/REST enforcement (#1673)
* WW-5626 add ParameterAuthorizer#resolveTarget for centralized ModelDriven resolution

Move the ValueStack peek logic that derives the target object from action+ModelDriven
state out of ParametersInterceptor and into ParameterAuthorizer. Callers that need
both authorization and the resolved target (for downstream OGNL allowlisting) can
now call resolveTarget once and reuse the result.

* WW-5626 delegate ModelDriven target resolution to ParameterAuthorizer

Replace the inline ValueStack peek in ParametersInterceptor#isParameterAnnotatedAndAllowlist
with a call to ParameterAuthorizer#resolveTarget. The ModelDriven import is no longer
needed in this class.

* WW-5626 defensively skip non-String JSON keys in authorization filter

The (String) cast in filterUnauthorizedKeysRecursive threw ClassCastException
for any custom JSONReader producing non-String keys. Replace with an instanceof
pattern that debug-logs and skips entries whose key cannot be converted to a
parameter path.

* WW-5626 add real JacksonJsonHandler integration tests for @StrutsParameter filtering

The existing ContentTypeInterceptorTest uses mock ContentTypeHandlers, so its
requireAnnotations=true tests verify only that intercept() returns SUCCESS — they
assert nothing about which properties were actually filtered. These integration
tests use a real JacksonJsonHandler + a real StrutsParameterAuthorizer to verify
end-to-end property-level filtering for top-level annotated/unannotated properties
and nested properties at varying authorized depths.

The SecureRestAction fixture documents a semantic divergence: REST's recursive
copy authorizes each path level independently, so depth-0 authorization on the
top-level property requires @StrutsParameter on the setter even when nested
field access is the actual goal. ParametersInterceptor only requires the getter
annotation. This divergence is tracked for the Approach C refactor.

* WW-5626 make ParameterAuthorizer#resolveTarget a default method to preserve SAM

Making resolveTarget abstract broke ParameterAuthorizer as a functional interface,
which the existing JSON and REST plugin tests rely on for lambda-based stubs:
  interceptor.setParameterAuthorizer((parameterName, target, action) -> true);

The default returns the action unchanged — adequate for lambda test stubs whose
authorization decisions don't depend on the resolved target. The production
implementation (StrutsParameterAuthorizer) overrides this with the proper
ModelDriven value-stack peek.
2026-05-09 12:09:03 +02:00
quactv c3a887085d WW-5624: Enforce @StrutsParameter on JSON/REST body deserialization (#1657)
* WW-5624 fix(security): enforce @StrutsParameter on JSON/REST body deserialization

Extract ParameterAuthorizer service from ParametersInterceptor to share
@StrutsParameter annotation enforcement across all input channels.

The json-plugin (JSONInterceptor) and rest-plugin (ContentTypeInterceptor)
previously bypassed @StrutsParameter checks when deserializing request
bodies, allowing mass assignment even when
struts.parameters.requireAnnotations=true.

Changes:
- New ParameterAuthorizer interface and DefaultParameterAuthorizer impl
- JSONInterceptor: filter unauthorized Map keys before populateObject()
- ContentTypeInterceptor: two-phase deserialization (fresh instance then
  copy authorized properties) when requireAnnotations=true; direct
  deserialization for backward compat when disabled
- OGNL ThreadAllowlist side effects remain in ParametersInterceptor only
- Full DI wiring: struts-beans.xml + StrutsBeanSelectionProvider +
  DefaultConfiguration
- 15 new unit tests for ParameterAuthorizer, 2 for JSON plugin,
  2 for REST plugin; 32 existing regression tests verified

* WW-5624 address review feedback from lukaszlenart on PR #1657

1. Rename DefaultParameterAuthorizer → StrutsParameterAuthorizer
   per Struts naming convention (inline suggestion)

2. Narrow ModelDriven exemption: require action instanceof ModelDriven
   before exempting target from @StrutsParameter checks. Prevents
   non-ModelDriven root objects (e.g. JSONInterceptor.root) from
   bypassing annotation enforcement.

3. Recursive JSON key filtering: filterUnauthorizedKeys() now recurses
   into nested Maps and Lists, building dot-notation paths (e.g.
   "address.city") for path-aware @StrutsParameter(depth=N) checks.

4. Deep REST property copy: copyAuthorizedProperties() now recurses
   into nested bean types with path-aware authorization. Collections,
   Maps, primitives, and java.lang/java.time types are copied directly.

5. Null-skip semantics preserved and documented: in two-phase
   deserialization, null in freshInstance is indistinguishable from
   "not present in request" — clearing would destroy pre-initialized
   fields. Kept as intentional design choice with inline documentation.

6. No-arg constructor fallback: when target class lacks a no-arg
   constructor, falls back to single-phase deserialization with
   post-scrub of unauthorized properties, preserving backward compat.

7. New regression tests:
   - Non-ModelDriven target with different object (must not exempt)
   - Nested JSON keys recursively filtered
   - Non-action root object still checked by authorizer

All 280+ core tests, 124 JSON tests, 76 REST tests pass with 0 regressions.

* WW-5624: v3 — fix indexed-path depth parity with ParametersInterceptor

Four gaps identified by lukaszlenart's April 10 review are now fully addressed:

1. JSON filterUnauthorizedList: pass prefix+"[0]" instead of bare prefix so
   that list element properties gain one extra '[' in their path — e.g.
   "publicPojoListDepthOne[0].key" (depth=2) is now correctly rejected when
   @StrutsParameter(depth=1), matching ParametersInterceptor semantics.
   Also recurse into nested List<List<Map>> via an else-if branch.

2. REST copyAuthorizedProperties: add authTarget parameter (always = root
   action/model, passed unchanged through all recursion levels).
   isAuthorized() now checks the full path against the root class, so
   "address.city" is looked up on the action, not on the Address object.

3. REST Collection/Map/array deep authorization: replaced the as-is copy
   with deepCopyAuthorizedCollection(), deepCopyAuthorizedMap(), and
   deepCopyAuthorizedArray() helpers — each iterates elements with
   path+"[0]" prefix, authorizing every complex element individually.
   No-arg fallback skips the element rather than copying an unfiltered
   object graph (security fix over plan's original as-is suggestion).

4. REST scrubUnauthorizedProperties: now fully recursive via
   scrubUnauthorizedPropertiesRecursive() — visits nested beans,
   collection elements, and map values with authTarget always pointing
   to the root. Includes identity-based visited-set to guard against
   circular reference cycles.

Tests: core 2920 + json 124 + rest 76 = 3120, 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5624: v3.1 — fix collection type, identity set, isNestedBeanType coverage

Three correctness/security issues identified by independent review:

1. deepCopyAuthorizedCollection/deepCopyAuthorizedMap type preservation:
   Previously always returned ArrayList/LinkedHashMap. If the action field
   is typed Set<Pojo> or SortedMap<K,V>, writeMethod.invoke would throw
   IllegalArgumentException. Now: SortedSet→TreeSet, Set→LinkedHashSet,
   List→ArrayList; SortedMap→TreeMap, Map→LinkedHashMap.

2. scrubUnauthorizedPropertiesRecursive visited-set identity safety:
   Replaced Set<Integer>+System.identityHashCode (not collision-safe) with
   Collections.newSetFromMap(new IdentityHashMap<>()) which uses reference
   equality (==). A hash collision could have caused a valid nested object
   to be skipped, leaving unauthorized properties un-scrubbed.

3. isNestedBeanType now excludes all standard-library leaf packages:
   java.util.* non-Collection/Map types (UUID, Currency, Locale, Date),
   java.time.* (all temporal types, not just Temporal subinterface),
   java.net.*, java.io.*, java.nio.*. Previously UUID etc. would return
   true, causing the code to recurse into their internal fields and silently
   drop the value when no @StrutsParameter annotation matched.

Tests: json 124 + rest 76 = 200, 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* WW-5624: v4 — close bulk-copy fallback, reject body when no no-arg ctor

Two remaining gaps addressed per lukaszlenart's April 11 review:

1. copyAuthorizedProperties bulk-copy fallback removed:
   When a nested target bean is null and createFreshInstance fails (no
   no-arg constructor), the previous code fell back to
   writeMethod.invoke(target, sourceValue) — copying the whole nested
   object graph without per-path authorization. Now logs a warning and
   skips the property entirely (same policy as deepCopyAuthorizedCollection
   elements with no no-arg constructor).

2. Top-level no-arg constructor fallback changed from scrub to reject:
   When requireAnnotations=true and the target class has no no-arg
   constructor, body deserialization is now rejected entirely
   (handler.toObject is never called). The previous best-effort scrub
   path could not guarantee that all nested unauthorized properties were
   nulled out. scrubUnauthorizedProperties and its recursive helper are
   removed as dead code.

Tests: rest 76, 0 failures.

---------

Co-authored-by: tranquac <tranquac@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 10:46:38 +02:00
quactv 8d6f13904f fix(core): WW-5623 HTML-encode form action in PostbackResult to prevent XSS (#1653)
* fix(core): HTML-encode form action in PostbackResult to prevent XSS

PostbackResult.doExecute() embeds finalLocation into a <form action="">
attribute via raw string concatenation without HTML encoding. A double
quote in the location breaks out of the attribute, enabling reflected
XSS. The response Content-Type is text/html (line 103).

This is an encoding inconsistency: form field names and values at lines
218-219 ARE properly URL-encoded via URLEncoder.encode(), but the form
action attribute was not encoded at all.

Add encodeHtml() to escape &, ", <, > in finalLocation before embedding
it in the HTML form tag, consistent with the existing encoding approach
for form field values in the same class.

* fix(core): WW-5623 use StringEscapeUtils and add regression tests

Address review feedback from @lukaszlenart:

- Replace custom encodeHtml() with StringEscapeUtils.escapeHtml4()
  for consistency with the rest of Struts core (DefaultActionProxy,
  Property, TextProviderHelper all use StringEscapeUtils)
- Add 3 focused unit tests in PostbackResultTest:
  - testFormActionHtmlEscaping: XSS payload with attribute breakout
  - testFormActionEscapesAllHtmlSpecialChars: covers ", &, <, >
  - testFormActionCleanLocationUnchanged: regression for clean URLs

---------

Co-authored-by: tranquac <tranquac@users.noreply.github.com>
2026-05-01 10:42:25 +02:00
aaaZayne f4c6349283 introduce private method to remove clones (#1666)
* introduce private method to remove clones

* Update naming
2026-04-20 04:57:28 +00:00
Lukasz Lenart d276b2dded WW-5622 perf(core): optimize Hibernate proxy detection when Hibernate is absent (#1649)
Detect Hibernate availability once at class-load time via Class.forName()
and short-circuit all Hibernate-related methods immediately when absent.
This eliminates repeated LinkageError/NoClassDefFoundError exceptions
that cause significant performance degradation in applications without
Hibernate on the classpath.

Fixes https://issues.apache.org/jira/browse/WW-5622

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 18:30:44 +02:00
Lukasz Lenart 620fcbd152 WW-5621 Harden XML parsers against Entity Expansion (Billion Laughs) attacks (#1642)
Modern JDKs (7u45+) already protect against this attack with a built-in
64K entity expansion limit. These changes add defense-in-depth hardening
and remove unnecessary attack surface.

- Remove unused parseStringAsXML feature from StringAdapter to eliminate
  a theoretical XML Entity Expansion vector
- Deprecate setParseStringAsXML() and getParseStringAsXML() for removal
- Enable SECURE_PROCESSING feature in DigesterDefinitionsReader
- Add unit test verifying JDK's entity expansion limit rejects
  Billion Laughs payloads
- Add research document with vulnerability analysis

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-04 11:20:33 +02:00
Lukasz Lenart 2215b6873c WW-5537 Resolve classloader/memory leaks during Tomcat hot deployment (#1632)
* WW-5537 Add InternalDestroyable and ContextAwareDestroyable interfaces

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 ContainerHolder: ThreadLocal with AtomicLong generation counter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 FinalizableReferenceQueue: volatile instance, join, classloader null

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 ScopeInterceptor.clearLocks: add synchronized block

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 CompoundRootAccessor, DefaultFileManager: implement InternalDestroyable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Add InternalDestroyable adapter classes for static cache cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Register InternalDestroyable beans in struts-beans.xml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 JSON plugin: add JSONCacheDestroyable for BeanInfo cache cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Dispatcher.cleanup: refactor into focused methods with InternalDestroyable discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Rewrite DispatcherCleanupTest for InternalDestroyable discovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Add log4j-web for proper Log4j2 lifecycle in Servlet container

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Dispatcher.destroyObjectFactory: add early return on null, use pattern matching

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Fix @since annotations: 7.1.0 -> 7.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Add Container.destroy() to clear internal caches on undeploy

Container now exposes a destroy() method that clears factories, injectors,
constructors, and ThreadLocals. This releases Class<?> keys and JDK
DelegatingClassLoader instances that pin the webapp classloader.

DefaultConfiguration.destroy() calls container.destroy() and
reloadContainer() delegates to destroy() to avoid duplication.

Also fixes JSONCacheDestroyable referencing non-existent DefaultJSONWriter
(renamed to StrutsJSONWriter).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Fix Container.destroy(): don't clear factories, don't call from reloadContainer

factories must remain intact because existing code holds direct
references to the Container after destroyConfiguration() and expects
it to still resolve dependencies (e.g. during configuration reload).

reloadContainer() reverted to clearing packageContexts/loadedFileNames
directly — calling destroy() there nulled the container reference and
cleared state needed during the bootstrap transition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Restore destroy() call in reloadContainer()

The test failures were caused by factories.clear() in
Container.destroy(), not by calling destroy() from reloadContainer().
Now that factories.clear() is removed, destroy() is safe to call
here — it clears packageContexts, loadedFileNames, and the container's
reflection caches in one place.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* WW-5537 Fix Sonar issues: thread-safe FinalizableReferenceQueue, empty method comments

- Replace volatile field with AtomicReference in FinalizableReferenceQueue
  for proper thread safety using getAndSet()
- Add comments to empty destroy() implementations in test mocks
- Replace deprecated new URL() with URI.toURL() in DispatcherCleanupTest
- Add comments to empty listener methods in DispatcherCleanupTest

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 07:17:57 +02:00
Lukasz Lenart 55e268b009 WW-2963 default-action-ref fails to find wildcard named actions (#1614)
* WW-2963 fix(core): resolve default-action-ref via wildcard matching

When default-action-ref names an action that only exists as a wildcard
pattern (e.g., "movie-list" matching "movie-*"), the fallback now tries
wildcard matching after the exact map lookup fails. This mirrors the
exact→wildcard resolution already used for request action names.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* WW-2963 refactor(core): reduce cognitive complexity of findActionConfigInNamespace

Extract default-action-ref resolution into findDefaultActionConfig() and
replace the deeply nested if-pyramid with early returns, reducing the
nesting depth from 5 to 1 to satisfy Sonar's complexity threshold.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Made-with: Cursor

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 16:29:01 +01:00
Lukasz Lenart 4c94c4f89a WW-5549 Fix I18nInterceptor supportedLocale breaking request_locale (#1594)
* fix(i18n): ensure request_locale takes precedence over Accept-Language when supportedLocale is configured

When supportedLocale was configured on the I18nInterceptor, the Accept-Language
header match in AcceptLanguageLocaleHandler.find() returned early before
SessionLocaleHandler/CookieLocaleHandler ever checked their explicit locale
parameters (request_locale, request_cookie_locale). This made it impossible
to switch locale via request parameters when supportedLocale was set.

Changes:
- Reorder AcceptLanguageLocaleHandler.find() to check request_only_locale
  before Accept-Language matching
- Reorder SessionLocaleHandler.find() to check request_locale before super
- Reorder CookieLocaleHandler.find() to check request_cookie_locale before super
- Add isLocaleSupported() helper to validate locales against supportedLocale
- Filter all locale sources (params, session, cookies) through supportedLocale
- Add 4 tests covering the bug scenario and supportedLocale filtering

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(i18n): cover missing supportedLocale locale-selection paths

Add regression tests for unsupported request_cookie_locale fallback, stored cookie revalidation, and request_only_locale precedence to lock in WW-5549 behavior across remaining branches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(i18n): extract locale handlers with deprecated inner wrappers

Move locale handler implementations into a dedicated interceptor.i18n package with reusable abstract bases, keep thin deprecated inner wrappers in I18nInterceptor for one release-cycle compatibility, and document the LocaleHandler contract.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(i18n): validate request_only_locale against supportedLocale and fix Accept-Language fallback

RequestLocaleHandler.find() now checks isLocaleSupported() before
returning, preventing unsupported locales from slipping through via
the request_only_locale parameter. AcceptLanguageLocaleHandler.find()
now returns the first Accept-Language locale when supportedLocale is
empty, fixing ACCEPT_LANGUAGE storage mode with no filter configured.

Also includes refactoring: deprecated inner classes collapsed with
LocaleHandlerAdapter, shouldStore field encapsulated via disableStore(),
logger pattern standardized to private static final, and class-level
JavaDoc added to handler classes.

Made-with: Cursor

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-03-06 07:50:06 +01:00
Lukasz Lenart 4d2eb93835 fix(core): correct isMethodSpecified() for wildcard-resolved methods (#1592)
DefaultActionProxy.resolveMethod() unconditionally set methodSpecified=false
when the method was not passed explicitly, including when it was resolved from
ActionConfig (e.g., wildcard substitution like method="{1}"). This caused
HttpMethodInterceptor to skip method-level annotation checks for wildcard
actions, falling back to class-level annotations instead.

Move methodSpecified=false inside the inner branch that defaults to "execute",
so config-resolved methods (including wildcard-substituted ones) correctly
report isMethodSpecified()=true. Update Javadoc to reflect the corrected
semantics.

Fixes [WW-5535](https://issues.apache.org/jira/browse/WW-5535)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 13:25:38 +01:00
Lukasz Lenart ca740ed8fb WW-5514 Add StrutsProxyService for proxy detection and resolution (#1586)
* feat(proxy): WW-5514 add StrutsProxyService for proxy detection and resolution

Introduces a configurable ProxyService interface and StrutsProxyService
implementation for detecting and resolving Spring AOP/Hibernate proxies.

Key changes:
- Add ProxyService interface with isProxy, ultimateTargetClass, and
  resolveTargetMember methods
- Add StrutsProxyService implementation using configurable caches
- Add ProxyCacheFactory and StrutsProxyCacheFactory for cache management
- Integrate ProxyService into ChainingInterceptor, ParametersInterceptor,
  and SecurityMemberAccess
- Add integration test with Spring AOP proxied action chaining
- Add configuration constants for proxy cache type and size

The StrutsProxyService correctly handles:
- Spring CGLIB proxies (class-based)
- Spring JDK dynamic proxies (interface-based)
- Hibernate entity proxies
- Member resolution for allowlist checking

Fixes WW-5514

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(proxy): WW-5514 add ProxyService integration tests for Spring proxies

Add integration tests to SpringProxyUtilTest that verify the new
ProxyService works correctly with real Spring AOP proxies, alongside
the existing deprecated ProxyUtil tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(proxy): WW-5514 address PR review feedback for proxy caches

Remove targetClassCache from StrutsProxyService to avoid memory leak
(object-keyed cache reintroduced from PR #1578). Change default proxy
cache type to wtlfu to align with all other caches. Switch deprecated
ProxyUtil static caches to BASIC to remove hard Caffeine dependency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-21 18:18:08 +01:00
dependabot[bot] d2810d42f0 build(deps): bump org.apache.commons:commons-fileupload2-jakarta-servlet6 (#1584)
Bumps org.apache.commons:commons-fileupload2-jakarta-servlet6 from 2.0.0-M4 to 2.0.0-M5.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-fileupload2-jakarta-servlet6
  dependency-version: 2.0.0-M5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 17:17:39 +00:00
Lukasz Lenart c90bb70c25 feat(ui): WW-3429 add configurable checkbox hidden field prefix (#1570)
Add struts.ui.checkbox.hiddenPrefix constant to allow configuring
the checkbox hidden field prefix, addressing HTML validation warnings
about double underscores while maintaining backward compatibility.

Changes:
- Add STRUTS_UI_CHECKBOX_HIDDEN_PREFIX constant to StrutsConstants
- Add default value __checkbox_ to default.properties
- Update Checkbox component to inject and pass prefix to templates
- Update CheckboxInterceptor to use configurable prefix
- Update simple/checkbox.ftl and html5/checkbox.ftl templates
- Update CheckboxHandler in javatemplates plugin
- Add tests for configurable prefix functionality
- Fix bug in CheckboxHandler where value was incorrectly prefixed

Configuration example:
  struts.ui.checkbox.hiddenPrefix=struts_checkbox_

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 07:14:03 +01:00
Lukasz Lenart a0a213f7c5 feat(security): WW-5294 add warning when JSP tags accessed directly (#1569)
Add security warning to TagUtils.getStack() that logs when JSP tags
are rendered outside of action scope (direct JSP access). This helps
developers identify potential security issues where JSPs are accessed
directly without going through the Struts action flow.

The warning message includes a link to the security documentation at
https://struts.apache.org/security/#never-expose-jsp-files-directly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 07:13:33 +01:00
dependabot[bot] e3d09dfc47 WW-5613 build(deps): bump ognl:ognl from 3.4.8 to 3.4.10 (#1567)
OgntUtil has been extended to properly pass root object if needed

Bumps [ognl:ognl](https://github.com/orphan-oss/ognl) from 3.4.8 to 3.4.10.
- [Release notes](https://github.com/orphan-oss/ognl/releases)
- [Commits](https://github.com/orphan-oss/ognl/commits)

---
updated-dependencies:
- dependency-name: ognl:ognl
  dependency-version: 3.4.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-17 07:13:05 +01:00
Kusal Kithul-Godage 11bc215c94 WW-5614 Remove cache for ProxyUtil#ultimateTargetClass 2026-02-12 12:46:17 +11:00
Lukasz Lenart fd87425863 fix(spring): WW-3647 change autowire alwaysRespect default to true (#1571)
Change the default value of struts.objectFactory.spring.autoWire.alwaysRespect
from false to true to fix the Spring constructor autowiring issue.

When a Spring String bean exists (e.g., JNDI lookup with default-value),
Spring's AUTOWIRE_CONSTRUCTOR strategy incorrectly injects that value into
ALL String parameters of ServletActionRedirectResult constructors, causing
malformed redirect URLs.

Setting alwaysRespect to true by default ensures the configured autowire
strategy (AUTOWIRE_BY_NAME) is consistently used, preventing unintended
bean injection.

Users who rely on the legacy constructor autowiring behavior can restore
it by setting:
<constant name="struts.objectFactory.spring.autoWire.alwaysRespect" value="false" />

Fixes https://issues.apache.org/jira/browse/WW-3647

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-09 10:08:33 +02:00
Lukasz Lenart 720e603d2b feat(conversion): WW-4291 allow Spring bean names for type converters (#1562)
Implement two-phase processing for conversion properties to enable
Spring bean name resolution in struts-conversion.properties files.

The issue was a timing problem: type converters were processed during
bootstrap phase before SpringObjectFactory was available. Now:
- Early phase: process struts-default-conversion.properties (class names)
- Late phase: process user properties when SpringObjectFactory is ready

Changes:
- Add UserConversionPropertiesProvider interface for late initialization
- Add UserConversionPropertiesProcessor to trigger late phase processing
- Split StrutsConversionPropertiesProcessor.init() into early/late phases
- Register new beans in DefaultConfiguration and struts-beans.xml
- Add alias in StrutsBeanSelectionProvider for dependency injection
- Improve JavaDocs for BeanSelectionProvider classes

Closes WW-4291

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-06 07:43:55 +01:00
Lukasz Lenart 22b0fa9f12 chore: updates SNAPSHOT version to reflect current scope of changes (#1563) 2026-02-01 12:26:48 +00:00
dependabot[bot] b64cd2e4ac WW-5536 Bump ognl:ognl from 3.3.5 to 3.4.8 (#1405)
* Bump ognl:ognl from 3.3.5 to 3.4.8

Bumps [ognl:ognl](https://github.com/orphan-oss/ognl) from 3.3.5 to 3.4.8.
- [Release notes](https://github.com/orphan-oss/ognl/releases)
- [Commits](https://github.com/orphan-oss/ognl/commits)

---
updated-dependencies:
- dependency-name: ognl:ognl
  dependency-version: 3.4.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat(ognl): implement OGNL 3.4.8 compatibility changes

Implement comprehensive code changes to support OGNL 3.4.8 upgrade:

- Create StrutsContext wrapper extending OgnlContext for type-safe context operations
- Update 13 PropertyAccessor implementations: change Map context to OgnlContext
  (XWorkObjectPropertyAccessor, XWorkCollectionPropertyAccessor, XWorkMapPropertyAccessor,
  XWorkListPropertyAccessor, XWorkIteratorPropertyAccessor, XWorkEnumerationAccessor,
  ParameterPropertyAccessor, ObjectProxyPropertyAccessor, ObjectAccessor,
  HttpParametersPropertyAccessor, CompoundRootAccessor, XWorkMethodAccessor)
- Update TypeConverter implementations: OgnlTypeConverterWrapper, XWorkTypeConverterWrapper
- Update NullHandler implementation: OgnlNullHandlerWrapper
- Update SecurityMemberAccess interface methods to use OgnlContext
- Update createDefaultContext return type from Map to OgnlContext in OgnlUtil and OgnlReflectionContextFactory
- Fix OgnlUtil method calls with proper OgnlContext casting
- Fix OgnlReflectionProvider: remove obsolete exception handling
- Update CompoundRootAccessor: remove unnecessary exception handling

Breaking API changes in OGNL 3.4.8:
- PropertyAccessor: getProperty/setProperty methods now require OgnlContext instead of Map
- TypeConverter: convertValue method now requires OgnlContext and uses Class<?> generic
- NullHandler: nullMethodResult/nullPropertyValue methods now require OgnlContext
- Ognl.createDefaultContext: returns OgnlContext instead of Map
- OgnlRuntime methods: simplified signatures without OgnlContext where not needed

This commit addresses the binary-incompatible API changes introduced in OGNL 3.4.8
as detailed in the research document.

Relates to WW-5326

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ognl): update tests for OGNL 3.4.8 compatibility

- Update NullHandler implementations to use OgnlContext instead of Map
- Add explicit OgnlContext casts for Ognl.getValue() calls
- Fix isAccessible() method calls to use OgnlContext parameter
- Add OgnlContext imports where needed
- Update context variable types from Map to OgnlContext

This fixes compilation errors in test files after OGNL 3.4.8 upgrade.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): use OgnlContext instead of HashMap in SecurityMemberAccessTest

- Change context field from Map to OgnlContext to avoid ClassCastException
- Initialize context using Ognl.createDefaultContext() instead of HashMap
- Remove unnecessary casts since context is now OgnlContext

This fixes runtime ClassCastException: HashMap cannot be cast to OgnlContext

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(test): use OgnlContext in SecurityMemberAccessInServletsTest

- Change context field from Map to OgnlContext
- Initialize using Ognl.createDefaultContext() to avoid ClassCastException
- Remove unnecessary casts since context is now OgnlContext

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(ognl): add ensureOgnlContext for backward compatibility

Add ensureOgnlContext() helper method to handle cases where HashMap
is passed instead of OgnlContext. This provides backward compatibility
for code that still passes plain Map objects to setProperties() and
setProperty() methods.

The method checks if the context is already an OgnlContext and returns
it as-is, otherwise creates a new OgnlContext and copies the Map contents.

This fixes ClassCastException errors in validation interceptor tests where
legacy code passes HashMap contexts during validator initialization.

Fixes:
- DefaultWorkflowInterceptorTest (12 tests)
- ValidationInterceptorPrefixMethodInvocationTest (2 tests)
- ValidationErrorAwareTest (2 tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ognl): temporarily disable testCustomOgnlMapBlocked

Disable testCustomOgnlMapBlocked test that fails with OGNL 3.4.8 due to
behavior changes in custom OGNL Map handling. Test needs investigation
to determine if it's a legitimate security issue or if the test needs
to be updated for OGNL 3.4.8 behavior.

Renamed method from testCustomOgnlMapBlocked to disabledTestCustomOgnlMapBlocked
to prevent JUnit from running it.

Test results: 2714 tests, 0 failures, 0 errors ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ognl): update spring and tiles plugins for OGNL 3.4.8

- Update SecurityMemberAccessProxyTest to use OgnlContext
- Update tiles PropertyAccessor implementations for new signatures
- Update tiles PropertyAccessor tests to use OgnlContext
- All property accessors now use OgnlContext instead of Map

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ognl): re-enable testCustomOgnlMapBlocked for OGNL 3.4.8

- Re-enable testCustomOgnlMapBlocked test that was temporarily disabled
- Update assertions to expect null instead of exception (OGNL 3.4.8 behavior)
- Add testDisallowCustomOgnlMapFlagExplicitlyEnabled to verify flag behavior

Custom map blocking now returns null instead of throwing OgnlException,
which is still secure behavior - the custom map instantiation is prevented.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ognl): use StrutsContext instead of OgnlContext

- Add StrutsContext.create() factory method with default configuration
- Update OgnlValueStack to use StrutsContext.create()
- Update OgnlUtil to use StrutsContext throughout
- Rename ensureOgnlContext() to ensureStrutsContext()
- Update XWorkTypeConverterWrapper to use StrutsContext
- Update DefaultTypeConverter to check for StrutsContext first
- Update OgnlReflectionContextFactory to return StrutsContext

This provides a Struts-specific context abstraction layer while
maintaining compatibility with OGNL 3.4.8+ API requirements.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Revert "refactor(ognl): use StrutsContext instead of OgnlContext"

This reverts commit ee7fdbd5bd.

* chore(ognl): remove unused StrutsContext class

The StrutsContext wrapper class is no longer used after reverting
the refactoring commit. Removing it to keep the codebase clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ognl): fix custom OGNL map security tests for OGNL 3.4.8

Rewrite tests for custom OGNL map security to properly verify behavior:

- testCustomOgnlMapBlockedByDisallowFlag: verifies disallowCustomOgnlMap
  flag blocks custom map class resolution (throws OgnlException)
- testCustomOgnlMapBlockedByAllowlist: verifies allowlist blocks method
  calls on non-allowlisted custom map classes (throws OgnlException)
- testCustomOgnlMapAllowedWhenSecurityDisabled: verifies custom maps
  work when both security layers are disabled

Key fixes:
- Use non-null root objects to avoid OGNL chain short-circuit behavior
- Explicitly configure security flags (test container doesn't load
  default.properties)
- Expect OgnlException when security blocks access, not silent null

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* nit: removes unneeded assigment

* nit: removes useless null check

* nit: removes misleading exception declaration on test methods

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lukasz Lenart <lukaszlenart@apache.org>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-28 12:48:53 +01:00
Lukasz Lenart 05003d237a fix(core): move xwork-default.xml to test resources (#1513)
The Struts IDEA plugin incorrectly displayed xwork-default.xml as a
framework configuration file. This was misleading since the file is
only used in testing and is not loaded by the framework by default.

Changes:
- Move xwork-default.xml from core/src/main/resources to
  core/src/test/resources and rename to struts-tests-default.xml
- Copy struts-tests-default.xml to plugins/spring/src/test/resources
- Update all test file references to use the new filename
- Update Javadoc examples to use modern Struts terminology
  (xwork -> struts, xwork-default -> struts-default)

Closes [WW-5603](https://issues.apache.org/jira/browse/WW-5603)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 10:23:53 +01:00
Lukasz Lenart dfc659d9be WW-5602 Fix StreamResult contentCharSet handling and refactor for extensibility (#1510)
* fix(core): WW-5602 fix StreamResult contentCharSet handling

- Evaluate contentCharSet expression before checking for emptiness
- Use StringUtils.isEmpty() for null/empty check on parsed value
- Call setCharacterEncoding(null) to clear Dispatcher's default encoding
- Set charset via setCharacterEncoding() instead of appending to content-type
- Add test for expression evaluating to null

Closes WW-5602

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(core): extract methods and modernize StreamResult

- Add constants: DEFAULT_BUFFER_SIZE, DEFAULT_CONTENT_TYPE,
  DEFAULT_CONTENT_DISPOSITION, DEFAULT_INPUT_NAME
- Extract resolveInputStream() for custom stream sources
- Extract applyResponseHeaders() for custom header handling
- Extract applyContentLength() for custom length calculation
- Extract streamContent() for custom streaming behavior
- Use try-with-resources for cleaner resource management
- Add JavaDoc explaining extensibility of each method

All extracted methods are protected to enable easy extension
by users creating custom streaming result types.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(core): resolve setCharacterEncoding ambiguity for Jakarta EE 11

Cast null to String to disambiguate between overloaded methods:
- setCharacterEncoding(String)
- setCharacterEncoding(Charset)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 19:18:07 +01:00
Lukasz Lenart 6d778ac9b7 fix(convention): WW-5594 exclude root package classes with wildcard patterns (#1468)
The exclusion pattern "org.apache.struts2.*" was not properly excluding
classes directly in the root package (like XWorkTestCase) because:

1. PackageBasedActionConfigBuilder extracts package names using
   substringBeforeLast(className, ".") which produces "org.apache.struts2"
   (no trailing dot)
2. The wildcard pattern requires a literal "." before "*"
3. Result: Pattern doesn't match root package classes

Fix: Enhanced checkExcludePackages() to automatically handle patterns
ending with ".*" by also checking if the package name equals the base
pattern (without ".*").

Now "org.apache.struts2.*" properly excludes both:
- Classes in root package: org.apache.struts2.XWorkTestCase
- Classes in subpackages: org.apache.struts2.dispatcher.SomeClass

Closes [WW-5594](https://issues.apache.org/jira/browse/WW-5594)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 19:41:59 +01:00
Lukasz Lenart e24d2f2d3e WW-5444 Add HTML5 theme with comprehensive unit tests (#1422)
* WW-5444 Defines new html5 theme

* feat(themes): move html5 theme from showcase to core

- Move 42 FreeMarker templates to core/src/main/resources/template/html5/
- Remove duplicate templates from showcase
- Makes html5 theme available to all applications via struts2-core.jar
- Enables unit testing in core module
- Fix Html5Action import from XWork2 to Struts2 package
- Add Apache license header to Html5Action

Resolves WW-5444

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add implementation plan for html5 theme migration

Add detailed implementation plan documenting the migration of html5 theme
from showcase to core, including step-by-step instructions, verification
procedures, and acceptance criteria.

Related to WW-5444

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(html5-theme): add comprehensive unit tests and fix template variables

- Add testGenericHtml5() methods to 13 UI tag test classes
- Create theme.properties with parent=simple configuration
- Migrate all 32 HTML5 templates from deprecated 'parameters' to 'attributes' variable
- Exclude DoubleSelect and UpDownSelect (not supported in HTML5 theme)
- All 26 HTML5 theme tests passing (100% success rate)

Components tested: Textfield, Textarea, Password, Checkbox, CheckboxList,
Radio, Select, File, Hidden, Label, ComboBox, Reset, Submit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: update CLAUDE.md and remove jakarta-migration-helper agent

- Update CLAUDE.md with current project guidelines
- Update test-runner agent documentation
- Remove deprecated jakarta-migration-helper agent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Adds missing header

* test(html5-theme): complete unit test coverage for remaining 5 tag classes

Add HTML5 theme tests to ActionErrorTagTest, ActionMessageTagTest,
FieldErrorTagTest, AnchorTest, and FormTagTest. This completes Phase 1
of the HTML5 theme testing plan.

Changes:
- Add 24 new HTML5 theme unit tests across 5 test classes
- Create 17 expected output files for HTML5 theme validation
- Test coverage: action errors, action messages, field errors, anchors, and forms
- All tests verify HTML5 clean markup without table-based layouts

Coverage: 18 of 18 UI tag test classes now have HTML5 tests (100%)
Tests passing: 24/24 HTML5 tests successful

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(html5-theme): add integration tests for showcase application

- Create Html5TagExampleTest with 5 comprehensive integration tests
- Add fielderror tag demonstration to html5/index.jsp
- Validate HTML5 theme rendering, error display, and clean markup
- All tests passing (5/5) with HtmlUnit and Jetty integration

Tests validate:
- Page load and HTTP status (200)
- Action errors, messages, and field errors display
- Clean semantic markup (ul lists, no table layouts)
- Anchor tag rendering
- Namespace isolation under /html5

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(html5-theme): WW-5444 replace FreeMarker compress with Struts compress tag

- Create CompressModel.java to enable <@s.compress> in FreeMarker templates
- Register compress tag in StrutsModels.java for @s namespace access
- Migrate all 29 HTML5 theme templates from <#compress> to <@s.compress>
- Maintain identical compression behavior (uses same >\\s+< regex pattern)
- All 153 HTML5 theme unit tests pass without modification

This change leverages the new configurable Compress component (WW-5256)
which provides devMode awareness and security protections while maintaining
the same whitespace removal behavior as FreeMarker's built-in directive.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat(html5-theme): WW-5444 simplifies tags templates and extends example

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-14 19:24:05 +01:00
Lukasz Lenart 8ee415f03d refactor(config): WW-5256 move compress constants to struts.tag.compress namespace (#1446)
Rename struts.compress.* configuration constants to struts.tag.compress.*
to align with established tag-specific configuration patterns (e.g.,
struts.tag.includetag.useResponseEncoding).

Changes:
- STRUTS_COMPRESS_ENABLED: struts.tag.compress.enabled
- STRUTS_COMPRESS_MAX_SIZE: struts.tag.compress.maxSize
- STRUTS_COMPRESS_LOG_MAX_LENGTH: struts.tag.compress.log.maxLength

The struts.tag.* prefix is used for tag-specific configuration, distinguishing
it from UI component configuration (struts.ui.*) and template engine
configuration (struts.freemarker.*).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-06 11:16:08 +01:00
Lukasz Lenart 9305a58123 WW-5256 Add configurable FreeMarker whitespace stripping and compress tag (#1418)
* WW-5256 Prepares a research paper to be used to prepare an implementation plan

* WW-5256 Implements dedicated tag to compress output

* feat(freemarker): WW-5256 add configurable whitespace stripping

- Add struts.freemarker.whitespaceStripping configuration option
- Automatically disable whitespace stripping in devMode
- Add @since 7.2.0 tags to new Compress component and configuration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(compress): WW-5256 add global compression configuration

- Add struts.compress.enabled configuration option
- Compress tag respects global setting unless force=true
- Add @since 7.2.0 tag to new constant

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(compress): WW-5256 add tests and documentation

- Add tests for global compression disabled setting
- Add test for force attribute overriding global setting
- Update compress tag documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(freemarker): WW-5256 add whitespace stripping tests

- Test whitespace stripping enabled by default
- Test whitespace stripping disabled via configuration
- Test whitespace stripping disabled in devMode
- Test whitespace stripping enabled when not in devMode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(compress): WW-5256 simplify whitespace compression implementation

Replace complex CompressWriter (125 lines) with simple regex-based compression
(15 lines). Remove bufferSize parameter as buffering is no longer needed when
processing strings directly. Update tests to remove buffer-related test cases.

- Remove CompressWriter inner class and state machine
- Add compressWhitespace() static method using regex
- Remove bufferSize field and setter from Compress and CompressTag
- Remove 3 buffer-related tests from CompressTest
- Remove 1 buffer-related test from CompressTagTest
- All remaining 19 tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(compress): WW-5256 add singleLine attribute documentation

Add documentation for the singleLine attribute in compress tag.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(security): add security enhancements to Compress component

Add configurable size limits and log truncation to prevent:
- DoS attacks via large input (configurable maxSize, default 10MB)
- Sensitive data exposure in logs (configurable logMaxLength, default 200 chars)
- ReDoS attacks via regex operations (50MB hard limit)

Adds new configuration constants:
- STRUTS_COMPRESS_MAX_SIZE for maximum compressible body size
- STRUTS_COMPRESS_LOG_MAX_LENGTH for maximum log content length

Includes implementation plan documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(compress): add security tests for Compress component

Add comprehensive test coverage for security enhancements:
- testMaxSizeLimit: verify size limit enforcement
- testMaxSizeDisabled: verify disabled limit behavior
- testLogTruncation: verify log truncation works correctly
- testVeryLargeInputSafety: verify 50MB hard limit protection
- testInvalidMaxSizeConfiguration: verify invalid config handling
- testInvalidLogMaxLengthConfiguration: verify invalid config handling

All tests use behavior-based assertions following existing patterns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(compress): add security section to compress tag documentation

Update @StrutsTag annotation description to include security considerations:
- DoS attack prevention via configurable size limits
- Sensitive data exposure prevention via log truncation

The description will be used to generate the site documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(compress): WW-5256 simplify to single compression algorithm

Remove singleLine parameter and complex whitespace normalization logic.
Revert to simple original compression: trim and remove whitespace between tags.

Changes:
- Remove singleLine field and setter from Compress and CompressTag
- Simplify compressWhitespace() to use single regex: >\\s+<
- Update documentation to remove singleLine attribute
- Update all tests to reflect simple compression behavior
- Maintain all security features (maxSize, log truncation, ReDoS protection)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-06 09:59:49 +01:00
Lukasz Lenart 6131c9364e Fixes site and JavaDocs generation (#1419) 2025-11-22 16:55:18 +01:00
Lukasz Lenart 939576c1c3 WW-5585: Implement dynamic parameter evaluation for file upload validation (#1413)
* feat(fileupload): implement dynamic parameter evaluation for file upload validation

- Add WithLazyParams interface to ActionFileUploadInterceptor
- Enable runtime evaluation of ${...} expressions for validation rules
- Add comprehensive JavaDoc with static and dynamic examples
- Add 7 new unit tests for dynamic parameter scenarios
- Create DynamicFileUploadAction showcase with document/image modes
- All 23 tests pass successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(fileupload): address Sonar quality issues in dynamic upload feature

- Mark uploadConfig field as transient for serialization compliance
- Add @Override annotation to input() method
- Add DOCTYPE html declarations to JSP files
- Add lang="en" attributes to html elements for accessibility
- Fix minor code formatting issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(fileupload): add integration tests for dynamic file upload

- Add DynamicFileUploadTest with 7 comprehensive test cases
- Test valid document and image uploads
- Test file type validation (documents reject images, images reject documents)
- Test size limit validation (5MB for documents, 2MB for images)
- Test switching between upload modes
- Add helper methods for creating test files of various sizes
- Follow existing FileUploadTest patterns using HtmlUnit

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(fileupload): use Preparable to initialize upload config before interceptors

Read uploadType directly from request in prepareUpload() method to ensure
upload validation config is set before WithLazyParams interceptor evaluates
the OGNL expressions. This fixes dynamic file type validation not working.

Also fixes:
- Test file creation using correct File.createTempFile prefix pattern
- Default port changed to 8090 in test utils
- Increased struts.multipart.maxSize for testing
- maximumSize parameter changed to String to support OGNL expressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(fileupload): simplify maximumSize type from String to Long

- Change maximumSize field type from String to Long for type safety
- Remove NumberUtils dependency and parsing logic
- Remove unused isNonEmpty() method
- Modernize instanceof patterns using Java 16+ pattern matching
- Fix error message key for null content validation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(research): add lazy multipart parsing research for WW-5585

Documents investigation into dynamic file upload limits at parsing time.
Conclusion: current approach with global hard limits + WithLazyParams
interceptor validation is sufficient.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-22 15:34:45 +01:00
Lukasz Lenart 1164ada511 WW-5588 Allow Preparable interface to work with only per-method prepare*() implementations (#1417)
* fix(core): make Preparable.prepare() a default method

Allow actions to implement only per-method variants like prepareInput(),
prepareEdit() without requiring an empty prepare() override.

Fixes WW-5588

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: add PR guidelines requiring Jira ticket ID and link

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-22 15:34:03 +01:00
Lukasz Lenart a108e2340b fix(core): preserve parameters in InterceptorMapping for WithLazyParams interceptors (#1416)
InterceptorBuilder.constructParameterizedInterceptorReferences() was
creating InterceptorMapping instances without passing the params map,
causing getParams() to return empty and breaking lazy parameter
injection at invocation time.

This fix adds the params map to the InterceptorMapping constructor call,
enabling LazyParamInjector to re-evaluate expression parameters during
action invocation as designed.

Enhanced test to verify params are preserved in InterceptorMapping.

Fixes WW-5587

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-21 06:48:42 +01:00
Lukasz Lenart 6cfd34c945 fix(core): enable WithLazyParams interceptor configuration in stacks (#1414)
Resolves WW-5586

Previously, interceptors implementing WithLazyParams could not be
configured via interceptor stack parameters because DefaultInterceptorFactory
skipped property setting entirely for these interceptors.

Changes:
- DefaultInterceptorFactory: Always set properties for all interceptors
- WithLazyParams: Updated JavaDoc to clarify dual initialization
- Added test for stack parameter configuration with WithLazyParams

Expression parameters are re-evaluated at invocation time via LazyParamInjector,
while static parameters remain unchanged. This enables both static configuration
(e.g., allowedTypes) and dynamic expressions (e.g., ${maxUploadSize}) in the
same interceptor stack reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-17 15:24:09 +01:00
Lukasz Lenart 3e7710c583 WW-5576 Java 25 build (#1415)
* WW-5576 Upgrades commons-lang to version 3.20.0 which is Java 25 compatible

* WW-5576 Defines a new build on Java 25

* WW-5576 Fixes generating TLD file when building on Java 25

* WW-5576 Moves version property to the root pom.xml
2025-11-17 14:42:14 +01:00
Martin Ashby dc7cdfdb9b WW-5578 Don't catch and swallow ConfigurationException in InterceptorBuilder. (#1377)
This can mask configuration errors and result in a non-working
application.

Extend FetchMetadataInterceptorTest; in order to correctly load all of
the interceptors from struts-testing.xml, it needs additional configuration
providers.
2025-10-19 19:12:52 +02:00
Lukasz Lenart eba05e53da Reverse merge changes related to releasing Struts 7.1.1 (#1378)
* [maven-release-plugin] prepare release STRUTS_7_1_1

* [maven-release-plugin] rollback the release of STRUTS_7_1_1

* [maven-release-plugin] prepare release STRUTS_7_1_1

* [maven-release-plugin] prepare for next development iteration
2025-10-19 19:12:31 +02:00
Lukasz Lenart fef0539567 WW-5579 Add missing DoubleRangeFieldValidator and ShortRangeFieldValidator to @Validations container (#1390)
This commit addresses a long-standing gap in the Struts validation framework where @DoubleRangeFieldValidator
and @ShortRangeFieldValidator annotations could not be used within the @Validations container annotation,
despite being fully functional as standalone annotations.

Changes made:
- Added doubleRangeFields() and shortRangeFields() methods to @Validations annotation interface
- Updated JavaDoc to document the new fields and corrected the parameters table
- Added processing loops for both validators in AnnotationValidationConfigurationBuilder
- Created comprehensive test coverage in AnnotationValidationConfigurationBuilderTest
- Added AnnotationValidationsContainerAction test class demonstrating proper usage

The implementation follows the exact same pattern used by IntRangeFieldValidator and LongRangeFieldValidator,
ensuring consistency across all range validators in the framework.

All existing tests continue to pass, and new tests verify that multiple validators of each type
can be properly configured within the @Validations container.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-19 19:11:08 +02:00
Lukasz Lenart 57c33654cd Add tests for conversion error repopulation with indexed properties (#1391)
This commit addresses recommendation #4 from the visitor pattern research:
"Test repopulation behavior specifically with indexed properties to confirm it works as expected."

Two new test methods have been added to VisitorFieldValidatorTest:

1. testArrayConversionErrorRepopulation()
   - Tests conversion errors in indexed array properties (testBeanArray[0].count, etc.)
   - Verifies that conversion errors are properly detected with correct indexed notation
   - Confirms repopulateField parameter preserves invalid values

2. testListConversionErrorRepopulation()
   - Tests conversion errors in indexed list properties (testBeanList[1].count, etc.)
   - Verifies proper field error key generation for list elements
   - Validates that elements without conversion errors don't generate false positives

Supporting validation configuration files:
- TestBean-validateArrayWithConversion-validation.xml
- TestBean-validateListWithConversion-validation.xml
- VisitorValidatorTestAction-validateArrayWithConversion-validation.xml
- VisitorValidatorTestAction-validateListWithConversion-validation.xml

These tests verify the VisitorFieldValidator correctly handles:
- Conversion error detection for indexed properties
- Field name construction with proper index notation
- Error message generation for specific indexed elements
- Selective validation (only elements with conversion errors fail)

Research notes documenting the visitor pattern investigation are included
in thoughts/lukaszlenart/notes/2025-10-17-struts2-iterator-validation-visitor-pattern.md

Also updated .claude/settings.json to refine permissions:
- More specific WebFetch domain (struts.apache.org vs apache.org)
- Added git checkout and git log permissions for better workflow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-18 12:45:18 +02:00