Compare commits

...

139 Commits

Author SHA1 Message Date
Lukasz Lenart 4bfbfeb239 [maven-release-plugin] prepare release STRUTS_6_10_0 2026-05-25 17:19:48 +02:00
Lukasz Lenart 0db282a51a pom(version): sets proper SNAPSHOT version (#1709) 2026-05-25 15:04:31 +00:00
Lukasz Lenart aaf286afa8 WW-5623 fix(core): HTML-encode form action in PostbackResult to prevent XSS (#1701)
PostbackResult.doExecute() embeds finalLocation into a <form action="">
attribute via raw string concatenation. A double quote in the location
breaks out of the attribute, enabling reflected XSS. The response
Content-Type is text/html.

Form field names and values elsewhere in the same class are properly
URL-encoded via URLEncoder.encode(); the action attribute was not
encoded at all.

Wrap finalLocation with StringEscapeUtils.escapeHtml4() before embedding
it in the form tag, consistent with the encoding approach used in
DefaultActionProxy, Property, and TextProviderHelper.

Adds three regression tests in PostbackResultTest:
- testFormActionHtmlEscaping: XSS payload with attribute breakout
- testFormActionEscapesAllHtmlSpecialChars: covers ", &, <, >
- testFormActionCleanLocationUnchanged: regression for clean URLs
2026-05-25 16:38:20 +02:00
Lukasz Lenart 84ef60eae8 WW-5535 fix(core): enforce class-level HTTP method annotations for wildcard-resolved unannotated methods (#1693)
The WW-5535 change to DefaultActionProxy.resolveMethod() (which made
wildcard-resolved methods report isMethodSpecified()=true) interacted
with HttpMethodInterceptor's if/else-if so that the class-level
annotation branch became unreachable when the resolved method carried
no method-level annotation:

    if (isMethodSpecified()) {
        if (method has annotation) return doIntercept(method);
        // unannotated method falls through silently
    } else if (class has annotation) {
        return doIntercept(class);  // never reached when methodSpecified=true
    }

Convert the else-if to a standalone if so the class-level check is
always evaluated as a fallback. Method-level annotations still take
precedence — they are checked first and return early.

Adds three tests:
- testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation:
  GET on a wildcard-resolved unannotated method is rejected when the
  class is @AllowedHttpMethod(POST).
- testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation:
  POST on the same configuration succeeds.
- testWildcardResolvedExecuteRejectsGetThroughRealProxy: end-to-end
  via a real DefaultActionProxy with <action name="Wild-*" method="{1}">,
  resolving to ActionSupport.execute().
2026-05-25 16:38:04 +02:00
dependabot[bot] fb35ed410c build(deps): bump log4j2.version from 2.25.4 to 2.26.0 (#1706)
Bumps `log4j2.version` from 2.25.4 to 2.26.0.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-core` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.4 to 2.26.0

Updates `org.apache.logging.log4j:log4j-web` from 2.25.4 to 2.26.0

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.26.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-web
  dependency-version: 2.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 09:58:16 +02:00
dependabot[bot] 9abd02d961 build(deps): bump asm.version from 9.9.1 to 9.10 (#1703)
Bumps `asm.version` from 9.9.1 to 9.10.

Updates `org.ow2.asm:asm` from 9.9.1 to 9.10

Updates `org.ow2.asm:asm-commons` from 9.9.1 to 9.10

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: '9.10'
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: '9.10'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 09:56:17 +02:00
dependabot[bot] 59536d824a build(deps): bump slf4j.version from 2.0.17 to 2.0.18 (#1704)
Bumps `slf4j.version` from 2.0.17 to 2.0.18.

Updates `org.slf4j:slf4j-api` from 2.0.17 to 2.0.18

Updates `org.slf4j:slf4j-simple` from 2.0.17 to 2.0.18

---
updated-dependencies:
- dependency-name: org.slf4j:slf4j-api
  dependency-version: 2.0.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.slf4j:slf4j-simple
  dependency-version: 2.0.18
  dependency-type: direct:development
  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-05-20 09:26:54 +02:00
Lukasz Lenart ca1b22d9be Struts 6.9.0 (#1662)
* [maven-release-plugin] prepare release STRUTS_6_9_0

* [maven-release-plugin] prepare for next development iteration
2026-05-01 10:41:26 +02:00
Lukasz Lenart 59b5e47575 ci(scorecards): score cards analysis are only supported for default branch (#1661) 2026-04-10 07:13:17 +00:00
Lukasz Lenart 0e22570763 ci(struts6): adjust workflows to use the new branch names (#1658) 2026-04-10 08:23:04 +02:00
Lukasz Lenart 94e3ffd1e7 WW-5622 perf(core): cache Hibernate class presence to avoid repeated NoClassDefFoundError (#1650)
Detect Hibernate availability once at class-load time via Class.forName()
and short-circuit all Hibernate-related methods immediately when absent.
This eliminates repeated NoClassDefFoundError exceptions that cause
significant performance degradation in applications without Hibernate
on the classpath.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 18:30:57 +02:00
Lukasz Lenart b936fcbf8b WW-5621 Harden XML parsers against Entity Expansion (Billion Laughs) attacks (#1643)
Backport of apache/struts#1642 from Struts 7 to Struts 6.

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.

- Enable FEATURE_SECURE_PROCESSING in DomHelper SAX parser
- Enable FEATURE_SECURE_PROCESSING in DigesterDefinitionsReader
- Remove unused parseStringAsXML feature from StringAdapter to eliminate
  a theoretical XML Entity Expansion vector
- Deprecate setParseStringAsXML() and getParseStringAsXML() for removal
- Add Billion Laughs protection tests for DomHelper and DigesterDefinitionsReader

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 11:20:47 +02:00
dependabot[bot] 39c1e29958 build(deps): bump log4j2.version from 2.25.3 to 2.25.4 (#1647)
Bumps `log4j2.version` from 2.25.3 to 2.25.4.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-core` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.3 to 2.25.4

Updates `org.apache.logging.log4j:log4j-web` from 2.25.3 to 2.25.4

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.25.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.25.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-web
  dependency-version: 2.25.4
  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-04-02 16:23:08 +02:00
Lukasz Lenart 6c0189560b ci: fix nightly publishing on release/struts-6-8-x (#1644)
* ci: update Jenkinsfile branch guards from struts-6-7-x to struts-6-8-x

The Build Source & JavaDoc, Deploy Snapshot, and Upload nightlies stages
still referenced release/struts-6-7-x in their when conditions, causing
them to be skipped on every release/struts-6-8-x build. This prevented
6.9.0-SNAPSHOT artifacts from being published to nightlies.

Made-with: Cursor

* chore: add .metals, .bloop, and .vscode to .gitignore

Made-with: Cursor
2026-03-30 12:37:45 +02:00
Lukasz Lenart 0a8b111e36 WW-5537 fix(core): resolve classloader/memory leaks during Tomcat hot deployment (#1631)
* WW-5537 fix(core): resolve classloader/memory leaks during Tomcat hot deployment

Introduce InternalDestroyable interface with container-based discovery to
clean up static caches, daemon threads, and shared references that pin the
webapp classloader after undeploy. This prevents OutOfMemoryError (Metaspace)
on repeated hot deployments.

Changes:
- Add InternalDestroyable/ContextAwareDestroyable interfaces for cleanup hooks
- Clear OGNL, Component, ScopeInterceptor, DefaultFileManager static caches
- Stop FinalizableReferenceQueue daemon thread and null its classloader
- Clear FreeMarker template/introspection caches from ServletContext
- Replace ContainerHolder ThreadLocal with volatile to prevent thread-pool leaks
- Clear static dispatcherListeners list on Dispatcher cleanup
- Add JSONCacheDestroyable for json plugin cache cleanup
- Register all destroyables via struts-beans.xml / struts-plugin.xml

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

* WW-5537 chore(showcase): add log4j-web for proper Log4j2 lifecycle in Servlet container

Without log4j-web, Log4j2 SoftReferences delay classloader GC after undeploy.
The log4j-web module provides Log4jServletContextListener which ensures proper
Log4j2 shutdown during ServletContext destruction.

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

* WW-5537 fix(core): use ThreadLocal with generation counter in ContainerHolder

Replace the volatile shared reference with a ThreadLocal backed by a volatile
generation counter. Per-request clear() only affects the current thread (safe
for concurrent requests and tests). On undeploy, invalidateAll() advances the
generation counter so idle pool threads detect staleness on next access and
self-clear, preventing classloader leaks without breaking test isolation.

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:44 +02:00
dependabot[bot] 696ee73b72 build(deps): bump org.apache.maven.doxia:doxia-module-markdown (#1638)
Bumps org.apache.maven.doxia:doxia-module-markdown from 2.0.0 to 2.1.0.

---
updated-dependencies:
- dependency-name: org.apache.maven.doxia:doxia-module-markdown
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 13:08:32 +01:00
dependabot[bot] be3db87b7f build(deps): bump org.apache.maven.doxia:doxia-core from 2.0.0 to 2.1.0 (#1636)
Bumps [org.apache.maven.doxia:doxia-core](https://github.com/apache/maven-doxia) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/apache/maven-doxia/releases)
- [Commits](https://github.com/apache/maven-doxia/compare/doxia-2.0.0...doxia-2.1.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.doxia:doxia-core
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 13:07:54 +01:00
dependabot[bot] bd93c1bc19 build(deps): bump jackson.version from 2.21.1 to 2.21.2 (#1635)
Bumps `jackson.version` from 2.21.1 to 2.21.2.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.1 to 2.21.2
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.1...jackson-core-2.21.2)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.1 to 2.21.2
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.21.1 to 2.21.2
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.21.1...jackson-dataformat-xml-2.21.2)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.2
  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-03-27 13:07:16 +01:00
Lukasz Lenart f644ea54e2 WW-5618 feat(json): add configurable limits to JSON plugin for DoS prevention (#1626)
Add configurable limits to the JSON plugin to prevent denial-of-service
attacks via malicious JSON payloads. Limits are enforced directly in the
existing JSONReader class without breaking backward compatibility (no
interface extraction or class renames).

New configurable constants (struts-plugin.xml defaults):
- struts.json.maxElements (10000) - per-container element count
- struts.json.maxDepth (64) - maximum nesting depth
- struts.json.maxLength (2097152) - maximum input length in chars
- struts.json.maxStringLength (262144) - maximum string value length
- struts.json.maxKeyLength (512) - maximum object key length

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 12:11:22 +01:00
Lukasz Lenart 0c47627910 WW-2963 fix(core): resolve default-action-ref via wildcard matching (#1623)
When a default-action-ref points to an action name that only exists as a
wildcard pattern (e.g. "movie-input" matching "movie-*"), the framework
now falls back to wildcard matching after the exact lookup fails.

Port of PR #1614 from Struts 7 to Struts 6.x.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 07:25:30 +01:00
Lukasz Lenart bfebc3e4a1 WW-4428 feat(json): add java.time serialization and deserialization support (#1616)
- Add serialization support for LocalDate, LocalDateTime, LocalTime,
  ZonedDateTime, OffsetDateTime, and Instant in DefaultJSONWriter
- Add deserialization support for the same types in JSONPopulator
- Support @JSON(format=...) custom formats for all temporal types
- Add Calendar deserialization support (was serialize-only)
- Add comprehensive tests including custom formats, null handling,
  malformed input, and round-trip serialization/deserialization

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:43:08 +01:00
dependabot[bot] 5a90581033 build(deps-dev): bump commons-logging:commons-logging (#1621)
Bumps [commons-logging:commons-logging](https://github.com/apache/commons-logging) from 1.3.5 to 1.3.6.
- [Changelog](https://github.com/apache/commons-logging/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-logging/compare/rel/commons-logging-1.3.5...rel/commons-logging-1.3.6)

---
updated-dependencies:
- dependency-name: commons-logging:commons-logging
  dependency-version: 1.3.6
  dependency-type: direct:development
  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-03-11 13:29:50 +01:00
Lukasz Lenart edd604a9c5 Sets a proper SNAPSHOT version before next release (#1615) 2026-03-09 10:24:37 +01:00
Lukasz Lenart a0d4f21ad4 Simplifies branch namming patter (#1605) 2026-03-09 09:57:57 +01:00
Lukasz Lenart df97ee23dc fix(i18n): WW-5549 validate locale parameters against supportedLocale (#1602)
When supportedLocale is configured on I18nInterceptor, request_locale
and request_cookie_locale parameters were ignored because
AcceptLanguageLocaleHandler.find() matched the Accept-Language header
before session/cookie handlers checked their explicit locale parameters.
Additionally, stored locales (session/cookie) were never validated
against supportedLocale.

Changes:
- Add isLocaleSupported() helper to validate locales against config
- RequestLocaleHandler.find() now validates against supportedLocale
- AcceptLanguageLocaleHandler.find() checks request_only_locale first,
  then falls back to Accept-Language matching
- SessionLocaleHandler.find() checks request_locale before super.find()
- CookieLocaleHandler.find() checks request_cookie_locale before
  super.find()
- SessionLocaleHandler.read() discards stale session locales
- CookieLocaleHandler.read() discards stale cookie locales

Port of PR #1594 (bug fix only, no refactoring)

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-06 07:50:38 +01:00
Lukasz Lenart 66ea9eaf24 fix(core): WW-5535 enforce HTTP method annotations for wildcard actions (#1593)
DefaultActionProxy.resolveMethod() incorrectly set methodSpecified=false
for config-resolved methods (including wildcard-substituted ones), causing
HttpMethodInterceptor to skip method-level @HttpPost/@HttpGet annotation
checks. Move methodSpecified=false inside the inner if block so it only
applies when truly defaulting to "execute".

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-27 13:26:06 +01:00
dependabot[bot] 66e02ba621 build(deps-dev): bump org.apache.maven.plugins:maven-failsafe-plugin (#1601)
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.4 to 3.5.5.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-version: 3.5.5
  dependency-type: direct:development
  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-25 09:00:20 +01:00
dependabot[bot] 0e16915b4f build(deps): bump maven-surefire-plugin.version from 3.5.4 to 3.5.5 (#1600)
Bumps `maven-surefire-plugin.version` from 3.5.4 to 3.5.5.

Updates `org.apache.maven.surefire:surefire-junit47` from 3.5.4 to 3.5.5

Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.4 to 3.5.5
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.4...surefire-3.5.5)

---
updated-dependencies:
- dependency-name: org.apache.maven.surefire:surefire-junit47
  dependency-version: 3.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.5
  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-25 09:00:10 +01:00
dependabot[bot] 7b99d9eacb build(deps): bump jackson.version from 2.21.0 to 2.21.1 (#1599)
Bumps `jackson.version` from 2.21.0 to 2.21.1.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.21.0 to 2.21.1
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.21.0...jackson-core-2.21.1)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.21.0 to 2.21.1
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.21.0 to 2.21.1
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.21.0...jackson-dataformat-xml-2.21.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.1
  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-25 09:00:00 +01:00
brianandle d4a549672f WW-5616 - JakartaStreamMultiPartRequest warns on file delete if the file doesnt exist (#1591)
* Pull aspects into alignment with main/7.x+ AbstractMultiPartRequest.java
* Update JakartaMultiPartRequest and JakartaStreamMultiPartRequest to use isFile()
* Update cleanup text to mirror main/7.x+
2026-02-22 08:40:15 +01:00
Lukasz Lenart 4b2915682d fix(convention): WW-4421 detect duplicate @Action names when execute() is annotated (#1590)
The duplicate @Action name detection in PackageBasedActionConfigBuilder
was embedded inside a conditional block that only ran when execute() was
NOT annotated with @Action. This meant two methods could map to the same
action name silently when execute() had an @Action annotation, with one
overwriting the other non-deterministically.

Extract the duplicate check to run unconditionally before the conditional
block, so it applies to all annotated methods regardless of whether
execute() is annotated.

Backport of apache/struts#1579 from Struts 7.x to 6.x.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 18:18:27 +01:00
Lukasz Lenart 18d6e77bf2 WW-5514: Make ProxyUtil cache configurable via struts constants (#1573)
* fix(ognl): make ProxyUtil cache configurable via struts constants

Makes the ProxyUtil cache type configurable through Struts constants,
allowing applications to use BASIC cache type (default) without
requiring Caffeine as a mandatory dependency.

New configuration properties:
- struts.proxy.cacheType: basic (default), lru, or wtlfu
- struts.proxy.cacheMaxSize: 10000 (default)

Fixes WW-5514

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

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

* refactor(ognl): use LazyRef for proxy caches and reset on factory change

Extract lazy initialization into reusable LazyRef<T> utility with
double-checked locking and reset support. ProxyUtil.setProxyCacheFactory()
now resets both caches so they are recreated with the new factory,
fixing the bug where caches were never refreshed after factory changes.
Default proxy cache type changed from 'basic' to 'wtlfu' for consistency
with expression and beanInfo caches. Fix @since version to 6.9.0.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-21 18:17:39 +01:00
dependabot[bot] 2bd1b60802 build(deps): bump org.apache.maven.plugins:maven-dependency-plugin (#1576)
Bumps [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/apache/maven-dependency-plugin/releases)
- [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.9.0...maven-dependency-plugin-3.10.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-dependency-plugin
  dependency-version: 3.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-12 18:52:58 +02:00
Kusal Kithul-Godage 43b83731b7 Merge pull request #1565 from apache/WW-5610-extend-forwards-compat
WW-5610 Extend Struts 7 forwards compat to more interceptors
2026-02-12 21:59:28 +11:00
Lukasz Lenart 122dec4d73 feat(conversion): WW-4291 allow Spring bean names for type converters (#1564)
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:44:06 +01:00
dependabot[bot] 2907291ef7 build(deps): bump org.owasp:dependency-check-maven from 12.1.9 to 12.2.0 (#1527)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 12.1.9 to 12.2.0.
- [Release notes](https://github.com/dependency-check/DependencyCheck/releases)
- [Changelog](https://github.com/dependency-check/DependencyCheck/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dependency-check/DependencyCheck/compare/v12.1.9...v12.2.0)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 12.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 19:56:44 +01:00
dependabot[bot] 7065af8304 build(deps): bump jackson.version from 2.20.1 to 2.21.0 (#1550)
Bumps `jackson.version` from 2.20.1 to 2.21.0.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.20.1 to 2.21.0
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.20.1...jackson-core-2.21.0)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.20.1 to 2.21.0
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.20.1 to 2.21.0
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.20.1...jackson-dataformat-xml-2.21.0)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 19:50:51 +01:00
dependabot[bot] 08fe3a2267 build(deps): bump org.apache.commons:commons-text from 1.12.0 to 1.15.0 (#1549)
Bumps [org.apache.commons:commons-text](https://github.com/apache/commons-text) from 1.12.0 to 1.15.0.
- [Changelog](https://github.com/apache/commons-text/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-text/compare/rel/commons-text-1.12.0...rel/commons-text-1.15.0)

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-02 19:43:30 +01:00
dependabot[bot] ff496c8850 build(deps): bump org.assertj:assertj-core from 3.27.6 to 3.27.7 (#1560)
Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.6 to 3.27.7.
- [Release notes](https://github.com/assertj/assertj/releases)
- [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.6...assertj-build-3.27.7)

---
updated-dependencies:
- dependency-name: org.assertj:assertj-core
  dependency-version: 3.27.7
  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-02 18:51:32 +01:00
Kusal Kithul-Godage 87d8feaa8f WW-5610 Extend Struts 7 forwards compat to more interceptors 2026-02-02 18:27:01 +11:00
Lukasz Lenart a5b736b488 chore(conf): skips scans if PR created by Dependabot (#1554) 2026-01-26 16:21:39 +01:00
dependabot[bot] 55ed8629fe build(deps): bump org.apache.velocity:velocity-engine-core (#1546)
Bumps org.apache.velocity:velocity-engine-core from 2.3 to 2.4.1.

---
updated-dependencies:
- dependency-name: org.apache.velocity:velocity-engine-core
  dependency-version: 2.4.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 12:00:20 +01:00
dependabot[bot] a3820104d1 build(deps): bump commons-beanutils:commons-beanutils (#1544)
Bumps commons-beanutils:commons-beanutils from 1.9.4 to 1.11.0.

---
updated-dependencies:
- dependency-name: commons-beanutils:commons-beanutils
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-26 11:57:28 +01:00
dependabot[bot] 984383023b build(deps-dev): bump org.apache.maven.plugins:maven-wrapper-plugin (#1542)
Bumps [org.apache.maven.plugins:maven-wrapper-plugin](https://github.com/apache/maven-wrapper) from 3.3.3 to 3.3.4.
- [Release notes](https://github.com/apache/maven-wrapper/releases)
- [Commits](https://github.com/apache/maven-wrapper/compare/maven-wrapper-3.3.3...maven-wrapper-3.3.4)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-wrapper-plugin
  dependency-version: 3.3.4
  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-01-26 10:24:25 +01:00
dependabot[bot] ba6572eab1 build(deps-dev): bump org.codehaus.mojo:versions-maven-plugin (#1535)
Bumps [org.codehaus.mojo:versions-maven-plugin](https://github.com/mojohaus/versions) from 2.20.1 to 2.21.0.
- [Release notes](https://github.com/mojohaus/versions/releases)
- [Changelog](https://github.com/mojohaus/versions/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/mojohaus/versions/compare/2.20.1...2.21.0)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:versions-maven-plugin
  dependency-version: 2.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 13:11:55 +01:00
dependabot[bot] 921f07f091 build(deps): bump org.apache.maven.plugins:maven-source-plugin (#1532)
Bumps [org.apache.maven.plugins:maven-source-plugin](https://github.com/apache/maven-source-plugin) from 3.3.1 to 3.4.0.
- [Release notes](https://github.com/apache/maven-source-plugin/releases)
- [Commits](https://github.com/apache/maven-source-plugin/compare/maven-source-plugin-3.3.1...maven-source-plugin-3.4.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-source-plugin
  dependency-version: 3.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-20 10:39:13 +01:00
dependabot[bot] 6e15fbde32 build(deps-dev): bump commons-validator:commons-validator (#1523)
Bumps [commons-validator:commons-validator](https://github.com/apache/commons-validator) from 1.10.0 to 1.10.1.
- [Changelog](https://github.com/apache/commons-validator/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-validator/compare/rel/commons-validator-1.10.0...rel/commons-validator-1.10.1)

---
updated-dependencies:
- dependency-name: commons-validator:commons-validator
  dependency-version: 1.10.1
  dependency-type: direct:development
  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-01-14 19:19:51 +01:00
Lukasz Lenart c60670b22b WW-5602 fix StreamResult contentCharSet handling (#1511)
Evaluates contentCharSet expression before emptiness check to prevent
malformed content-type headers when expression evaluates to null.

- Parse contentCharSet expression first, then check if result is empty
- Use StringUtils.isNotEmpty() for proper null/empty validation
- Use setCharacterEncoding() instead of appending to content-type string
- Add test for null-evaluating charset expressions

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 19:18:27 +01:00
dependabot[bot] 6d20e33a37 build(deps): bump io.github.x-stream:mxparser from 1.2.1 to 1.2.3 (#1517)
Bumps [io.github.x-stream:mxparser](https://github.com/x-stream/mxparser) from 1.2.1 to 1.2.3.
- [Changelog](https://github.com/x-stream/mxparser/blob/master/changes.xml)
- [Commits](https://github.com/x-stream/mxparser/compare/v-1.2.1...v-1.2.3)

---
updated-dependencies:
- dependency-name: io.github.x-stream:mxparser
  dependency-version: 1.2.3
  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-01-12 19:26:49 +01:00
dependabot[bot] 43330ec5c5 build(deps): bump org.easymock:easymock from 5.4.0 to 5.6.0 (#1520)
Bumps [org.easymock:easymock](https://github.com/easymock/easymock) from 5.4.0 to 5.6.0.
- [Release notes](https://github.com/easymock/easymock/releases)
- [Changelog](https://github.com/easymock/easymock/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/easymock/easymock/compare/easymock-5.4.0...easymock-5.6.0)

---
updated-dependencies:
- dependency-name: org.easymock:easymock
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 19:26:23 +01:00
dependabot[bot] 752c00f1ae build(deps): bump org.apache.maven.plugins:maven-war-plugin (#1519)
Bumps [org.apache.maven.plugins:maven-war-plugin](https://github.com/apache/maven-war-plugin) from 3.4.0 to 3.5.1.
- [Release notes](https://github.com/apache/maven-war-plugin/releases)
- [Commits](https://github.com/apache/maven-war-plugin/compare/maven-war-plugin-3.4.0...maven-war-plugin-3.5.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-war-plugin
  dependency-version: 3.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-12 19:25:59 +01:00
Lukasz Lenart f3408b756d Merge pull request #1516 from apache/dependabot/maven/release/struts-6-8-x/commons-logging-commons-logging-1.3.5
build(deps-dev): bump commons-logging:commons-logging from 1.3.4 to 1.3.5
2026-01-12 19:25:42 +01:00
dependabot[bot] 1a17211d05 build(deps-dev): bump commons-logging:commons-logging
Bumps commons-logging:commons-logging from 1.3.4 to 1.3.5.

---
updated-dependencies:
- dependency-name: commons-logging:commons-logging
  dependency-version: 1.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-12 17:52:27 +00:00
dependabot[bot] d955721e17 build(deps): bump org.codehaus.mojo:exec-maven-plugin (#1509)
Bumps [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases)
- [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.6.2...3.6.3)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:exec-maven-plugin
  dependency-version: 3.6.3
  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-01-12 18:41:04 +01:00
dependabot[bot] 7f96a4f16e build(deps): bump org.codehaus.mojo:versions-maven-plugin (#1501)
Bumps [org.codehaus.mojo:versions-maven-plugin](https://github.com/mojohaus/versions) from 2.17.1 to 2.20.1.
- [Release notes](https://github.com/mojohaus/versions/releases)
- [Changelog](https://github.com/mojohaus/versions/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/mojohaus/versions/compare/2.17.1...2.20.1)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:versions-maven-plugin
  dependency-version: 2.20.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 13:36:20 +01:00
dependabot[bot] 4982e61f09 build(deps): bump org.owasp:dependency-check-maven from 10.0.4 to 12.1.9 (#1500)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 10.0.4 to 12.1.9.
- [Release notes](https://github.com/dependency-check/DependencyCheck/releases)
- [Changelog](https://github.com/dependency-check/DependencyCheck/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dependency-check/DependencyCheck/compare/v10.0.4...v12.1.9)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 12.1.9
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 13:36:08 +01:00
dependabot[bot] 71048d41a5 build(deps): bump org.apache.maven.plugins:maven-release-plugin (#1499)
Bumps [org.apache.maven.plugins:maven-release-plugin](https://github.com/apache/maven-release) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/apache/maven-release/releases)
- [Commits](https://github.com/apache/maven-release/compare/maven-release-3.3.0...maven-release-3.3.1)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-release-plugin
  dependency-version: 3.3.1
  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>
2025-12-29 13:35:54 +01:00
dependabot[bot] e8640b21f1 build(deps): bump asm.version from 9.9 to 9.9.1 (#1495)
Bumps `asm.version` from 9.9 to 9.9.1.

Updates `org.ow2.asm:asm` from 9.9 to 9.9.1

Updates `org.ow2.asm:asm-commons` from 9.9 to 9.9.1

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: 9.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: 9.9.1
  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>
2025-12-29 11:23:43 +01:00
dependabot[bot] daa9e9c9e1 build(deps): bump org.apache.maven.plugins:maven-site-plugin (#1494)
Bumps [org.apache.maven.plugins:maven-site-plugin](https://github.com/apache/maven-site-plugin) from 3.20.0 to 3.21.0.
- [Release notes](https://github.com/apache/maven-site-plugin/releases)
- [Commits](https://github.com/apache/maven-site-plugin/compare/maven-site-plugin-3.20.0...maven-site-plugin-3.21.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-site-plugin
  dependency-version: 3.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-29 11:23:08 +01:00
dependabot[bot] 07d5533cd6 build(deps): bump log4j2.version from 2.25.2 to 2.25.3 (#1486)
Bumps `log4j2.version` from 2.25.2 to 2.25.3.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.2 to 2.25.3

Updates `org.apache.logging.log4j:log4j-core` from 2.25.2 to 2.25.3

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.2 to 2.25.3

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.2 to 2.25.3

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.25.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.25.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.25.3
  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>
2025-12-23 09:36:21 +01:00
Ryan J Murphy b490e2c2fc Merge pull request #1445 from ryanmurf/multipartCleanup
WW-5573  Multipart stream file cleanup
2025-12-11 12:48:13 +01:00
Lukasz Lenart d5bff6aef8 Merge pull request #1466 from apache/dependabot/maven/release/struts-6-8-x/org.apache.maven.plugins-maven-failsafe-plugin-3.5.4
Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
2025-12-11 12:47:41 +01:00
Lukasz Lenart 69ccd406fe Merge pull request #1462 from apache/dependabot/maven/release/struts-6-8-x/org.awaitility-awaitility-4.3.0
Bump org.awaitility:awaitility from 4.2.2 to 4.3.0
2025-12-11 12:47:21 +01:00
dependabot[bot] 9afee9994d Bump org.awaitility:awaitility from 4.2.2 to 4.3.0
Bumps [org.awaitility:awaitility](https://github.com/awaitility/awaitility) from 4.2.2 to 4.3.0.
- [Changelog](https://github.com/awaitility/awaitility/blob/master/changelog.txt)
- [Commits](https://github.com/awaitility/awaitility/compare/awaitility-4.2.2...awaitility-4.3.0)

---
updated-dependencies:
- dependency-name: org.awaitility:awaitility
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 10:44:01 +00:00
Lukasz Lenart 7d7060896b Merge pull request #1458 from apache/release/struts-6-7-x
Merges changes from older release branch
2025-12-11 11:33:22 +01:00
dependabot[bot] e37b68570f Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.1 to 3.5.4.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.4)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-version: 3.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-11 10:22:39 +00:00
dependabot[bot] 80739da117 Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.20.0 (#1455)
Bumps org.apache.commons:commons-lang3 from 3.17.0 to 3.20.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-11 10:26:46 +01:00
dependabot[bot] c8b1d0e2f2 Bump org.apache.maven.plugins:maven-release-plugin from 3.1.1 to 3.3.0 (#1454)
Bumps [org.apache.maven.plugins:maven-release-plugin](https://github.com/apache/maven-release) from 3.1.1 to 3.3.0.
- [Release notes](https://github.com/apache/maven-release/releases)
- [Commits](https://github.com/apache/maven-release/compare/maven-release-3.1.1...maven-release-3.3.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-release-plugin
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 12:25:39 +01:00
dependabot[bot] e5d835bc19 Bump org.apache.maven.plugins:maven-enforcer-plugin from 3.5.0 to 3.6.2 (#1453)
Bumps [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.5.0 to 3.6.2.
- [Release notes](https://github.com/apache/maven-enforcer/releases)
- [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.5.0...enforcer-3.6.2)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-enforcer-plugin
  dependency-version: 3.6.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-10 12:25:21 +01:00
dependabot[bot] 4997040be4 Bump maven-surefire-plugin.version from 3.5.1 to 3.5.4 (#1452)
Bumps `maven-surefire-plugin.version` from 3.5.1 to 3.5.4.

Updates `org.apache.maven.surefire:surefire-junit47` from 3.5.1 to 3.5.4

Updates `org.apache.maven.plugins:maven-surefire-plugin` from 3.5.1 to 3.5.4
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.4)

---
updated-dependencies:
- dependency-name: org.apache.maven.surefire:surefire-junit47
  dependency-version: 3.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.4
  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>
2025-12-10 12:25:02 +01:00
dependabot[bot] 5d648a62bf Bump org.apache.rat:apache-rat-plugin from 0.15 to 0.17 (#1436)
* Bump org.apache.rat:apache-rat-plugin from 0.15 to 0.17

Bumps org.apache.rat:apache-rat-plugin from 0.15 to 0.17.

---
updated-dependencies:
- dependency-name: org.apache.rat:apache-rat-plugin
  dependency-version: '0.17'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Updates exclusions

* Cleans up files with missing header

---------

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>
2025-12-07 09:33:05 +01:00
dependabot[bot] 52dba77dc0 Bump org.jfree:jfreechart from 1.5.5 to 1.5.6 (#1440)
Bumps [org.jfree:jfreechart](https://github.com/jfree/jfreechart) from 1.5.5 to 1.5.6.
- [Release notes](https://github.com/jfree/jfreechart/releases)
- [Commits](https://github.com/jfree/jfreechart/compare/v1.5.5...v1.5.6)

---
updated-dependencies:
- dependency-name: org.jfree:jfreechart
  dependency-version: 1.5.6
  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>
2025-12-01 06:55:32 +01:00
dependabot[bot] 1fcfc42ab2 Bump log4j2.version from 2.25.1 to 2.25.2 (#1437)
Bumps `log4j2.version` from 2.25.1 to 2.25.2.

Updates `org.apache.logging.log4j:log4j-api` from 2.25.1 to 2.25.2

Updates `org.apache.logging.log4j:log4j-core` from 2.25.1 to 2.25.2

Updates `org.apache.logging.log4j:log4j-jcl` from 2.25.1 to 2.25.2

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.25.1 to 2.25.2

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.25.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.25.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.25.2
  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>
2025-12-01 06:54:13 +01:00
Lukasz Lenart cae08102d5 Removes unused jaxb-core dependency (#1434) 2025-11-28 08:44:15 +01:00
dependabot[bot] a810da1397 Bump com.sun.xml.bind:jaxb-core from 2.3.0.1 to 4.0.6 (#1429)
Bumps com.sun.xml.bind:jaxb-core from 2.3.0.1 to 4.0.6.

---
updated-dependencies:
- dependency-name: com.sun.xml.bind:jaxb-core
  dependency-version: 4.0.6
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 19:07:26 +01:00
dependabot[bot] 0489abde40 Bump org.jacoco:jacoco-maven-plugin from 0.8.12 to 0.8.14 (#1428)
Bumps [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.12 to 0.8.14.
- [Release notes](https://github.com/jacoco/jacoco/releases)
- [Commits](https://github.com/jacoco/jacoco/compare/v0.8.12...v0.8.14)

---
updated-dependencies:
- dependency-name: org.jacoco:jacoco-maven-plugin
  dependency-version: 0.8.14
  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>
2025-11-24 07:18:34 +01:00
dependabot[bot] 96006ab4ff Bump org.apache.maven.plugins:maven-dependency-plugin (#1426)
Bumps [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin) from 3.8.0 to 3.9.0.
- [Release notes](https://github.com/apache/maven-dependency-plugin/releases)
- [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.8.0...maven-dependency-plugin-3.9.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-dependency-plugin
  dependency-version: 3.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 07:18:05 +01:00
dependabot[bot] d9b84bc89e Bump jackson.version from 2.20.0 to 2.20.1 (#1412)
Bumps `jackson.version` from 2.20.0 to 2.20.1.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.20.0 to 2.20.1
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.20.0...jackson-core-2.20.1)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.20.0 to 2.20.1
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.20.0 to 2.20.1
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.20.0...jackson-dataformat-xml-2.20.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.20.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.20.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.20.1
  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>
2025-11-17 07:57:35 +01:00
dependabot[bot] 7003406d4c Bump org.codehaus.mojo:exec-maven-plugin from 3.4.1 to 3.6.2 (#1411)
Bumps [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) from 3.4.1 to 3.6.2.
- [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases)
- [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.4.1...3.6.2)

---
updated-dependencies:
- dependency-name: org.codehaus.mojo:exec-maven-plugin
  dependency-version: 3.6.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 07:56:47 +01:00
dependabot[bot] 8d935b1394 Bump com.thoughtworks.xstream:xstream from 1.4.20 to 1.4.21 (#1408)
Bumps [com.thoughtworks.xstream:xstream](https://github.com/x-stream/xstream) from 1.4.20 to 1.4.21.
- [Release notes](https://github.com/x-stream/xstream/releases)
- [Commits](https://github.com/x-stream/xstream/commits)

---
updated-dependencies:
- dependency-name: com.thoughtworks.xstream:xstream
  dependency-version: 1.4.21
  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>
2025-11-17 07:54:56 +01:00
dependabot[bot] f053c78db2 Bump asm.version from 9.7.1 to 9.9 (#1393)
Bumps `asm.version` from 9.7.1 to 9.9.

Updates `org.ow2.asm:asm` from 9.7.1 to 9.9

Updates `org.ow2.asm:asm-commons` from 9.7.1 to 9.9

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: '9.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: '9.9'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 06:51:55 +01:00
dependabot[bot] e805bb389f Bump org.freemarker:freemarker from 2.3.33 to 2.3.34 (#1386)
Bumps org.freemarker:freemarker from 2.3.33 to 2.3.34.

---
updated-dependencies:
- dependency-name: org.freemarker:freemarker
  dependency-version: 2.3.34
  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>
2025-10-27 06:49:41 +01:00
dependabot[bot] eb7a0804b8 Bump commons-validator:commons-validator from 1.9.0 to 1.10.0 (#1385)
Bumps commons-validator:commons-validator from 1.9.0 to 1.10.0.

---
updated-dependencies:
- dependency-name: commons-validator:commons-validator
  dependency-version: 1.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-27 06:48:12 +01:00
dependabot[bot] e07612b762 Bump org.assertj:assertj-core from 3.27.4 to 3.27.6 (#1387)
Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.4 to 3.27.6.
- [Release notes](https://github.com/assertj/assertj/releases)
- [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.4...assertj-build-3.27.6)

---
updated-dependencies:
- dependency-name: org.assertj:assertj-core
  dependency-version: 3.27.6
  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>
2025-10-19 19:13:17 +02:00
Lukasz Lenart f92b2372bf Merge pull request #1369 from apache/dependabot/maven/release/struts-6-7-x/org.apache.maven.doxia-doxia-module-markdown-2.0.0
Bump org.apache.maven.doxia:doxia-module-markdown from 1.12.0 to 2.0.0
2025-09-29 08:23:33 +02:00
dependabot[bot] e5e535b01a Bump org.apache.maven.doxia:doxia-module-markdown from 1.12.0 to 2.0.0
Bumps org.apache.maven.doxia:doxia-module-markdown from 1.12.0 to 2.0.0.

---
updated-dependencies:
- dependency-name: org.apache.maven.doxia:doxia-module-markdown
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-29 01:36:07 +00:00
Lukasz Lenart 39c3df3032 Merge pull request #1356 from apache/dependabot/maven/release/struts-6-7-x/org.apache.maven.plugins-maven-failsafe-plugin-3.5.4
Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
2025-09-28 11:51:33 +02:00
dependabot[bot] 730d553664 Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.1 to 3.5.4
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.1 to 3.5.4.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.1...surefire-3.5.4)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-version: 3.5.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-22 01:36:06 +00:00
Lukasz Lenart 34e279e6d7 [maven-release-plugin] prepare for next development iteration 2025-09-15 07:46:27 +02:00
Lukasz Lenart 80225c7169 [maven-release-plugin] prepare release STRUTS_6_8_0 2025-09-15 07:46:18 +02:00
Lukasz Lenart 6b2de2df4a WW-5504 Allows to use request instead of session attribute to store nonce (#1174) 2025-09-15 07:13:24 +02:00
Lukasz Lenart 15e257d075 Merge pull request #1345 from apache/dependabot/maven/release/struts-6-7-x/org.assertj-assertj-core-3.27.4
Bump org.assertj:assertj-core from 3.26.3 to 3.27.4
2025-09-15 07:12:26 +02:00
Lukasz Lenart daf2f51721 Merge pull request #1344 from apache/dependabot/maven/release/struts-6-7-x/org.apache.commons-commons-compress-1.28.0
WW-5571 Bump org.apache.commons:commons-compress from 1.27.1 to 1.28.0
2025-09-15 07:12:00 +02:00
Lukasz Lenart 440dfe8c6d Merge pull request #1343 from apache/dependabot/maven/release/struts-6-7-x/org.apache.maven.plugins-maven-project-info-reports-plugin-3.9.0
Bump org.apache.maven.plugins:maven-project-info-reports-plugin from 3.7.0 to 3.9.0
2025-09-15 07:10:30 +02:00
Lukasz Lenart a016d50b53 Merge pull request #1342 from apache/dependabot/maven/release/struts-6-7-x/org.apache.commons-commons-collections4-4.5.0
WW-5570 Bump org.apache.commons:commons-collections4 from 4.4 to 4.5.0
2025-09-15 07:09:59 +02:00
dependabot[bot] d718290ad1 Bump org.assertj:assertj-core from 3.26.3 to 3.27.4
Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.26.3 to 3.27.4.
- [Release notes](https://github.com/assertj/assertj/releases)
- [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.26.3...assertj-build-3.27.4)

---
updated-dependencies:
- dependency-name: org.assertj:assertj-core
  dependency-version: 3.27.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:37:30 +00:00
dependabot[bot] 2f73fc9ccf Bump org.apache.commons:commons-compress from 1.27.1 to 1.28.0
Bumps [org.apache.commons:commons-compress](https://github.com/apache/commons-compress) from 1.27.1 to 1.28.0.
- [Changelog](https://github.com/apache/commons-compress/blob/master/RELEASE-NOTES.txt)
- [Commits](https://github.com/apache/commons-compress/compare/rel/commons-compress-1.27.1...rel/commons-compress-1.28.0)

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-compress
  dependency-version: 1.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:36:18 +00:00
dependabot[bot] d054d638b6 Bump org.apache.maven.plugins:maven-project-info-reports-plugin
Bumps [org.apache.maven.plugins:maven-project-info-reports-plugin](https://github.com/apache/maven-project-info-reports-plugin) from 3.7.0 to 3.9.0.
- [Release notes](https://github.com/apache/maven-project-info-reports-plugin/releases)
- [Commits](https://github.com/apache/maven-project-info-reports-plugin/compare/maven-project-info-reports-plugin-3.7.0...maven-project-info-reports-plugin-3.9.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-project-info-reports-plugin
  dependency-version: 3.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:35:59 +00:00
dependabot[bot] 399ec92c4a Bump org.apache.commons:commons-collections4 from 4.4 to 4.5.0
Bumps org.apache.commons:commons-collections4 from 4.4 to 4.5.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-collections4
  dependency-version: 4.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:34:36 +00:00
Lukasz Lenart c58fe8bfb9 Merge pull request #1334 from apache/dependabot/maven/release/struts-6-7-x/jackson.version-2.20.0
WW-5568 Bump jackson.version from 2.18.0 to 2.20.0
2025-09-10 10:56:56 +02:00
Lukasz Lenart aa56c07972 Merge pull request #1337 from apache/dependabot/maven/release/struts-6-7-x/org.apache.maven.plugins-maven-wrapper-plugin-3.3.3
Bump org.apache.maven.plugins:maven-wrapper-plugin from 3.3.2 to 3.3.3
2025-09-10 10:24:19 +02:00
Lukasz Lenart 5148a9f088 Merge pull request #1335 from apache/dependabot/maven/release/struts-6-7-x/slf4j.version-2.0.17
Bump slf4j.version from 2.0.16 to 2.0.17
2025-09-10 10:23:47 +02:00
dependabot[bot] f743b18e19 Bump jackson.version from 2.18.0 to 2.20.0
Bumps `jackson.version` from 2.18.0 to 2.20.0.

Updates `com.fasterxml.jackson.core:jackson-core` from 2.18.0 to 2.20.0
- [Commits](https://github.com/FasterXML/jackson-core/compare/jackson-core-2.18.0...jackson-core-2.20.0)

Updates `com.fasterxml.jackson.core:jackson-databind` from 2.18.0 to 2.20.0
- [Commits](https://github.com/FasterXML/jackson/commits)

Updates `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` from 2.18.0 to 2.20.0
- [Commits](https://github.com/FasterXML/jackson-dataformat-xml/compare/jackson-dataformat-xml-2.18.0...jackson-dataformat-xml-2.20.0)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 08:22:31 +00:00
Lukasz Lenart b674e32cbe Merge pull request #1330 from apache/dependabot/maven/release/struts-6-7-x/log4j2.version-2.25.1
WW-5567 Bump log4j2.version from 2.24.1 to 2.25.1
2025-09-10 10:21:18 +02:00
dependabot[bot] 862013273b Bump org.apache.maven.plugins:maven-wrapper-plugin from 3.3.2 to 3.3.3
Bumps [org.apache.maven.plugins:maven-wrapper-plugin](https://github.com/apache/maven-wrapper) from 3.3.2 to 3.3.3.
- [Release notes](https://github.com/apache/maven-wrapper/releases)
- [Commits](https://github.com/apache/maven-wrapper/compare/maven-wrapper-3.3.2...maven-wrapper-3.3.3)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-wrapper-plugin
  dependency-version: 3.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 06:03:37 +00:00
dependabot[bot] d92509fc29 Bump slf4j.version from 2.0.16 to 2.0.17
Bumps `slf4j.version` from 2.0.16 to 2.0.17.

Updates `org.slf4j:slf4j-api` from 2.0.16 to 2.0.17

Updates `org.slf4j:slf4j-simple` from 2.0.16 to 2.0.17

---
updated-dependencies:
- dependency-name: org.slf4j:slf4j-api
  dependency-version: 2.0.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.slf4j:slf4j-simple
  dependency-version: 2.0.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 06:03:31 +00:00
dependabot[bot] e11ea4778f Bump log4j2.version from 2.24.1 to 2.25.1
Bumps `log4j2.version` from 2.24.1 to 2.25.1.

Updates `org.apache.logging.log4j:log4j-api` from 2.24.1 to 2.25.1

Updates `org.apache.logging.log4j:log4j-core` from 2.24.1 to 2.25.1

Updates `org.apache.logging.log4j:log4j-jcl` from 2.24.1 to 2.25.1

Updates `org.apache.logging.log4j:log4j-slf4j-impl` from 2.24.1 to 2.25.1

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.25.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.25.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.25.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 06:03:16 +00:00
Lukasz Lenart 0649b5686b Merge pull request #1328 from apache/WW-5564-fileupload
WW-5564 Upgrades commons-fileupload to version 1.6.0
2025-09-10 08:02:32 +02:00
Lukasz Lenart 272797f764 Fixes .ads.yaml to use proper structure for checks 2025-09-10 07:38:43 +02:00
Lukasz Lenart 03cbbdba90 WW-5564 Upgrades commons-fileupload to version 1.6.0 2025-09-10 07:35:21 +02:00
Lukasz Lenart 1460c482c3 Merge pull request #1317 from apache/feature/WW-5511-removes-addCspHeaders
WW-5511 Removes deprecated addCspHeaders method
2025-09-02 08:16:56 +02:00
Lukasz Lenart ea97930474 WW-5511 Removes deprecated addCspHeaders method 2025-08-22 08:35:26 +02:00
Lukasz Lenart f25321661f Merge pull request #1316 from apache/fix/WW-5524-converter-factory-s6
WW-5524 Fixup StrutsConverterFactory - Struts 6
2025-08-20 07:13:37 +02:00
Martin Ashby a92568423e Fixup StrutsConverterFactory
It should delegate back to ObjectFactory#buildBean instead of directly
calling Container#inject, otherwise overrides of buildBean in subclasses
of ObjectFactory e.g. SpringObjectFactory are skipped; meaning that
TypeConverters cannot make use of Spring dependency injection

Fixes: https://issues.apache.org/jira/projects/WW/issues/WW-5524
2025-08-20 06:48:31 +02:00
Lukasz Lenart 036c4ddb74 Merge pull request #1306 from apache/fix/clean-uploaded-files
Cleans up all uploaded files
2025-08-18 16:55:41 +02:00
Lukasz Lenart 831568929c Cleans up all uploaded files 2025-08-06 13:46:15 +02:00
Lukasz Lenart 84a51cce0e WW-5546 Fixes NPE when uploaded file is empty (#1262) 2025-05-06 20:48:24 +02:00
Lukasz Lenart 0f61944d64 WW-5544 Marks ReflectionContextFactory as deprecated and uses ActionContext instead (#1254) 2025-04-27 15:59:19 +02:00
Lukasz Lenart ca4e07cead [maven-release-plugin] prepare for next development iteration 2025-02-20 13:20:56 +01:00
Lukasz Lenart 8fb9aba74b [maven-release-plugin] prepare release STRUTS_6_7_4 2025-02-20 13:20:46 +01:00
Lukasz Lenart c45664a330 Merge pull request #1226 from apache/fix/WW-5529-maxlength-s6
WW-5529 Adds autogenerated files with updated desc
2025-02-20 13:05:57 +01:00
Lukasz Lenart 91c9314176 WW-5529 Adds autogenerated files with updated desc 2025-02-20 12:21:58 +01:00
Kusal Kithul-Godage a4e9902d94 Merge pull request #1225 from apache/WW-5531-jre8-compat
WW-5531 Enforce JRE8 compatibility (when compiled on JDK9+)
2025-02-20 17:22:42 +11:00
Kusal Kithul-Godage fe5b197738 WW-5531 Enforce JRE8 compatibility (when compiled on JDK9+) 2025-02-20 16:23:10 +11:00
Lukasz Lenart 87e322dbe2 [maven-release-plugin] prepare for next development iteration 2025-02-17 11:34:19 +01:00
Lukasz Lenart 7909f43237 [maven-release-plugin] prepare release STRUTS_6_7_3 2025-02-17 11:34:11 +01:00
Lukasz Lenart 1fe822cca0 WW-5529 Drops unused misleading setter setMaxLength in favour of setMaxlength (#1221) (#1222) 2025-02-17 10:27:04 +01:00
Kusal Kithul-Godage ef90b3ba3c Merge pull request #1220 from apache/WW-5525-proxyutil-npe-67
6.7: WW-5525 Fix NPE in ProxyUtil for SecurityMemberAccess originating static members
2025-02-17 19:11:21 +11:00
Lukasz Lenart d35ec15c76 WW-5525 Fixes NPE when checking if expressions is acceptable
(cherry picked from commit 9fee06cea0)
2025-02-17 18:57:27 +11:00
Kusal Kithul-Godage 583b174fe6 WW-5525 Fix NPE in ProxyUtil for SecurityMemberAccess originating static members 2025-02-17 18:57:27 +11:00
Lukasz Lenart 9b044377ac WW-5501 Reverts all changes related to WW-5501 (#1218)
* Reverts all changes related to WW-5501

* Fixes CodeQL scan by using proper versions of actions

* WW-5501 Uses FilenameUtils instead of a custom code
2025-02-16 12:05:49 +01:00
Kusal Kithul-Godage 58f37babbf Merge pull request #1215 from apache/WW-5528-multipart-illegal-char-errors-67 2025-02-09 23:44:10 +11:00
Kusal Kithul-Godage fe3dd6ba9f WW-5528 Ensure multipart upload illegal characters reported as error 2025-02-07 12:40:42 +11:00
Lukasz Lenart 7a77c7a82c [maven-release-plugin] prepare for next development iteration 2025-02-04 07:39:44 +01:00
Lukasz Lenart 5038a9209f [maven-release-plugin] prepare release STRUTS_6_7_2 2025-02-04 07:39:35 +01:00
Lukasz Lenart 2135ebf651 WW-5501 Ignores DMI related action field (#1211) 2025-02-04 06:47:43 +01:00
Kusal Kithul-Godage c0dd8dd80c Merge pull request #1210 from apache/WW-5516-attrmap-npe-67 2025-02-04 01:19:16 +11:00
Kusal Kithul-Godage c36eafa37a WW-5516 Fix AttributeMap NPE when PageContext has no request 2025-02-04 01:00:36 +11:00
Lukasz Lenart 07603b9591 [maven-release-plugin] prepare for next development iteration 2025-02-02 10:16:30 +01:00
176 changed files with 4617 additions and 828 deletions
+16 -5
View File
@@ -17,16 +17,27 @@ github:
protected_branches:
main:
# contexts are the names of checks that must pass.
contexts:
- build
required_status_checks:
contexts:
- "Build and Test (JDK 17)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
release/*:
support/struts-6-x-x:
# contexts are the names of checks that must pass.
contexts:
- build
required_status_checks:
contexts:
- "Build and Test (8)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
release/struts-6-*:
# contexts are the names of checks that must pass.
required_status_checks:
contexts:
- "Build and Test (8)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
+21 -13
View File
@@ -17,7 +17,9 @@ name: "CodeQL"
on:
push:
branches: [ "master" ]
branches:
- 'support/struts-6-x-x'
- 'release/*'
pull_request:
permissions:
@@ -41,15 +43,21 @@ jobs:
matrix:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.27.1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3.27.1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.27.1
with:
category: "/language:${{matrix.language}}"
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Java JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: 'maven'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.8
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3.28.8
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.8
with:
category: "/language:${{matrix.language}}"
+2 -1
View File
@@ -19,7 +19,8 @@ on:
pull_request:
push:
branches:
- master
- 'support/struts-6-x-x'
- 'release/*'
permissions: read-all
+2 -1
View File
@@ -20,7 +20,8 @@ on:
schedule:
- cron: "30 1 * * 6" # Weekly on Saturdays
push:
branches: [ "master" ]
branches:
- 'main' # only default branch is supported
permissions: read-all
+6 -5
View File
@@ -19,7 +19,8 @@ on:
pull_request:
push:
branches:
- master
- 'support/struts-6-x-x'
- 'release/*'
permissions: read-all
@@ -31,12 +32,12 @@ jobs:
sonarcloud:
name: Scan
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.head.repo.fork }}
if: ${{ !github.event.pull_request.base.repo.fork && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
@@ -44,4 +45,4 @@ jobs:
- env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}
run: mvn -B -V -Pcoverage -DskipAssembly verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar --no-transfer-progress
run: ./mvnw -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Pcoverage -DskipAssembly
+10 -4
View File
@@ -1,5 +1,5 @@
# IDEA
.idea
.idea/
*.iml
*.ipr
*.iws
@@ -38,11 +38,17 @@ buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
plugins/testng/test-output
test-output
plugins/testng/test-output/
test-output/
# Sonar
/.sonar/
.sonar/
# Tidelift CLI scanner
.tidelift
# Claude Code specific local settings
.claude/
.metals/
.bloop/
.vscode/
+37
View File
@@ -0,0 +1,37 @@
# CLAUDE.md
## Build Commands
```bash
# Build without tests (fastest)
./mvnw clean install -DskipTests -DskipAssembly
# Test a single module
./mvnw -pl core clean test
./mvnw -pl plugins/spring clean test
# Full build with tests
./mvnw clean install
# Integration tests
./mvnw clean verify -DskipAssembly
# Coverage report
./mvnw clean verify -Pcoverage -DskipAssembly
```
## Project-Specific Rules
- This is the **6.x.x** branch (`release/struts-6-8-x`).
- Uses **javax.servlet** (Java EE), not Jakarta EE. Verify imports use `javax.servlet` namespace.
- Test pattern: `**/*Test.java`. Test classes use JUnit 4 with `@Test` annotations.
- OGNL expressions have strict security via `SecurityMemberAccess` — test any new OGNL usage against the security sandbox.
- Each plugin has its own `struts-plugin.xml` descriptor — register new beans there, not in core config.
- Run `./mvnw clean prepare-package` before committing to verify Apache RAT license headers pass.
## Module Layout
- `core/` — framework core
- `plugins/` — 20+ plugin modules (spring, json, tiles, velocity, etc.)
- `apps/showcase/` — feature demo app
- `apps/rest-showcase/` — REST examples
Vendored
+22 -4
View File
@@ -1,4 +1,22 @@
#!groovy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
pipeline {
agent none
@@ -81,7 +99,7 @@ pipeline {
stage('Code Quality') {
when {
anyOf {
branch 'release/struts-6-7-x'
branch 'support/struts-6-x-x'
}
}
steps {
@@ -127,7 +145,7 @@ pipeline {
}
stage('Build Source & JavaDoc') {
when {
branch 'release/struts-6-7-x'
branch 'support/struts-x-x-x'
}
steps {
dir("local-snapshots-dir/") {
@@ -138,7 +156,7 @@ pipeline {
}
stage('Deploy Snapshot') {
when {
branch 'release/struts-6-7-x'
branch 'support/struts-6-x-x'
}
steps {
withCredentials([file(credentialsId: 'lukaszlenart-repository-access-token', variable: 'CUSTOM_SETTINGS')]) {
@@ -148,7 +166,7 @@ pipeline {
}
stage('Upload nightlies') {
when {
branch 'release/struts-6-7-x'
branch 'support/struts-6-x-x'
}
steps {
sh './mvnw -B package -DskipTests'
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-apps</artifactId>
<packaging>pom</packaging>
+2 -2
View File
@@ -24,12 +24,12 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>6.7.1</version>
<version>6.10.0</version>
<name>Struts 2 Rest Showcase Webapp</name>
<description>Struts 2 Rest Showcase Example</description>
+6 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-showcase</artifactId>
@@ -121,6 +121,10 @@
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
</dependency>
<dependency>
<groupId>opensymphony</groupId>
@@ -163,7 +167,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.1</version>
<version>3.5.5</version>
<configuration>
<includes>
<include>it.org.apache.struts2.showcase.*Test</include>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-assembly</artifactId>
+3 -4
View File
@@ -25,11 +25,10 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-bom</artifactId>
<version>6.7.1</version>
<packaging>pom</packaging>
<name>Struts 2 Bill of Materials</name>
@@ -44,7 +43,7 @@
</licenses>
<properties>
<struts-version.version>6.7.1</struts-version.version>
<struts-version.version>6.10.0</struts-version.version>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
@@ -190,7 +189,7 @@
</dependencyManagement>
<scm>
<tag>STRUTS_6_7_1</tag>
<tag>STRUTS_6_10_0</tag>
<connection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</developerConnection>
<url>https://github.com/apache/struts/</url>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-bundles</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-osgi-admin-bundle</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-bundles</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-osgi-demo-bundle</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-osgi-bundles</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-core</artifactId>
<packaging>jar</packaging>
@@ -168,8 +168,8 @@ public class DefaultActionProxy implements ActionProxy, Serializable {
this.method = config.getMethodName();
if (StringUtils.isEmpty(this.method)) {
this.method = ActionConfig.DEFAULT_METHOD;
methodSpecified = false;
}
methodSpecified = false;
}
}
@@ -19,7 +19,25 @@
package com.opensymphony.xwork2.config;
/**
* When implemented allows to alias already existing beans
* A {@link ConfigurationProvider} that selects and aliases bean implementations.
* <p>
* Implementations of this interface are responsible for selecting which bean implementation
* to use for a given interface type. The selection is typically based on configuration properties
* that specify the bean name or class name.
* </p>
* <p>
* The aliasing mechanism works as follows:
* </p>
* <ol>
* <li>Look for a bean by the name specified in the configuration property</li>
* <li>If found, alias it to the default name so it becomes the default implementation</li>
* <li>If not found, try to load the value as a class name and register it as a factory</li>
* <li>If class loading fails, delegate to {@link org.apache.struts2.ObjectFactory} at runtime
* (useful for Spring bean names)</li>
* </ol>
*
* @see AbstractBeanSelectionProvider
* @see StrutsBeanSelectionProvider
*/
public interface BeanSelectionProvider extends ConfigurationProvider {
@@ -82,6 +82,8 @@ import com.opensymphony.xwork2.ognl.BeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory;
import com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.ExpressionCacheFactory;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
import com.opensymphony.xwork2.ognl.StrutsProxyCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlReflectionProvider;
import com.opensymphony.xwork2.ognl.OgnlUtil;
@@ -93,6 +95,7 @@ import com.opensymphony.xwork2.ognl.accessor.XWorkMethodAccessor;
import com.opensymphony.xwork2.util.OgnlTextParser;
import com.opensymphony.xwork2.util.PatternMatcher;
import com.opensymphony.xwork2.util.StrutsLocalizedTextProvider;
import com.opensymphony.xwork2.util.StrutsProxyCacheFactoryBean;
import com.opensymphony.xwork2.util.TextParser;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;
@@ -106,6 +109,8 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.conversion.StrutsConversionPropertiesProcessor;
import org.apache.struts2.conversion.UserConversionPropertiesProcessor;
import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.conversion.StrutsTypeConverterCreator;
import org.apache.struts2.conversion.StrutsTypeConverterHolder;
import org.apache.struts2.factory.StrutsResultFactory;
@@ -125,12 +130,8 @@ import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
* DefaultConfiguration
*
* @author Jason Carreira
* Created Feb 24, 2003 7:38:06 AM
*/
public class DefaultConfiguration implements Configuration {
@@ -145,6 +146,8 @@ public class DefaultConfiguration implements Configuration {
constants.put(StrutsConstants.STRUTS_OGNL_EXPRESSION_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_OGNL_BEANINFO_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_TYPE, OgnlCacheFactory.CacheType.BASIC);
constants.put(StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE, 10000);
constants.put(StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, Boolean.FALSE);
BOOTSTRAP_CONSTANTS = Collections.unmodifiableMap(constants);
}
@@ -224,7 +227,7 @@ public class DefaultConfiguration implements Configuration {
name, packageContext.getLocation());
} else {
throw new ConfigurationException("The package name '" + name
+ "' at location "+packageContext.getLocation()
+ "' at location " + packageContext.getLocation()
+ " is already been used by another package at location " + check.getLocation(),
packageContext);
}
@@ -257,7 +260,6 @@ public class DefaultConfiguration implements Configuration {
*
* @param providers list of ContainerProvider
* @return list of package providers
*
* @throws ConfigurationException in case of any configuration errors
*/
@Override
@@ -269,8 +271,7 @@ public class DefaultConfiguration implements Configuration {
ContainerProperties props = new ContainerProperties();
ContainerBuilder builder = new ContainerBuilder();
Container bootstrap = createBootstrapContainer(providers);
for (final ContainerProvider containerProvider : providers)
{
for (final ContainerProvider containerProvider : providers) {
bootstrap.inject(containerProvider);
containerProvider.init(this);
containerProvider.register(builder, props);
@@ -298,13 +299,16 @@ public class DefaultConfiguration implements Configuration {
setContext(container);
objectFactory = container.getInstance(ObjectFactory.class);
// Trigger late initialization of user conversion properties (WW-4291)
// This must happen after full container is built so SpringObjectFactory is available
container.getInstance(UserConversionPropertiesProcessor.class);
// Process the configuration providers first
for (final ContainerProvider containerProvider : providers)
{
for (final ContainerProvider containerProvider : providers) {
if (containerProvider instanceof PackageProvider) {
container.inject(containerProvider);
((PackageProvider)containerProvider).loadPackages();
packageProviders.add((PackageProvider)containerProvider);
((PackageProvider) containerProvider).loadPackages();
packageProviders.add((PackageProvider) containerProvider);
}
}
@@ -380,6 +384,8 @@ public class DefaultConfiguration implements Configuration {
.factory(ConversionAnnotationProcessor.class, DefaultConversionAnnotationProcessor.class, Scope.SINGLETON)
.factory(TypeConverterCreator.class, StrutsTypeConverterCreator.class, Scope.SINGLETON)
.factory(TypeConverterHolder.class, StrutsTypeConverterHolder.class, Scope.SINGLETON)
.factory(UserConversionPropertiesProvider.class, StrutsConversionPropertiesProcessor.class, Scope.SINGLETON)
.factory(UserConversionPropertiesProcessor.class, Scope.SINGLETON)
.factory(TextProvider.class, "system", DefaultTextProvider.class, Scope.SINGLETON)
.factory(LocalizedTextProvider.class, StrutsLocalizedTextProvider.class, Scope.SINGLETON)
@@ -393,6 +399,8 @@ public class DefaultConfiguration implements Configuration {
.factory(ExpressionCacheFactory.class, DefaultOgnlExpressionCacheFactory.class, Scope.SINGLETON)
.factory(BeanInfoCacheFactory.class, DefaultOgnlBeanInfoCacheFactory.class, Scope.SINGLETON)
.factory(ProxyCacheFactory.class, StrutsProxyCacheFactory.class, Scope.SINGLETON)
.factory(StrutsProxyCacheFactoryBean.class, Scope.SINGLETON)
.factory(OgnlUtil.class, Scope.SINGLETON)
.factory(SecurityMemberAccess.class, Scope.PROTOTYPE)
.factory(OgnlGuard.class, StrutsOgnlGuard.class, Scope.SINGLETON)
@@ -443,10 +451,9 @@ public class DefaultConfiguration implements Configuration {
Map<String, ActionConfig> actionConfigs = packageConfig.getAllActionConfigs();
for (Object o : actionConfigs.keySet()) {
String actionName = (String) o;
ActionConfig baseConfig = actionConfigs.get(actionName);
configs.put(actionName, buildFullActionConfig(packageConfig, baseConfig));
for (Map.Entry<String, ActionConfig> entry : actionConfigs.entrySet()) {
ActionConfig baseConfig = entry.getValue();
configs.put(entry.getKey(), buildFullActionConfig(packageConfig, baseConfig));
}
namespaceActionConfigs.put(namespace, configs);
@@ -487,8 +494,7 @@ public class DefaultConfiguration implements Configuration {
* @param baseConfig the ActionConfig which holds only the configuration specific to itself, without the defaults
* and inheritance
* @return a full ActionConfig for runtime configuration with all of the inherited and default params
* @throws com.opensymphony.xwork2.config.ConfigurationException
*
* @throws com.opensymphony.xwork2.config.ConfigurationException in case of any configuration errors
*/
private ActionConfig buildFullActionConfig(PackageConfig packageContext, ActionConfig baseConfig) throws ConfigurationException {
Map<String, String> params = new TreeMap<>(baseConfig.getParams());
@@ -500,7 +506,7 @@ public class DefaultConfiguration implements Configuration {
results.putAll(packageContext.getAllGlobalResults());
}
results.putAll(baseConfig.getResults());
results.putAll(baseConfig.getResults());
setDefaultResults(results, packageContext);
@@ -511,7 +517,7 @@ public class DefaultConfiguration implements Configuration {
if (defaultInterceptorRefName != null) {
interceptors.addAll(InterceptorBuilder.constructInterceptorReference(new PackageConfig.Builder(packageContext), defaultInterceptorRefName,
new LinkedHashMap<String, String>(), packageContext.getLocation(), objectFactory));
new LinkedHashMap<>(), packageContext.getLocation(), objectFactory));
}
}
@@ -523,14 +529,14 @@ public class DefaultConfiguration implements Configuration {
LOG.debug("Using pattern [{}] to match allowed methods when SMI is disabled!", methodRegex);
return new ActionConfig.Builder(baseConfig)
.addParams(params)
.addResultConfigs(results)
.defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
.interceptors(interceptors)
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
.setDefaultMethodRegex(methodRegex)
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
.build();
.addParams(params)
.addResultConfigs(results)
.defaultClassName(packageContext.getDefaultClassRef()) // fill in default if non class has been provided
.interceptors(interceptors)
.setStrictMethodInvocation(packageContext.isStrictMethodInvocation())
.setDefaultMethodRegex(methodRegex)
.addExceptionMappings(packageContext.getAllExceptionMappingConfigs())
.build();
}
@@ -546,8 +552,7 @@ public class DefaultConfiguration implements Configuration {
Map<String, String> namespaceConfigs,
PatternMatcher<int[]> matcher,
boolean appendNamedParameters,
boolean fallbackToEmptyNamespace)
{
boolean fallbackToEmptyNamespace) {
this.namespaceActionConfigs = namespaceActionConfigs;
this.namespaceConfigs = namespaceConfigs;
this.fallbackToEmptyNamespace = fallbackToEmptyNamespace;
@@ -617,6 +622,9 @@ public class DefaultConfiguration implements Configuration {
String defaultActionRef = namespaceConfigs.get(namespace);
if (defaultActionRef != null) {
config = actions.get(defaultActionRef);
if (config == null) {
config = namespaceActionConfigMatchers.get(namespace).match(defaultActionRef);
}
}
}
}
@@ -630,7 +638,7 @@ public class DefaultConfiguration implements Configuration {
* @return a Map of namespace - > Map of ActionConfig objects, with the key being the action name
*/
@Override
public Map<String, Map<String, ActionConfig>> getActionConfigs() {
public Map<String, Map<String, ActionConfig>> getActionConfigs() {
return namespaceActionConfigs;
}
@@ -664,7 +672,7 @@ public class DefaultConfiguration implements Configuration {
public void setConstants(ContainerBuilder builder) {
for (Object keyobj : keySet()) {
String key = (String)keyobj;
String key = (String) keyobj;
builder.factory(String.class, key, new LocatableConstantFactory<>(getProperty(key), getPropertyLocation(key)));
}
}
@@ -68,6 +68,8 @@ import com.opensymphony.xwork2.validator.ValidatorFileParser;
import ognl.PropertyAccessor;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.interceptor.exec.StrutsExecutorProvider;
import org.apache.struts2.url.QueryStringBuilder;
@@ -159,7 +161,9 @@ public class StrutsDefaultConfigurationProvider implements ConfigurationProvider
.factory(UrlEncoder.class, StrutsUrlEncoder.class, Scope.SINGLETON)
.factory(UrlDecoder.class, StrutsUrlDecoder.class, Scope.SINGLETON)
.factory(ExecutorProvider.class, StrutsExecutorProvider.class, Scope.SINGLETON);
.factory(ExecutorProvider.class, StrutsExecutorProvider.class, Scope.SINGLETON)
.factory(CspNonceReader.class, StrutsCspNonceReader.class, Scope.SINGLETON);
for (Map.Entry<String, Object> entry : DefaultConfiguration.BOOTSTRAP_CONSTANTS.entrySet()) {
props.setProperty(entry.getKey(), String.valueOf(entry.getValue()));
@@ -18,8 +18,8 @@
*/
package com.opensymphony.xwork2.factory;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.conversion.TypeConverter;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
@@ -33,16 +33,15 @@ public class StrutsConverterFactory implements ConverterFactory {
private static final Logger LOG = LogManager.getLogger(StrutsConverterFactory.class);
private Container container;
private ObjectFactory objectFactory;
@Inject
public void setContainer(Container container) {
this.container = container;
public void setObjectFactory(ObjectFactory objectFactory) {
this.objectFactory = objectFactory;
}
public TypeConverter buildConverter(Class<? extends TypeConverter> converterClass, Map<String, Object> extraContext) throws Exception {
LOG.debug("Creating converter of type [{}]", converterClass.getCanonicalName());
return container.inject(converterClass);
return (TypeConverter)objectFactory.buildBean(converterClass, extraContext);
}
}
@@ -26,7 +26,7 @@ import java.util.logging.Logger;
*
* @author Bob Lee (crazybob@google.com)
*/
class FinalizableReferenceQueue extends ReferenceQueue<Object> {
public class FinalizableReferenceQueue extends ReferenceQueue<Object> {
private static final Logger logger =
Logger.getLogger(FinalizableReferenceQueue.class.getName());
@@ -45,22 +45,49 @@ class FinalizableReferenceQueue extends ReferenceQueue<Object> {
logger.log(Level.SEVERE, "Error cleaning up after reference.", t);
}
private volatile Thread drainThread;
void start() {
Thread thread = new Thread("FinalizableReferenceQueue") {
@Override
public void run() {
while (true) {
while (!Thread.currentThread().isInterrupted()) {
try {
cleanUp(remove());
} catch (InterruptedException e) { /* ignore */ }
} catch (InterruptedException e) {
break;
}
}
}
};
thread.setDaemon(true);
thread.start();
this.drainThread = thread;
}
static ReferenceQueue<Object> instance = createAndStart();
/**
* Stops the background drain thread and releases the singleton instance,
* preventing the webapp classloader from being pinned after undeploy.
*/
public static synchronized void stopAndClear() {
if (instance instanceof FinalizableReferenceQueue) {
FinalizableReferenceQueue queue = (FinalizableReferenceQueue) instance;
Thread t = queue.drainThread;
if (t != null) {
t.interrupt();
try {
t.join(5000);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
t.setContextClassLoader(null);
queue.drainThread = null;
}
}
instance = null;
}
static volatile ReferenceQueue<Object> instance = createAndStart();
static FinalizableReferenceQueue createAndStart() {
FinalizableReferenceQueue queue = new FinalizableReferenceQueue();
@@ -98,8 +98,8 @@ public abstract class MethodFilterInterceptor extends AbstractInterceptor {
@Override
public String intercept(ActionInvocation invocation) throws Exception {
if (applyInterceptor(invocation)) {
return doIntercept(invocation);
if (applyInterceptor((org.apache.struts2.ActionInvocation) invocation)) {
return doIntercept((org.apache.struts2.ActionInvocation) invocation);
}
return invocation.invoke();
}
@@ -114,6 +114,10 @@ public abstract class MethodFilterInterceptor extends AbstractInterceptor {
return applyMethod;
}
protected boolean applyInterceptor(org.apache.struts2.ActionInvocation invocation) {
return applyInterceptor(ActionInvocation.adapt(invocation));
}
/**
* Subclasses must override to implement the interceptor logic.
*
@@ -123,4 +127,7 @@ public abstract class MethodFilterInterceptor extends AbstractInterceptor {
*/
protected abstract String doIntercept(ActionInvocation invocation) throws Exception;
protected String doIntercept(org.apache.struts2.ActionInvocation invocation) throws Exception {
return doIntercept(ActionInvocation.adapt(invocation));
}
}
@@ -23,6 +23,10 @@ import ognl.Ognl;
import java.util.Map;
/**
* @deprecated since 6.8.0, to be removed, see {@link ReflectionContextFactory}
*/
@Deprecated
public class OgnlReflectionContextFactory implements ReflectionContextFactory {
public Map createDefaultContext(Object root) {
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.opensymphony.xwork2.ognl;
/**
* A factory interface for ProxyUtil cache to be used with Struts DI mechanism.
* This allows the proxy detection cache type to be configurable via Struts constants.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
* @since 6.9.0
*/
public interface ProxyCacheFactory<Key, Value> extends OgnlCacheFactory<Key, Value> {
}
@@ -160,6 +160,9 @@ public class SecurityMemberAccess implements MemberAccess {
public boolean isAccessible(Map context, Object target, Member member, String propertyName) {
LOG.debug("Checking access for [target: {}, member: {}, property: {}]", target, member, propertyName);
if (member == null) {
throw new IllegalArgumentException("Member cannot be null!");
}
if (target != null) {
// Special case: Target is a Class object but not Class.class
if (Class.class.equals(target.getClass()) && !Class.class.equals(target)) {
@@ -228,7 +231,7 @@ public class SecurityMemberAccess implements MemberAccess {
return true;
}
if (!disallowProxyObjectAccess && target != null && ProxyUtil.isProxy(target)) {
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities to their underlying
// classes/members. This allows the allowlist capability to continue working and offer some level of
// protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate
@@ -0,0 +1,44 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.opensymphony.xwork2.ognl;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.EnumUtils;
import org.apache.struts2.StrutsConstants;
/**
* Struts Proxy Cache factory implementation for ProxyUtil caches.
* <p>
* This factory is used to create caches for proxy detection in ProxyUtil.
* The cache type and size can be configured via Struts constants.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
* @since 6.9.0
*/
public class StrutsProxyCacheFactory<Key, Value> extends DefaultOgnlCacheFactory<Key, Value>
implements ProxyCacheFactory<Key, Value> {
@Inject
public StrutsProxyCacheFactory(
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_MAXSIZE) String cacheMaxSize,
@Inject(value = StrutsConstants.STRUTS_PROXY_CACHE_TYPE) String defaultCacheType) {
super(Integer.parseInt(cacheMaxSize), EnumUtils.getEnumIgnoreCase(CacheType.class, defaultCacheType));
}
}
@@ -33,6 +33,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.dispatcher.InternalDestroyable;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
@@ -53,7 +54,7 @@ import static org.apache.commons.lang3.BooleanUtils.toBoolean;
* @author Rainer Hermanns
* @version $Revision$
*/
public class CompoundRootAccessor implements RootAccessor {
public class CompoundRootAccessor implements RootAccessor, InternalDestroyable {
/**
* Used by OGNl to generate bytecode
@@ -74,6 +75,22 @@ public class CompoundRootAccessor implements RootAccessor {
private final static Logger LOG = LogManager.getLogger(CompoundRootAccessor.class);
private final static Class[] EMPTY_CLASS_ARRAY = new Class[0];
private static final Map<MethodCall, Boolean> invalidMethods = new ConcurrentHashMap<>();
/**
* Clears the cached invalid methods map to prevent classloader leaks on hot redeploy.
*/
public static void clearCache() {
invalidMethods.clear();
}
/**
* @since 6.9.0
*/
@Override
public void destroy() {
clearCache();
}
private boolean devMode;
private boolean disallowCustomOgnlMap;
@@ -38,6 +38,7 @@ import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.XMLConstants;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
@@ -104,6 +105,7 @@ public class DomHelper {
try {
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException | SAXNotRecognizedException | SAXNotSupportedException e) {
throw new StrutsException("Unable to disable resolving external entities!", e);
}
@@ -0,0 +1,71 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.opensymphony.xwork2.util;
import java.util.function.Supplier;
/**
* A thread-safe lazy reference that computes its value on first access using
* double-checked locking. The cached value can be invalidated via {@link #reset()},
* causing the next {@link #get()} call to recompute the value.
*
* @param <T> the type of the lazily computed value
* @since 6.9.0
*/
public class LazyRef<T> implements Supplier<T> {
private final Supplier<T> factory;
private volatile T value;
/**
* Creates a new LazyRef with the given factory supplier.
*
* @param factory the supplier used to compute the value; must not be null
*/
public LazyRef(Supplier<T> factory) {
this.factory = factory;
}
/**
* Returns the cached value, computing it on first access or after a {@link #reset()}.
*
* @return the computed value
*/
@Override
public T get() {
T result = value;
if (result == null) {
synchronized (this) {
result = value;
if (result == null) {
result = factory.get();
value = result;
}
}
}
return result;
}
/**
* Invalidates the cached value so the next {@link #get()} call recomputes it.
*/
public void reset() {
value = null;
}
}
@@ -21,6 +21,7 @@ package com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.ognl.DefaultOgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCache;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
@@ -41,7 +42,6 @@ import static java.lang.reflect.Modifier.isPublic;
* <p>
* Various utility methods dealing with proxies
* </p>
*
*/
public class ProxyUtil {
private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised";
@@ -51,15 +51,56 @@ public class ProxyUtil {
private static final String HIBERNATE_HIBERNATEPROXY_CLASS_NAME = "org.hibernate.proxy.HibernateProxy";
private static final int CACHE_MAX_SIZE = 10000;
private static final int CACHE_INITIAL_CAPACITY = 256;
private static final OgnlCache<Class<?>, Boolean> isProxyCache = new DefaultOgnlCacheFactory<Class<?>, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final OgnlCache<Member, Boolean> isProxyMemberCache = new DefaultOgnlCacheFactory<Member, Boolean>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
private static final boolean HIBERNATE_AVAILABLE = detectHibernate();
private static boolean detectHibernate() {
try {
Class.forName("org.hibernate.proxy.HibernateProxy");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
// Holder for the cache factory (set by container)
private static volatile ProxyCacheFactory<?, ?> cacheFactory;
// Lazy-initialized caches with reset support
private static final LazyRef<OgnlCache<Class<?>, Boolean>> isProxyCache =
new LazyRef<>(ProxyUtil::createCache);
private static final LazyRef<OgnlCache<Member, Boolean>> isProxyMemberCache =
new LazyRef<>(ProxyUtil::createCache);
/**
* Sets the cache factory. Called by the container during initialization.
* Resets existing caches so they are recreated with the new factory.
*
* @param factory the cache factory to use for creating proxy caches
* @since 6.9.0
*/
public static void setProxyCacheFactory(ProxyCacheFactory<?, ?> factory) {
cacheFactory = factory;
isProxyCache.reset();
isProxyMemberCache.reset();
}
@SuppressWarnings("unchecked")
private static <K, V> OgnlCache<K, V> createCache() {
if (cacheFactory != null) {
return ((ProxyCacheFactory<K, V>) cacheFactory).buildOgnlCache(
CACHE_MAX_SIZE, CACHE_INITIAL_CAPACITY, 0.75f, cacheFactory.getDefaultCacheType());
}
// Fallback to BASIC if container hasn't initialized yet
return new DefaultOgnlCacheFactory<K, V>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.BASIC, CACHE_INITIAL_CAPACITY).buildOgnlCache();
}
/**
* Determine the ultimate target class of the given instance, traversing
* not only a top-level proxy but any number of nested proxies as well &mdash;
* as long as possible without side effects.
*
* @param candidate the instance to check (might be a proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
@@ -78,23 +119,26 @@ public class ProxyUtil {
/**
* Check whether the given object is a proxy.
*
* @param object the object to check
*/
public static boolean isProxy(Object object) {
if (object == null) return false;
Class<?> clazz = object.getClass();
Boolean flag = isProxyCache.get(clazz);
Boolean flag = isProxyCache.get().get(clazz);
if (flag != null) {
return flag;
}
boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object);
isProxyCache.put(clazz, isProxy);
isProxyCache.get().put(clazz, isProxy);
return isProxy;
}
/**
* Check whether the given member is a proxy member of a proxy object or is a static proxy member.
*
* @param member the member to check
* @param object the object to check
*/
@@ -103,14 +147,14 @@ public class ProxyUtil {
return false;
}
Boolean flag = isProxyMemberCache.get(member);
Boolean flag = isProxyMemberCache.get().get(member);
if (flag != null) {
return flag;
}
boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member);
isProxyMemberCache.put(member, isProxyMember);
isProxyMemberCache.get().put(member, isProxyMember);
return isProxyMember;
}
@@ -120,8 +164,9 @@ public class ProxyUtil {
* @param object the object to check
*/
public static boolean isHibernateProxy(Object object) {
if (!HIBERNATE_AVAILABLE) return false;
try {
return HibernateProxy.class.isAssignableFrom(object.getClass());
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (NoClassDefFoundError ignored) {
return false;
}
@@ -133,6 +178,7 @@ public class ProxyUtil {
* @param member the member to check
*/
public static boolean isHibernateProxyMember(Member member) {
if (!HIBERNATE_AVAILABLE) return false;
try {
Class<?> clazz = ClassLoaderUtil.loadClass(HIBERNATE_HIBERNATEPROXY_CLASS_NAME, ProxyUtil.class);
return hasMember(clazz, member);
@@ -146,6 +192,7 @@ public class ProxyUtil {
* Determine the ultimate target class of the given spring bean instance, traversing
* not only a top-level spring proxy but any number of nested spring proxies as well &mdash;
* as long as possible without side effects, that is, just for singleton targets.
*
* @param candidate the instance to check (might be a spring AOP proxy)
* @return the ultimate target class (or the plain class of the given
* object as fallback; never {@code null})
@@ -169,6 +216,7 @@ public class ProxyUtil {
/**
* Check whether the given object is a Spring proxy.
*
* @param object the object to check
*/
private static boolean isSpringAopProxy(Object object) {
@@ -179,6 +227,7 @@ public class ProxyUtil {
/**
* Check whether the given member is a member of a spring proxy.
*
* @param member the member to check
*/
private static boolean isSpringProxyMember(Member member) {
@@ -200,6 +249,7 @@ public class ProxyUtil {
/**
* Obtain the singleton target object behind the given spring proxy, if any.
*
* @param candidate the (potential) spring proxy to check
* @return the singleton target object, or {@code null} in any other case
* (not a spring proxy, not an existing singleton target)
@@ -220,6 +270,7 @@ public class ProxyUtil {
/**
* Check whether the specified class is a CGLIB-generated class.
*
* @param clazz the class to check
*/
private static boolean isCglibProxyClass(Class<?> clazz) {
@@ -228,7 +279,8 @@ public class ProxyUtil {
/**
* Check whether the given class implements an interface with a given class name.
* @param clazz the class to check
*
* @param clazz the class to check
* @param ifaceClassName the interface class name to check
*/
private static boolean implementsInterface(Class<?> clazz, String ifaceClassName) {
@@ -242,7 +294,8 @@ public class ProxyUtil {
/**
* Check whether the given class has a given member.
* @param clazz the class to check
*
* @param clazz the class to check
* @param member the member to check
*/
private static boolean hasMember(Class<?> clazz, Member member) {
@@ -263,6 +316,7 @@ public class ProxyUtil {
* @return the target instance of the given object if it is a Hibernate proxy object, otherwise the given object
*/
public static Object getHibernateProxyTarget(Object object) {
if (!HIBERNATE_AVAILABLE) return object;
try {
return Hibernate.unproxy(object);
} catch (NoClassDefFoundError ignored) {
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.ProxyCacheFactory;
/**
* Bean that wires the ProxyCacheFactory to ProxyUtil during container initialization.
* <p>
* This bean is created by the container and receives the configured ProxyCacheFactory
* via dependency injection, then passes it to the static ProxyUtil class.
*
* @since 6.9.0
*/
public class StrutsProxyCacheFactoryBean {
@Inject
public StrutsProxyCacheFactoryBean(ProxyCacheFactory<?, ?> proxyCacheFactory) {
ProxyUtil.setProxyCacheFactory(proxyCacheFactory);
}
}
@@ -21,6 +21,7 @@ package com.opensymphony.xwork2.util.fs;
import com.opensymphony.xwork2.FileManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.dispatcher.InternalDestroyable;
import java.io.IOException;
import java.io.InputStream;
@@ -33,7 +34,7 @@ import java.util.regex.Pattern;
/**
* Default implementation of {@link FileManager}
*/
public class DefaultFileManager implements FileManager {
public class DefaultFileManager implements FileManager, InternalDestroyable {
private static Logger LOG = LogManager.getLogger(DefaultFileManager.class);
@@ -43,6 +44,22 @@ public class DefaultFileManager implements FileManager {
protected static final Map<String, Revision> files = Collections.synchronizedMap(new HashMap<String, Revision>());
private static final List<URL> lazyMonitoredFilesCache = Collections.synchronizedList(new ArrayList<URL>());
/**
* Clears both the files and lazy monitored files caches to prevent classloader leaks on hot redeploy.
*/
public static void clearCache() {
files.clear();
lazyMonitoredFilesCache.clear();
}
/**
* @since 6.9.0
*/
@Override
public void destroy() {
clearCache();
}
protected boolean reloadingConfigs = false;
public DefaultFileManager() {
@@ -20,6 +20,10 @@ package com.opensymphony.xwork2.util.reflection;
import java.util.Map;
/**
* @deprecated since 6.8.0, avoid using this interface and any of its implementation, it's going to be removed soon
*/
@Deprecated
public interface ReflectionContextFactory {
/**
* Creates and returns a new standard naming context for evaluating an OGNL
@@ -257,9 +257,13 @@ public class ValidationInterceptor extends MethodFilterInterceptor {
}
}
protected void doBeforeInvocation(org.apache.struts2.ActionInvocation invocation) throws Exception {
doBeforeInvocation(ActionInvocation.adapt(invocation));
}
@Override
protected String doIntercept(ActionInvocation invocation) throws Exception {
doBeforeInvocation(invocation);
doBeforeInvocation((org.apache.struts2.ActionInvocation) invocation);
return invocation.invoke();
}
@@ -93,9 +93,11 @@ public interface ActionProxy {
String getMethod();
/**
* Gets status of the method value's initialization.
* Gets status of the method value's initialization. Returns {@code true} when the method was explicitly provided
* (e.g. via URL parameter, wildcard substitution, or action configuration), and {@code false} only when the
* framework defaults to {@code "execute"} because no method was specified anywhere.
*
* @return true if the method returned by getMethod() is not a default initializer value.
* @return true if the method returned by getMethod() is not the default "execute" fallback.
*/
boolean isMethodSpecified();
@@ -411,6 +411,7 @@ public final class StrutsConstants {
public static final String STRUTS_CONVERTER_ANNOTATION_PROCESSOR = "struts.converter.annotation.processor";
public static final String STRUTS_CONVERTER_CREATOR = "struts.converter.creator";
public static final String STRUTS_CONVERTER_HOLDER = "struts.converter.holder";
public static final String STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER = "struts.converter.userPropertiesProvider";
public static final String STRUTS_EXPRESSION_PARSER = "struts.expression.parser";
@@ -509,4 +510,25 @@ public final class StrutsConstants {
/** See {@link org.apache.struts2.interceptor.exec.ExecutorProvider} */
public static final String STRUTS_EXECUTOR_PROVIDER = "struts.executor.provider";
/**
* See {@link org.apache.struts2.interceptor.csp.CspNonceReader}
* @since 6.8.0
*/
public static final String STRUTS_CSP_NONCE_READER = "struts.csp.nonce.reader";
public static final String STRUTS_CSP_NONCE_SOURCE = "struts.csp.nonce.source";
/**
* Specifies the type of cache to use for proxy detection in ProxyUtil.
* Valid values defined in {@link com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType}.
* Default is 'basic' (no Caffeine dependency required).
* @since 6.8.0
*/
public static final String STRUTS_PROXY_CACHE_TYPE = "struts.proxy.cacheType";
/**
* Specifies the maximum cache size for proxy detection caches in ProxyUtil.
* @since 6.8.0
*/
public static final String STRUTS_PROXY_CACHE_MAXSIZE = "struts.proxy.cacheMaxSize";
}
@@ -68,6 +68,13 @@ public class Component {
*/
protected static ConcurrentMap<Class<?>, Collection<String>> standardAttributesMap = new ConcurrentHashMap<>();
/**
* Clears the cached standard attributes map to prevent classloader leaks on hot redeploy.
*/
public static void clearStandardAttributesMap() {
standardAttributesMap.clear();
}
protected boolean devMode = false;
protected boolean escapeHtmlBody = false;
protected ValueStack stack;
@@ -50,17 +50,16 @@ import javax.servlet.http.HttpServletResponse;
* </pre>
*/
@StrutsTag(
name="textfield",
tldTagClass="org.apache.struts2.views.jsp.ui.TextFieldTag",
description="Render an HTML input field of type text",
allowDynamicAttributes=true)
name = "textfield",
tldTagClass = "org.apache.struts2.views.jsp.ui.TextFieldTag",
description = "Render an HTML input field of type text",
allowDynamicAttributes = true)
public class TextField extends UIBean {
/**
* The name of the default template for the TextFieldTag
*/
final public static String TEMPLATE = "text";
protected String maxlength;
protected String readonly;
protected String size;
@@ -95,27 +94,22 @@ public class TextField extends UIBean {
}
@StrutsTagAttribute(description="HTML maxlength attribute", type="Integer")
@StrutsTagAttribute(description = "HTML maxlength attribute", type = "Integer")
public void setMaxlength(String maxlength) {
this.maxlength = maxlength;
}
@StrutsTagAttribute(description="Deprecated. Use maxlength instead.", type="Integer")
public void setMaxLength(String maxlength) {
this.maxlength = maxlength;
}
@StrutsTagAttribute(description="Whether the input is readonly", type="Boolean", defaultValue="false")
@StrutsTagAttribute(description = "Whether the input is readonly", type = "Boolean", defaultValue = "false")
public void setReadonly(String readonly) {
this.readonly = readonly;
}
@StrutsTagAttribute(description="HTML size attribute", type="Integer")
@StrutsTagAttribute(description = "HTML size attribute", type = "Integer")
public void setSize(String size) {
this.size = size;
}
@StrutsTagAttribute(description="Specifies the html5 type element to display. e.g. text, email, url", defaultValue="text")
@StrutsTagAttribute(description = "Specifies the html5 type element to display. e.g. text, email, url", defaultValue = "text")
public void setType(String type) {
this.type = type;
}
@@ -33,6 +33,7 @@ import org.apache.struts2.components.template.TemplateEngineManager;
import org.apache.struts2.components.template.TemplateRenderingContext;
import org.apache.struts2.dispatcher.AttributeMap;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.util.ComponentUtils;
import org.apache.struts2.util.TextProviderHelper;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
@@ -40,7 +41,6 @@ import org.apache.struts2.views.util.ContextUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.Writer;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -528,6 +528,8 @@ public abstract class UIBean extends Component {
protected TemplateEngineManager templateEngineManager;
protected CspNonceReader cspNonceReader;
@Inject(StrutsConstants.STRUTS_UI_TEMPLATEDIR)
public void setDefaultTemplateDir(String dir) {
this.defaultTemplateDir = dir;
@@ -553,6 +555,11 @@ public abstract class UIBean extends Component {
this.templateEngineManager = mgr;
}
@Inject
public void setCspNonceReader(CspNonceReader cspNonceReader) {
this.cspNonceReader = cspNonceReader;
}
@Override
public boolean end(Writer writer, String body) {
evaluateParams();
@@ -886,13 +893,12 @@ public abstract class UIBean extends Component {
}
// to be used with the CSP interceptor - adds the nonce value as a parameter to be accessed from ftl files
HttpSession session = stack.getActionContext().getServletRequest().getSession(false);
Object nonceValue = session != null ? session.getAttribute("nonce") : null;
CspNonceReader.NonceValue nonceValue = cspNonceReader.readNonceValue(stack);
if (nonceValue != null) {
addParameter("nonce", nonceValue.toString());
if (nonceValue.isNonceValueSet()) {
addParameter("nonce", nonceValue.getNonceValue());
} else {
LOG.debug("Session is not active, cannot obtain nonce value");
LOG.debug("Nonce not defined in: {}", nonceValue.getSource());
}
evaluateExtraParams();
@@ -30,7 +30,44 @@ import org.apache.logging.log4j.Logger;
import java.util.Properties;
/**
* TODO lukaszlenart: write a JavaDoc
* Base implementation of {@link BeanSelectionProvider} that provides bean aliasing functionality.
* <p>
* This class provides the {@link #alias(Class, String, ContainerBuilder, Properties, Scope)} method
* which is used to select and register bean implementations based on configuration properties.
* </p>
*
* <h2>Bean Selection Process</h2>
* <p>
* The {@code alias} method selects a bean implementation using the following process:
* </p>
* <ol>
* <li>Read the property value for the given key from the configuration properties</li>
* <li>If no property is set, use {@value #DEFAULT_BEAN_NAME} as the default bean name</li>
* <li>Check if a bean with that name already exists in the container:
* <ul>
* <li>If found, alias it to {@link Container#DEFAULT_NAME} making it the default</li>
* <li>If not found, try to load the property value as a fully qualified class name</li>
* </ul>
* </li>
* <li>If class loading succeeds, register the class as a factory for the interface type</li>
* <li>If class loading fails and the name is not the default, create a delegate factory
* that will resolve the bean through {@link ObjectFactory} at runtime. This allows
* Spring bean names to be used in configuration.</li>
* </ol>
*
* <h2>Usage Example</h2>
* <pre>
* // In struts.properties or struts.xml:
* // struts.objectFactory = spring
* // struts.converter.collection = myCustomCollectionConverter
*
* // In a subclass:
* alias(ObjectFactory.class, StrutsConstants.STRUTS_OBJECTFACTORY, builder, props);
* alias(CollectionConverter.class, StrutsConstants.STRUTS_CONVERTER_COLLECTION, builder, props);
* </pre>
*
* @see BeanSelectionProvider
* @see StrutsBeanSelectionProvider
*/
public abstract class AbstractBeanSelectionProvider implements BeanSelectionProvider {
@@ -73,7 +110,7 @@ public abstract class AbstractBeanSelectionProvider implements BeanSelectionProv
// Perhaps a spring bean id, so we'll delegate to the object factory at runtime
LOG.trace("Choosing bean ({}) for ({}) to be loaded from the ObjectFactory", foundName, type.getName());
if (DEFAULT_BEAN_NAME.equals(foundName)) {
// Probably an optional bean, will ignore
LOG.trace("No bean registered for type ({}) with default name '{}', skipping as optional", type.getName(), DEFAULT_BEAN_NAME);
} else {
if (ObjectFactory.class != type) {
builder.factory(type, new ObjectFactoryDelegateFactory(foundName, type), scope);
@@ -103,7 +140,7 @@ public abstract class AbstractBeanSelectionProvider implements BeanSelectionProv
try {
return objFactory.buildBean(name, null, true);
} catch (ClassNotFoundException ex) {
throw new ConfigurationException("Unable to load bean "+type.getName()+" ("+name+")");
throw new ConfigurationException(String.format("Unable to load bean %s (name = %s)", type.getName(), name));
}
}
@@ -65,10 +65,12 @@ import ognl.MethodAccessor;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.components.UrlRenderer;
import org.apache.struts2.components.date.DateFormatter;
import org.apache.struts2.conversion.UserConversionPropertiesProvider;
import org.apache.struts2.dispatcher.DispatcherErrorHandler;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.ognl.OgnlGuard;
import org.apache.struts2.url.QueryStringBuilder;
@@ -87,7 +89,7 @@ import org.apache.struts2.views.util.UrlHelper;
*
* <p>
* The following is a list of the allowed extension points:
*
* <p>
* <!-- START SNIPPET: extensionPoints -->
* <table border="1" summary="">
* <tr>
@@ -352,7 +354,7 @@ import org.apache.struts2.views.util.UrlHelper;
* <td>Provides access to resource bundles used to localise messages (since 2.5.11)</td>
* </tr>
* </table>
*
* <p>
* <!-- END SNIPPET: extensionPoints -->
*
* <p>
@@ -404,6 +406,7 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
alias(ConversionAnnotationProcessor.class, StrutsConstants.STRUTS_CONVERTER_ANNOTATION_PROCESSOR, builder, props);
alias(TypeConverterCreator.class, StrutsConstants.STRUTS_CONVERTER_CREATOR, builder, props);
alias(TypeConverterHolder.class, StrutsConstants.STRUTS_CONVERTER_HOLDER, builder, props);
alias(UserConversionPropertiesProvider.class, StrutsConstants.STRUTS_CONVERTER_USER_PROPERTIES_PROVIDER, builder, props);
alias(TextProvider.class, StrutsConstants.STRUTS_TEXT_PROVIDER, builder, props, Scope.PROTOTYPE);
alias(TextProviderFactory.class, StrutsConstants.STRUTS_TEXT_PROVIDER_FACTORY, builder, props, Scope.PROTOTYPE);
@@ -450,6 +453,8 @@ public class StrutsBeanSelectionProvider extends AbstractBeanSelectionProvider {
alias(ExecutorProvider.class, StrutsConstants.STRUTS_EXECUTOR_PROVIDER, builder, props, Scope.SINGLETON);
alias(CspNonceReader.class, StrutsConstants.STRUTS_CSP_NONCE_READER, builder, props, Scope.SINGLETON);
switchDevMode(props);
}
@@ -35,7 +35,7 @@ import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable {
public class StrutsConversionPropertiesProcessor implements ConversionPropertiesProcessor, EarlyInitializable, UserConversionPropertiesProvider {
private static final Logger LOG = LogManager.getLogger(StrutsConversionPropertiesProcessor.class);
@@ -58,8 +58,27 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
@Override
public void init() {
LOG.debug("Processing default conversion properties files");
// Early phase: Only process framework defaults (class names only)
// User properties are processed later in initUserConversions() when
// SpringObjectFactory is available for bean name resolution (WW-4291)
LOG.debug("Processing default conversion properties files (early phase)");
processRequired(STRUTS_DEFAULT_CONVERSION_PROPERTIES);
}
/**
* Process user conversion properties. Called during late initialization
* when SpringObjectFactory is available for bean name resolution.
* <p>
* This allows users to reference Spring bean names in struts-conversion.properties
* instead of only fully qualified class names.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @since 7.2.0
*/
@Override
public void initUserConversions() {
LOG.debug("Processing user conversion properties files (late phase)");
process(STRUTS_CONVERSION_PROPERTIES);
process(XWORK_CONVERSION_PROPERTIES);
}
@@ -78,7 +97,7 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
while (resources.hasNext()) {
if (XWORK_CONVERSION_PROPERTIES.equals(propsName)) {
LOG.warn("Instead of using deprecated {} please use the new file name {}",
XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
XWORK_CONVERSION_PROPERTIES, STRUTS_CONVERSION_PROPERTIES);
}
URL url = resources.next();
Properties props = new Properties();
@@ -86,8 +105,7 @@ public class StrutsConversionPropertiesProcessor implements ConversionProperties
LOG.debug("Processing conversion file [{}]", propsName);
for (Object o : props.entrySet()) {
Map.Entry entry = (Map.Entry) o;
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String key = (String) entry.getKey();
try {
@@ -0,0 +1,55 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.conversion;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.opensymphony.xwork2.inject.Initializable;
import com.opensymphony.xwork2.inject.Inject;
/**
* Late initialization processor for user conversion properties.
* <p>
* Processes struts-conversion.properties and xwork-conversion.properties
* after the full container is built, allowing Spring bean name resolution.
* This enables users to reference Spring bean names instead of only fully
* qualified class names in their conversion property files.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @see UserConversionPropertiesProvider
* @since 6.9.0
*/
public class UserConversionPropertiesProcessor implements Initializable {
private static final Logger LOG = LogManager.getLogger(UserConversionPropertiesProcessor.class);
private UserConversionPropertiesProvider provider;
@Inject
public void setUserConversionPropertiesProvider(UserConversionPropertiesProvider provider) {
this.provider = provider;
}
@Override
public void init() {
LOG.debug("Initializing user conversion properties via late initialization");
provider.initUserConversions();
}
}
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.conversion;
/**
* Interface for processors that support late initialization of user conversion properties.
* <p>
* Implementations provide user conversion properties processing after the full container
* is built, allowing Spring bean name resolution for type converters.
* </p>
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
* @since 6.9.0
*/
public interface UserConversionPropertiesProvider {
/**
* Process user conversion properties (struts-conversion.properties, xwork-conversion.properties).
* Called during late initialization when SpringObjectFactory is available.
*/
void initUserConversions();
}
@@ -86,7 +86,7 @@ public class AttributeMap extends AbstractMap<String, Object> {
PageContext pc = getPageContext();
if (pc == null) {
if (pc == null || pc.getRequest() == null) {
RequestMap request = (RequestMap) context.get(DispatcherConstants.REQUEST);
SessionMap session = (SessionMap) context.get(DispatcherConstants.SESSION);
ApplicationMap application = (ApplicationMap) context.get(DispatcherConstants.APPLICATION);
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.components.Component;
/**
* Clears {@link Component}'s static standard attributes cache to prevent
* classloader leaks on hot redeploy. Wrapper is needed because {@code Component}
* requires constructor arguments that prevent direct container instantiation.
*
* @since 6.9.0
*/
public class ComponentCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
Component.clearStandardAttributesMap();
}
}
@@ -21,27 +21,70 @@ package org.apache.struts2.dispatcher;
import com.opensymphony.xwork2.inject.Container;
/**
* Simple class to hold Container instance per thread to minimise number of attempts
* to read configuration and build each time a new configuration.
* Per-thread cache for the Container instance, minimising repeated reads from
* {@link com.opensymphony.xwork2.config.ConfigurationManager}.
* <p>
* As ContainerHolder operates just per thread (which means per request) there is no need
* to check if configuration changed during the same request. If changed between requests,
* first call to store Container in ContainerHolder will be with the new configuration.
* WW-5537: Uses a ThreadLocal for per-request isolation with a volatile generation
* counter for cross-thread invalidation during app undeploy. When
* {@link #invalidateAll()} is called, all threads see the updated generation on their
* next {@link #get()} and return {@code null}, forcing a fresh read from
* ConfigurationManager. This prevents classloader leaks caused by idle pool threads
* retaining stale Container references after hot redeployment.
*/
class ContainerHolder {
private static final ThreadLocal<Container> instance = new ThreadLocal<>();
private static final ThreadLocal<CachedContainer> instance = new ThreadLocal<>();
/**
* Incremented on each {@link #invalidateAll()} call. Threads compare their cached
* generation against this value to detect staleness.
*/
private static volatile long generation = 0;
public static void store(Container newInstance) {
instance.set(newInstance);
instance.set(new CachedContainer(newInstance, generation));
}
public static Container get() {
return instance.get();
CachedContainer cached = instance.get();
if (cached == null) {
return null;
}
if (cached.generation != generation) {
instance.remove();
return null;
}
return cached.container;
}
/**
* Clears the current thread's cached container reference.
* Used for per-request cleanup.
*/
public static void clear() {
instance.remove();
}
/**
* Invalidates all threads' cached container references by advancing the generation
* counter. Each thread will detect the stale generation on its next {@link #get()}
* call and clear its own ThreadLocal. Also clears the calling thread immediately.
* <p>
* Used during application undeploy ({@link Dispatcher#cleanup()}) to ensure idle
* pool threads do not pin the webapp classloader via retained Container references.
*/
public static void invalidateAll() {
generation++;
instance.remove();
}
private static class CachedContainer {
final Container container;
final long generation;
CachedContainer(Container container, long generation) {
this.container = container;
this.generation = generation;
}
}
}
@@ -0,0 +1,53 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import javax.servlet.ServletContext;
/**
* Extension of {@link InternalDestroyable} for components that require
* {@link ServletContext} during cleanup (e.g. clearing servlet-scoped caches).
*
* <p>During {@link Dispatcher#cleanup()}, the discovery loop checks each
* {@code InternalDestroyable} bean: if it implements this subinterface,
* {@link #destroy(ServletContext)} is called instead of {@link #destroy()}.</p>
*
* @since 6.9.0
* @see InternalDestroyable
* @see Dispatcher#cleanup()
*/
public interface ContextAwareDestroyable extends InternalDestroyable {
/**
* Releases state that requires access to the {@link ServletContext}.
*
* @param servletContext the current servlet context, may be {@code null}
* if the Dispatcher was created without one
*/
void destroy(ServletContext servletContext);
/**
* Default no-op — {@link Dispatcher} calls
* {@link #destroy(ServletContext)} instead when it recognises this type.
*/
@Override
default void destroy() {
// no-op: context-aware variant is the real entry point
}
}
@@ -451,7 +451,37 @@ public class Dispatcher {
* Releases all instances bound to this dispatcher instance.
*/
public void cleanup() {
// clean up ObjectFactory
destroyObjectFactory();
// clean up Dispatcher itself for this thread
instance.remove();
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
destroyDispatcherListeners();
destroyInterceptors();
destroyInternalBeans();
// WW-5537: Invalidate all threads' cached Container references to prevent
// classloader leaks from idle pool threads retaining stale references after undeploy.
ContainerHolder.invalidateAll();
//cleanup action context
ActionContext.clear();
// clean up configuration
configurationManager.destroyConfiguration();
configurationManager = null;
}
/**
* Destroys the {@link ObjectFactory} if it implements {@link ObjectFactoryDestroyable}.
* Called at the beginning of {@link #cleanup()}.
*
* @since 6.9.0
*/
protected void destroyObjectFactory() {
if (objectFactory == null) {
LOG.warn("Object Factory is null, something is seriously wrong, no clean up will be performed");
}
@@ -459,23 +489,36 @@ public class Dispatcher {
try {
((ObjectFactoryDestroyable) objectFactory).destroy();
} catch (Exception e) {
// catch any exception that may occur during destroy() and log it
LOG.error("Exception occurred while destroying ObjectFactory [{}]", objectFactory.toString(), e);
}
}
}
// clean up Dispatcher itself for this thread
instance.remove();
servletContext.setAttribute(StrutsStatics.SERVLET_DISPATCHER, null);
// clean up DispatcherListeners
/**
* Notifies all registered {@link DispatcherListener}s that this dispatcher
* is being destroyed, then clears the listener list.
*
* @since 6.9.0
*/
protected void destroyDispatcherListeners() {
if (!dispatcherListeners.isEmpty()) {
for (DispatcherListener l : dispatcherListeners) {
l.dispatcherDestroyed(this);
}
// WW-5537: Clear the static listener list to release references that may
// pin the webapp classloader after undeploy. Listeners must be re-registered
// if a new Dispatcher is created (e.g. on redeploy).
dispatcherListeners.clear();
}
}
// clean up all interceptors by calling their destroy() method
/**
* Destroys all interceptors registered in the current configuration.
* Called during {@link #cleanup()} before {@link #destroyInternalBeans()}.
*
* @since 6.9.0
*/
protected void destroyInterceptors() {
Set<Interceptor> interceptors = new HashSet<>();
Collection<PackageConfig> packageConfigs = configurationManager.getConfiguration().getPackageConfigs().values();
for (PackageConfig packageConfig : packageConfigs) {
@@ -490,16 +533,38 @@ public class Dispatcher {
for (Interceptor interceptor : interceptors) {
interceptor.destroy();
}
}
// Clear container holder when application is unloaded / server shutdown
ContainerHolder.clear();
//cleanup action context
ActionContext.clear();
// clean up configuration
configurationManager.destroyConfiguration();
configurationManager = null;
/**
* Discovers and invokes all {@link InternalDestroyable} beans registered
* in the container, clearing static caches and stopping daemon threads
* to prevent classloader leaks during hot redeployment (WW-5537).
*
* <p>Beans implementing {@link ContextAwareDestroyable} receive the
* {@link javax.servlet.ServletContext} via
* {@link ContextAwareDestroyable#destroy(javax.servlet.ServletContext)}.</p>
*
* @since 6.9.0
*/
protected void destroyInternalBeans() {
if (configurationManager != null && configurationManager.getConfiguration() != null) {
Container container = configurationManager.getConfiguration().getContainer();
Set<String> destroyableNames = container.getInstanceNames(InternalDestroyable.class);
for (String name : destroyableNames) {
try {
InternalDestroyable destroyable = container.getInstance(InternalDestroyable.class, name);
if (destroyable instanceof ContextAwareDestroyable) {
((ContextAwareDestroyable) destroyable).destroy(servletContext);
} else {
destroyable.destroy();
}
} catch (Exception e) {
LOG.warn("Error during internal cleanup [{}]", name, e);
}
}
} else {
LOG.warn("ConfigurationManager is null during cleanup, InternalDestroyable beans will not be invoked");
}
}
private void init_FileManager() throws ClassNotFoundException {
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import com.opensymphony.xwork2.inject.util.FinalizableReferenceQueue;
/**
* Adapter that exposes {@link FinalizableReferenceQueue#stopAndClear()} as an
* {@link InternalDestroyable} bean, since {@code FinalizableReferenceQueue}
* has a private constructor and cannot be directly registered in the container.
*
* @since 6.9.0
*/
public class FinalizableReferenceQueueDestroyable implements InternalDestroyable {
@Override
public void destroy() {
FinalizableReferenceQueue.stopAndClear();
}
}
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import freemarker.ext.beans.BeansWrapper;
import freemarker.template.Configuration;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import javax.servlet.ServletContext;
/**
* WW-5537: Clears FreeMarker's template and class introspection caches
* stored in {@link ServletContext} during application undeploy, preventing
* classloader leaks.
*
* @since 6.9.0
*/
public class FreemarkerCacheDestroyable implements ContextAwareDestroyable {
private static final Logger LOG = LogManager.getLogger(FreemarkerCacheDestroyable.class);
@Override
public void destroy(ServletContext servletContext) {
if (servletContext == null) {
return;
}
Object fmConfig = servletContext.getAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
if (fmConfig instanceof Configuration) {
Configuration cfg = (Configuration) fmConfig;
cfg.clearTemplateCache();
cfg.clearEncodingMap();
if (cfg.getObjectWrapper() instanceof BeansWrapper) {
((BeansWrapper) cfg.getObjectWrapper()).clearClassIntrospectionCache();
}
servletContext.removeAttribute(FreemarkerManager.CONFIG_SERVLET_CONTEXT_KEY);
LOG.debug("FreeMarker configuration cleaned up");
}
}
}
@@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
/**
* Internal framework interface for components that hold static state
* (caches, daemon threads, etc.) requiring cleanup during application
* undeploy to prevent classloader leaks.
*
* <p>Implementations are registered as named beans in {@code struts-beans.xml}
* (or plugin descriptors) with type {@code InternalDestroyable}. During
* {@link Dispatcher#cleanup()}, all registered implementations are discovered
* via {@code Container.getInstanceNames(InternalDestroyable.class)} and
* invoked automatically.</p>
*
* <p>The order in which implementations are invoked is not guaranteed.
* Implementations must not depend on other {@code InternalDestroyable}
* beans having been (or not yet been) destroyed. Ordering can be
* influenced via the {@code order} attribute in bean registration.</p>
*
* <p>This is not part of the public user API. For user/plugin lifecycle
* callbacks, use {@link DispatcherListener} instead.</p>
*
* @since 6.9.0
* @see Dispatcher#cleanup()
*/
public interface InternalDestroyable {
/**
* Releases static state held by this component. Called once during
* {@link Dispatcher#cleanup()}.
*/
void destroy();
}
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import com.opensymphony.xwork2.ognl.OgnlUtil;
import java.beans.Introspector;
/**
* Clears OGNL runtime caches and JDK introspection caches that hold
* {@code Class<?>} references, preventing classloader leaks on hot redeploy.
*
* @since 6.9.0
*/
public class OgnlCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
OgnlUtil.clearRuntimeCache();
Introspector.flushCaches();
}
}
@@ -77,6 +77,7 @@ public class PrepareOperations {
} finally {
ActionContext.clear();
Dispatcher.clearInstance();
ContainerHolder.clear();
devModeOverride.remove();
}
});
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import org.apache.struts2.interceptor.ScopeInterceptor;
/**
* Clears {@link ScopeInterceptor}'s static locks map to prevent classloader
* leaks on hot redeploy. Separated from the interceptor itself because the
* locks map is static and must be cleared regardless of whether the interceptor
* is configured in any package.
*
* @since 6.9.0
*/
public class ScopeInterceptorCacheDestroyable implements InternalDestroyable {
@Override
public void destroy() {
ScopeInterceptor.clearLocks();
}
}
@@ -20,8 +20,7 @@ package org.apache.struts2.dispatcher.multipart;
import com.opensymphony.xwork2.LocaleProviderFactory;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.security.DefaultExcludedPatternsChecker;
import com.opensymphony.xwork2.security.ExcludedPatternsChecker;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
@@ -40,15 +39,13 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
private static final Logger LOG = LogManager.getLogger(AbstractMultiPartRequest.class);
private static final String EXCLUDED_FILE_PATTERN = ".*[<>&\"'|;\\\\/?*:]+.*|.*\\.\\..*";
/**
* Defines the internal buffer size used during streaming operations.
*/
public static final int BUFFER_SIZE = 10240;
/**
* Internal list of raised errors to be passed to the Struts2 framework.
* Internal list of raised errors to be passed to the the Struts2 framework.
*/
protected List<LocalizedMessage> errors = new ArrayList<>();
@@ -84,13 +81,6 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
*/
protected Locale defaultLocale = Locale.ENGLISH;
private final ExcludedPatternsChecker patternsChecker;
public AbstractMultiPartRequest() {
patternsChecker = new DefaultExcludedPatternsChecker();
((DefaultExcludedPatternsChecker) patternsChecker).setAdditionalExcludePatterns(EXCLUDED_FILE_PATTERN);
}
/**
* @param bufferSize Sets the buffer size to be used.
*/
@@ -134,7 +124,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
/**
* @param request Inspect the servlet request and set the locale if one wasn't provided by
* the Struts2 framework.
* the Struts2 framework.
*/
protected void setLocale(HttpServletRequest request) {
if (defaultLocale == null) {
@@ -145,7 +135,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
/**
* Build error message.
*
* @param e the Throwable/Exception
* @param e the Throwable/Exception
* @param args arguments
* @return error message
*/
@@ -158,7 +148,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
*/
*/
public List<LocalizedMessage> getErrors() {
return errors;
}
@@ -168,24 +158,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
* @return the canonical name based on the supplied filename
*/
protected String getCanonicalName(final String originalFileName) {
String fileName = originalFileName;
int forwardSlash = fileName.lastIndexOf('/');
int backwardSlash = fileName.lastIndexOf('\\');
if (forwardSlash != -1 && forwardSlash > backwardSlash) {
fileName = fileName.substring(forwardSlash + 1);
} else {
fileName = fileName.substring(backwardSlash + 1);
}
return fileName;
}
/**
* @param fileName file name to check
* @return true if the file name is excluded
*/
protected boolean isExcluded(String fileName) {
return patternsChecker.isExcluded(fileName).isExcluded();
return FilenameUtils.getName(originalFileName);
}
}
@@ -42,7 +42,6 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
@@ -59,6 +58,9 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
// maps parameter name -> List of param values
protected Map<String, List<String>> params = new HashMap<>();
// List to track all FileItem instances for comprehensive cleanup
protected List<FileItem> allFileItems = new ArrayList<>();
/**
* Creates a new request wrapper to handle multi-part data using methods adapted from Jason Pell's
* multipart classes (see class description).
@@ -101,8 +103,11 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {
if (ServletFileUpload.isMultipartContent(request)) {
for (FileItem item : parseRequest(request, saveDir)) {
// Track all FileItem instances for comprehensive cleanup - addAll in case exception in for loop
allFileItems.addAll(parseRequest(request, saveDir));
for (FileItem item : allFileItems) {
LOG.debug("Found file item: [{}]", normalizeSpace(item.getFieldName()));
if (item.isFormField()) {
processNormalFormField(item, request.getCharacterEncoding());
} else {
@@ -115,16 +120,6 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
protected void processFileField(FileItem item) {
LOG.debug("Item is a file upload");
if (isExcluded(item.getName())) {
LOG.warn("File name [{}] is not accepted", normalizeSpace(item.getName()));
return;
}
if (isExcluded(item.getFieldName())) {
LOG.warn("Field name [{}] is not accepted", normalizeSpace(item.getFieldName()));
return;
}
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (item.getName() == null || item.getName().trim().isEmpty()) {
LOG.debug("No file has been uploaded for the field: {}", normalizeSpace(item.getFieldName()));
@@ -146,11 +141,6 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
try {
LOG.debug("Item is a normal form field");
if (isExcluded(item.getFieldName())) {
LOG.warn("Form field name [{}] is not accepted", normalizeSpace(item.getFieldName()));
return;
}
List<String> values;
if (params.get(item.getFieldName()) != null) {
values = params.get(item.getFieldName());
@@ -255,7 +245,11 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
// Ensure file exists even if it is empty.
if (diskFileItem.getSize() == 0 && storeLocation != null && !storeLocation.exists()) {
try {
storeLocation.createNewFile();
if (storeLocation.createNewFile()) {
LOG.debug("File {} has been created", storeLocation.getAbsolutePath());
} else {
LOG.warn("File {} already exists", storeLocation.getAbsolutePath());
}
} catch (IOException e) {
LOG.error("Cannot write uploaded empty file to disk: {}", storeLocation.getAbsolutePath(), e);
}
@@ -372,15 +366,35 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
*/
public void cleanUp() {
Set<String> names = files.keySet();
for (String name : names) {
List<FileItem> items = files.get(name);
for (FileItem item : items) {
LOG.debug("Removing file [{}]", normalizeSpace(name));
if (!item.isInMemory()) {
item.delete();
try {
LOG.debug("Performing comprehensive cleanup for {} file items.", allFileItems.size());
for (FileItem item : allFileItems) {
try {
if (item instanceof DiskFileItem) {
DiskFileItem diskItem = (DiskFileItem) item;
File storeLocation = diskItem.getStoreLocation();
if (storeLocation != null) {
if(storeLocation.isFile()) {
LOG.debug("Deleting file: {}", storeLocation.getName());
if (!storeLocation.delete()) {
LOG.warn("There was a problem attempting to delete file: {}", storeLocation.getName());
}
} else {
LOG.debug("File: {} already deleted", storeLocation.getName());
}
}
}
// Also call the item's delete method as backup
if (!item.isInMemory()) {
item.delete();
}
} catch (Exception e) {
LOG.warn("Error during cleanup of file item: [{}]", normalizeSpace(item.getFieldName()), e);
}
}
} finally {
// Clear only the tracking collection, preserve parsed data
allFileItems.clear();
}
}
@@ -76,10 +76,22 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
LOG.debug("Performing File Upload temporary storage cleanup.");
for (List<FileInfo> fileInfoList : fileInfos.values()) {
for (FileInfo fileInfo : fileInfoList) {
File file = fileInfo.getFile();
LOG.debug("Deleting file '{}'.", file.getName());
if (!file.delete()) {
LOG.warn("There was a problem attempting to delete file [{}].", file.getName());
try {
// catch any exceptions during cleanup to ensure all files are deleted.
File file = fileInfo.getFile();
if(file != null) {
if(file.isFile()) {
LOG.debug("Deleting file: {}", file.getName());
if (!file.delete()) {
LOG.warn("There was a problem attempting to delete file: {}",
file.getName());
}
} else {
LOG.debug("File: {} already deleted", file.getName());
}
}
} catch (Exception e) {
LOG.warn("Error during cleanup of file item: [{}]", normalizeSpace(fileInfo.getOriginalName()), e);
}
}
}
@@ -314,10 +326,6 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
*/
protected void processFileItemStreamAsFormField(FileItemStream itemStream) {
String fieldName = itemStream.getFieldName();
if (isExcluded(fieldName)) {
LOG.warn("Form field [{}] rejected!", normalizeSpace(fieldName));
return;
}
try {
List<String> values;
String fieldValue = Streams.asString(itemStream.openStream());
@@ -346,11 +354,6 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
return;
}
if (isExcluded(itemStream.getName())) {
LOG.warn("File field [{}] rejected", normalizeSpace(itemStream.getName()));
return;
}
File file = null;
try {
// Create the temporary upload file.
@@ -396,9 +399,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
File file = File.createTempFile(prefix + "_", suffix, new File(location));
if (LOG.isDebugEnabled()) {
LOG.debug("Creating temporary file [{}] (originally [{}]).", file.getName(), normalizeSpace(fileName));
}
LOG.debug("Creating temporary file [{}] (originally [{}]).", file.getName(), normalizeSpace(fileName));
return file;
}
@@ -116,7 +116,7 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
}
// If it's null the upload failed
if (file == null) {
if (file == null || file.getContent() == null) {
String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_UPLOADING_KEY, new String[]{inputName});
if (validation != null) {
validation.addFieldError(inputName, errMsg);
@@ -65,7 +65,7 @@ public class I18nInterceptor extends AbstractInterceptor {
private Set<Locale> supportedLocale = Collections.emptySet();
protected enum Storage { COOKIE, SESSION, REQUEST, ACCEPT_LANGUAGE }
protected enum Storage {COOKIE, SESSION, REQUEST, ACCEPT_LANGUAGE}
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
@@ -103,10 +103,14 @@ public class I18nInterceptor extends AbstractInterceptor {
*/
public void setSupportedLocale(String supportedLocale) {
this.supportedLocale = TextParseUtil
.commaDelimitedStringToSet(supportedLocale)
.stream()
.map(Locale::new)
.collect(Collectors.toSet());
.commaDelimitedStringToSet(supportedLocale)
.stream()
.map(Locale::new)
.collect(Collectors.toSet());
}
protected boolean isLocaleSupported(Locale locale) {
return supportedLocale.isEmpty() || supportedLocale.contains(locale);
}
@Inject
@@ -222,8 +226,11 @@ public class I18nInterceptor extends AbstractInterceptor {
*/
protected interface LocaleHandler {
Locale find();
Locale read(ActionInvocation invocation);
Locale store(ActionInvocation invocation, Locale locale);
boolean shouldStore();
}
@@ -241,7 +248,10 @@ public class I18nInterceptor extends AbstractInterceptor {
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
return null;
@@ -278,6 +288,11 @@ public class I18nInterceptor extends AbstractInterceptor {
@Override
@SuppressWarnings("rawtypes")
public Locale find() {
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
return requestOnlyLocale;
}
if (!supportedLocale.isEmpty()) {
Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales();
while (locales.hasMoreElements()) {
@@ -287,7 +302,7 @@ public class I18nInterceptor extends AbstractInterceptor {
}
}
}
return super.find();
return null;
}
}
@@ -300,18 +315,18 @@ public class I18nInterceptor extends AbstractInterceptor {
@Override
public Locale find() {
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
LOG.debug("Found locale under request only param, it won't be stored in session!");
shouldStore = false;
return requestOnlyLocale;
}
LOG.debug("Searching locale in request under parameter {}", parameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
shouldStore = false;
return requestOnlyLocale;
}
return null;
@@ -344,7 +359,12 @@ public class I18nInterceptor extends AbstractInterceptor {
Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName);
if (sessionLocale instanceof Locale) {
locale = (Locale) sessionLocale;
LOG.debug("Applied session locale: {}", locale);
if (!isLocaleSupported(locale)) {
LOG.debug("Stored session locale {} is not supported, discarding", locale);
locale = null;
} else {
LOG.debug("Applied session locale: {}", locale);
}
}
}
}
@@ -368,17 +388,18 @@ public class I18nInterceptor extends AbstractInterceptor {
@Override
public Locale find() {
Locale requestOnlySessionLocale = super.find();
if (requestOnlySessionLocale != null) {
shouldStore = false;
return requestOnlySessionLocale;
}
LOG.debug("Searching locale in request under parameter {}", requestCookieParameterName);
Parameter requestedLocale = findLocaleParameter(actionInvocation, requestCookieParameterName);
if (requestedLocale.isDefined()) {
return getLocaleFromParam(requestedLocale.getValue());
Locale locale = getLocaleFromParam(requestedLocale.getValue());
if (locale != null && isLocaleSupported(locale)) {
return locale;
}
}
Locale requestOnlyLocale = super.find();
if (requestOnlyLocale != null) {
shouldStore = false;
return requestOnlyLocale;
}
return null;
@@ -404,6 +425,10 @@ public class I18nInterceptor extends AbstractInterceptor {
for (Cookie cookie : cookies) {
if (attributeName.equals(cookie.getName())) {
locale = getLocaleFromParam(cookie.getValue());
if (locale != null && !isLocaleSupported(locale)) {
LOG.debug("Stored cookie locale {} is not supported, discarding", locale);
locale = null;
}
}
}
}
@@ -232,7 +232,21 @@ public class ScopeInterceptor extends AbstractInterceptor implements PreResultLi
return o;
}
private static Map<Object, Object> locks = new IdentityHashMap<>();
private static final Map<Object, Object> locks = new IdentityHashMap<>();
/**
* Clears the locks map to prevent classloader leaks on hot redeploy.
*/
public static void clearLocks() {
synchronized (locks) {
locks.clear();
}
}
@Override
public void destroy() {
clearLocks();
}
static void lock(Object o, ActionInvocation invocation) throws Exception {
synchronized (o) {
@@ -135,7 +135,7 @@ public class TokenInterceptor extends MethodFilterInterceptor {
@Override
protected String doIntercept(ActionInvocation invocation) throws Exception {
LOG.debug("Intercepting invocation to check for valid transaction token.");
return handleToken(invocation);
return handleToken((org.apache.struts2.ActionInvocation) invocation);
}
protected String handleToken(ActionInvocation invocation) throws Exception {
@@ -144,22 +144,19 @@ public class TokenInterceptor extends MethodFilterInterceptor {
HttpSession session = ServletActionContext.getRequest().getSession(true);
synchronized (session.getId().intern()) {
if (!TokenHelper.validToken()) {
return handleInvalidToken(invocation);
return handleInvalidToken((org.apache.struts2.ActionInvocation) invocation);
}
}
return handleValidToken(invocation);
return handleValidToken((org.apache.struts2.ActionInvocation) invocation);
}
protected String handleToken(org.apache.struts2.ActionInvocation invocation) throws Exception {
return handleToken(ActionInvocation.adapt(invocation));
}
/**
* Determines what to do if an invalid token is provided. If the action implements {@link ValidationAware}
*
* @param invocation the action invocation where the invalid token failed
* @return the return code to indicate should be processed
* @throws Exception when any unexpected error occurs.
*/
protected String handleInvalidToken(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
String errorMessage = getErrorMessage(invocation);
String errorMessage = getErrorMessage((org.apache.struts2.ActionInvocation) invocation);
if (action instanceof ValidationAware) {
((ValidationAware) action).addActionError(errorMessage);
@@ -170,6 +167,17 @@ public class TokenInterceptor extends MethodFilterInterceptor {
return INVALID_TOKEN_CODE;
}
/**
* Determines what to do if an invalid token is provided. If the action implements {@link ValidationAware}
*
* @param invocation the action invocation where the invalid token failed
* @return the return code to indicate should be processed
* @throws Exception when any unexpected error occurs.
*/
protected String handleInvalidToken(org.apache.struts2.ActionInvocation invocation) throws Exception {
return handleInvalidToken(ActionInvocation.adapt(invocation));
}
protected String getErrorMessage(ActionInvocation invocation) {
Object action = invocation.getAction();
if (action instanceof TextProvider) {
@@ -178,6 +186,14 @@ public class TokenInterceptor extends MethodFilterInterceptor {
return textProvider.getText(INVALID_TOKEN_MESSAGE_KEY, DEFAULT_ERROR_MESSAGE);
}
protected String getErrorMessage(org.apache.struts2.ActionInvocation invocation) {
return getErrorMessage(ActionInvocation.adapt(invocation));
}
protected String handleValidToken(ActionInvocation invocation) throws Exception {
return invocation.invoke();
}
/**
* Called when a valid token is found. This method invokes the action by can be changed to do something more
* interesting.
@@ -186,8 +202,8 @@ public class TokenInterceptor extends MethodFilterInterceptor {
* @return invocation result
* @throws Exception when any unexpected error occurs.
*/
protected String handleValidToken(ActionInvocation invocation) throws Exception {
return invocation.invoke();
protected String handleValidToken(org.apache.struts2.ActionInvocation invocation) throws Exception {
return handleValidToken(ActionInvocation.adapt(invocation));
}
}
@@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.csp;
import org.apache.struts2.util.ValueStack;
/**
* Reads the nonce value using the ValueStack, {@link StrutsCspNonceReader} is the default implementation
* @since 6.8.0
*/
public interface CspNonceReader {
NonceValue readNonceValue(ValueStack stack);
class NonceValue {
private final String nonceValue;
private final CspNonceSource source;
private NonceValue(String nonceValue, CspNonceSource source) {
this.nonceValue = nonceValue;
this.source = source;
}
public static NonceValue ofSession(String nonceValue) {
return new NonceValue(nonceValue, CspNonceSource.SESSION);
}
public static NonceValue ofRequest(String nonceValue) {
return new NonceValue(nonceValue, CspNonceSource.REQUEST);
}
public static NonceValue ofNullSession() {
return new NonceValue(null, CspNonceSource.SESSION);
}
public static NonceValue ofNullRequest() {
return new NonceValue(null, CspNonceSource.REQUEST);
}
public boolean isNonceValueSet() {
return nonceValue != null;
}
public String getNonceValue() {
return nonceValue;
}
public CspNonceSource getSource() {
return source;
}
@Override
public String toString() {
String displayNonce;
if (nonceValue != null && nonceValue.length() >= 4) {
displayNonce = String.format("nonceValue='%s**********'", nonceValue.substring(0, 4));
} else if (nonceValue != null) {
displayNonce = String.format("nonceValue='%s**********'", nonceValue);
} else {
displayNonce = "nonceValue='<null>'";
}
return "NonceValue{" +
displayNonce +
", source=" + source +
'}';
}
}
}
@@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.csp;
/**
* Source of the nonce value
*/
public enum CspNonceSource {
REQUEST,
SESSION
}
@@ -45,11 +45,10 @@ public interface CspSettings {
String CSP_REPORT_TYPE = "application/csp-report";
/**
* @deprecated use {@link #addCspHeaders(HttpServletRequest, HttpServletResponse)} instead
* Adds CSP related headers to response based on request state (e.g., if session has been created)
*
* @since Struts 6.0.3
*/
@Deprecated
void addCspHeaders(HttpServletResponse response);
void addCspHeaders(HttpServletRequest request, HttpServletResponse response);
/**
@@ -18,15 +18,17 @@
*/
package org.apache.struts2.interceptor.csp;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.action.CspSettingsAware;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Objects;
import static java.lang.String.format;
@@ -43,68 +45,102 @@ import static java.lang.String.format;
*/
public class DefaultCspSettings implements CspSettings {
private final static Logger LOG = LogManager.getLogger(DefaultCspSettings.class);
private static final Logger LOG = LogManager.getLogger(DefaultCspSettings.class);
private static final String NONCE_KEY = "nonce";
private final SecureRandom sRand = new SecureRandom();
private CspNonceSource nonceSource = CspNonceSource.SESSION;
protected String reportUri;
protected String reportTo;
// default to reporting mode
protected String cspHeader = CSP_REPORT_HEADER;
@Override
public void addCspHeaders(HttpServletResponse response) {
throw new UnsupportedOperationException("Unsupported implementation, use #addCspHeaders(HttpServletRequest request, HttpServletResponse response)");
@Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false)
public void setNonceSource(String nonceSource) {
if (StringUtils.isBlank(nonceSource)) {
this.nonceSource = CspNonceSource.SESSION;
} else {
this.nonceSource = CspNonceSource.valueOf(nonceSource.toUpperCase());
}
}
@Override
public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) {
if (this.nonceSource == CspNonceSource.SESSION) {
addCspHeadersWithSession(request, response);
} else if (this.nonceSource == CspNonceSource.REQUEST) {
addCspHeadersWithRequest(request, response);
} else {
LOG.warn("Unknown nonce source: {}, ignoring CSP settings", nonceSource);
}
}
private void addCspHeadersWithSession(HttpServletRequest request, HttpServletResponse response) {
if (isSessionActive(request)) {
LOG.trace("Session is active, applying CSP settings");
associateNonceWithSession(request);
response.setHeader(cspHeader, createPolicyFormat(request));
String nonceValue = generateNonceValue();
request.getSession().setAttribute(NONCE_KEY, nonceValue);
response.setHeader(cspHeader, createPolicyFormat(nonceValue));
} else {
LOG.trace("Session is not active, ignoring CSP settings");
LOG.debug("Session is not active, ignoring CSP settings");
}
}
private void addCspHeadersWithRequest(HttpServletRequest request, HttpServletResponse response) {
String nonceValue = generateNonceValue();
request.setAttribute(NONCE_KEY, nonceValue);
response.setHeader(cspHeader, createPolicyFormat(nonceValue));
}
private boolean isSessionActive(HttpServletRequest request) {
return request.getSession(false) != null;
}
private void associateNonceWithSession(HttpServletRequest request) {
String nonceValue = Base64.getUrlEncoder().encodeToString(getRandomBytes());
request.getSession().setAttribute("nonce", nonceValue);
private String generateNonceValue() {
return Base64.getUrlEncoder().encodeToString(getRandomBytes());
}
protected String createPolicyFormat(HttpServletRequest request) {
StringBuilder policyFormatBuilder = new StringBuilder()
.append(OBJECT_SRC)
.append(format(" '%s'; ", NONE))
.append(SCRIPT_SRC)
.append(" 'nonce-%s' ") // nonce placeholder
.append(format("'%s' ", STRICT_DYNAMIC))
.append(format("%s %s; ", HTTP, HTTPS))
.append(BASE_URI)
.append(format(" '%s'; ", NONE));
protected String createPolicyFormat(String nonceValue) {
StringBuilder builder = new StringBuilder()
.append(OBJECT_SRC)
.append(format(" '%s'; ", NONE))
.append(SCRIPT_SRC)
.append(format(" 'nonce-%s' ", nonceValue))
.append(format("'%s' ", STRICT_DYNAMIC))
.append(format("%s %s; ", HTTP, HTTPS))
.append(BASE_URI)
.append(format(" '%s'; ", NONE));
if (reportUri != null) {
policyFormatBuilder
.append(REPORT_URI)
.append(format(" %s; ", reportUri));
if(reportTo != null) {
policyFormatBuilder
builder
.append(REPORT_URI)
.append(format(" %s; ", reportUri));
if (reportTo != null) {
builder
.append(REPORT_TO)
.append(format(" %s; ", reportTo));
}
}
return format(policyFormatBuilder.toString(), getNonceString(request));
return builder.toString();
}
/**
* @deprecated since 6.8.0, for removal
*/
@Deprecated
protected String createPolicyFormat(HttpServletRequest request) {
throw new UnsupportedOperationException("Unsupported implementation, use createPolicyFormat(String) instead!");
}
/**
* @deprecated since 6.8.0, for removal
*/
@Deprecated
protected String getNonceString(HttpServletRequest request) {
Object nonce = request.getSession().getAttribute("nonce");
return Objects.toString(nonce);
throw new UnsupportedOperationException("Unsupported implementation, don't use!");
}
private byte[] getRandomBytes() {
@@ -133,10 +169,10 @@ public class DefaultCspSettings implements CspSettings {
@Override
public String toString() {
return "DefaultCspSettings{" +
"reportUri='" + reportUri + '\'' +
", reportTo='" + reportTo + '\'' +
", cspHeader='" + cspHeader + '\'' +
'}';
"reportUri='" + reportUri + '\'' +
", reportTo='" + reportTo + '\'' +
", cspHeader='" + cspHeader + '\'' +
'}';
}
}
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.csp;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.util.ValueStack;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* Reads nonce value from session or request attribute.
* @since 6.8.0
*/
public class StrutsCspNonceReader implements CspNonceReader {
private static final Logger LOG = LogManager.getLogger(StrutsCspNonceReader.class);
private final CspNonceSource nonceSource;
@Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false)
public StrutsCspNonceReader(String source) {
if (StringUtils.isBlank(source)) {
this.nonceSource = CspNonceSource.SESSION;
} else {
this.nonceSource = CspNonceSource.valueOf(source.toUpperCase());
}
}
@Override
public NonceValue readNonceValue(ValueStack stack) {
HttpServletRequest request = stack.getActionContext().getServletRequest();
NonceValue nonceValue;
if (nonceSource == CspNonceSource.SESSION) {
LOG.debug("Reading nonce value from session");
nonceValue = readNonceFromSession(request);
} else if (nonceSource == CspNonceSource.REQUEST) {
LOG.debug("Reading nonce value from request attribute");
nonceValue = readNonceFromRequest(request);
} else {
LOG.warn("Unknown nonce source: {}, reading nonce value from session", nonceSource);
nonceValue = readNonceFromSession(request);
}
return nonceValue;
}
private NonceValue readNonceFromSession(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Object nonceValue = session != null ? session.getAttribute("nonce") : null;
if (nonceValue == null) {
LOG.debug("Session is not active, cannot obtain nonce value");
return NonceValue.ofNullSession();
}
return NonceValue.ofSession(nonceValue.toString());
}
private NonceValue readNonceFromRequest(HttpServletRequest request) {
Object nonceValue = request.getAttribute("nonce");
if (nonceValue == null) {
LOG.warn("Request attribute 'nonce' is not set, cannot obtain nonce value");
return NonceValue.ofNullRequest();
}
return NonceValue.ofRequest(nonceValue.toString());
}
}
@@ -90,7 +90,8 @@ public class HttpMethodInterceptor extends AbstractInterceptor {
invocation.getProxy().getMethod(), AllowedHttpMethod.class.getSimpleName(), request.getMethod());
return doIntercept(invocation, method);
}
} else if (AnnotationUtils.isAnnotatedBy(action.getClass(), HTTP_METHOD_ANNOTATIONS)) {
}
if (AnnotationUtils.isAnnotatedBy(action.getClass(), HTTP_METHOD_ANNOTATIONS)) {
LOG.debug("Action: {} annotated with: {}, checking if request: {} meets allowed methods!",
action, AllowedHttpMethod.class.getSimpleName(), request.getMethod());
return doIntercept(invocation, action.getClass());
@@ -21,6 +21,7 @@ package org.apache.struts2.result;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.mapper.ActionMapping;
@@ -101,7 +102,7 @@ public class PostbackResult extends StrutsResultSupport {
// Render
PrintWriter pw = new PrintWriter(response.getOutputStream());
pw.write("<!DOCTYPE html><html><body><form action=\"" + finalLocation + "\" method=\"POST\">");
pw.write("<!DOCTYPE html><html><body><form action=\"" + StringEscapeUtils.escapeHtml4(finalLocation) + "\" method=\"POST\">");
writeFormElements(request, pw);
writePrologueScript(pw);
pw.write("</html>");
@@ -21,6 +21,7 @@ package org.apache.struts2.result;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.security.NotExcludedAcceptedPatternsChecker;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -223,7 +224,7 @@ public class StreamResult extends StrutsResultSupport {
if (inputStream == null) {
String msg = ("Can not find a java.io.InputStream with the name [" + parsedInputName + "] in the invocation stack. " +
"Check the <param name=\"inputName\"> tag specified for this action is correct, not excluded and accepted.");
"Check the <param name=\"inputName\"> tag specified for this action is correct, not excluded and accepted.");
LOG.error(msg);
throw new IllegalArgumentException(msg);
}
@@ -231,11 +232,12 @@ public class StreamResult extends StrutsResultSupport {
HttpServletResponse oResponse = invocation.getInvocationContext().getServletResponse();
LOG.debug("Set the content type: {};charset{}", contentType, contentCharSet);
if (contentCharSet != null && !contentCharSet.equals("")) {
oResponse.setContentType(conditionalParse(contentType, invocation) + ";charset=" + conditionalParse(contentCharSet, invocation));
} else {
oResponse.setContentType(conditionalParse(contentType, invocation));
LOG.debug("Set the content type: {};charset={}", contentType, contentCharSet);
String parsedContentType = conditionalParse(contentType, invocation);
String parsedContentCharSet = conditionalParse(contentCharSet, invocation);
oResponse.setContentType(parsedContentType);
if (StringUtils.isNotEmpty(parsedContentCharSet)) {
oResponse.setCharacterEncoding(parsedContentCharSet);
}
LOG.debug("Set the content length: {}", contentLength);
@@ -267,7 +269,7 @@ public class StreamResult extends StrutsResultSupport {
oOutput = oResponse.getOutputStream();
LOG.debug("Streaming result [{}] type=[{}] length=[{}] content-disposition=[{}] charset=[{}]",
inputName, contentType, contentLength, contentDisposition, contentCharSet);
inputName, contentType, contentLength, contentDisposition, contentCharSet);
LOG.debug("Streaming to output buffer +++ START +++");
byte[] oBuff = new byte[bufferSize];
@@ -55,7 +55,7 @@ public class InvocationSessionStore {
return null;
}
final ActionInvocation savedInvocation = invocationContext.invocation;
final ActionInvocation savedInvocation = ActionInvocation.adapt(invocationContext.invocation);
if (savedInvocation != null) {
// WW-5026 - Preserve the previous PageContext (even if null) and restore it to the
// ActionContext after loading the savedInvocation context. The saved context's PageContext
@@ -72,6 +72,10 @@ public class InvocationSessionStore {
return savedInvocation;
}
public static void storeInvocation(String key, String token, ActionInvocation invocation) {
storeInvocation(key, token, (org.apache.struts2.ActionInvocation) invocation);
}
/**
* Stores the DefaultActionInvocation and ActionContext into the Session using the provided key for loading later using
* {@link #loadInvocation}
@@ -80,7 +84,7 @@ public class InvocationSessionStore {
* @param token token for check
* @param invocation the action invocation
*/
public static void storeInvocation(String key, String token, ActionInvocation invocation) {
public static void storeInvocation(String key, String token, org.apache.struts2.ActionInvocation invocation) {
InvocationContext invocationContext = new InvocationContext(invocation, token);
Map<String, Object> invocationMap = getInvocationMap();
invocationMap.put(key, invocationContext);
@@ -120,11 +124,11 @@ public class InvocationSessionStore {
private static final long serialVersionUID = -286697666275777888L;
//WW-4873 transient since 2.5.15
transient ActionInvocation invocation;
transient org.apache.struts2.ActionInvocation invocation;
String token;
public InvocationContext(ActionInvocation invocation, String token) {
public InvocationContext(org.apache.struts2.ActionInvocation invocation, String token) {
this.invocation = invocation;
this.token = token;
}
@@ -247,6 +247,13 @@ struts.ognl.beanInfoCacheType=wtlfu
### application-specific needs.
struts.ognl.beanInfoCacheMaxSize=10000
### Specifies the type of cache to use for proxy detection in ProxyUtil.
### Valid values: basic, lru, wtlfu. Default is 'wtlfu'.
struts.proxy.cacheType=wtlfu
### Specifies the maximum cache size for proxy detection caches.
struts.proxy.cacheMaxSize=10000
### Indicates if Dispatcher should handle unexpected exceptions by calling sendError()
### or simply rethrow it as a ServletException to allow future processing by other frameworks like Spring Security
struts.handle.exception=true
@@ -280,4 +287,7 @@ struts.url.queryStringParser=strutsQueryStringParser
struts.url.encoder=strutsUrlEncoder
struts.url.decoder=strutsUrlDecoder
### Defines source to read nonce value from, possible values are: request, session
struts.csp.nonceSource=session
### END SNIPPET: complete_file
+25
View File
@@ -114,6 +114,9 @@
class="org.apache.struts2.conversion.StrutsTypeConverterCreator"/>
<bean type="com.opensymphony.xwork2.conversion.TypeConverterHolder" name="struts"
class="org.apache.struts2.conversion.StrutsTypeConverterHolder"/>
<bean type="org.apache.struts2.conversion.UserConversionPropertiesProvider" name="struts"
class="org.apache.struts2.conversion.StrutsConversionPropertiesProcessor"/>
<bean class="org.apache.struts2.conversion.UserConversionPropertiesProcessor" scope="singleton"/>
<bean class="com.opensymphony.xwork2.conversion.impl.XWorkBasicConverter"/>
@@ -252,6 +255,9 @@
class="com.opensymphony.xwork2.ognl.DefaultOgnlExpressionCacheFactory" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.BeanInfoCacheFactory" name="struts"
class="com.opensymphony.xwork2.ognl.DefaultOgnlBeanInfoCacheFactory" scope="singleton"/>
<bean type="com.opensymphony.xwork2.ognl.ProxyCacheFactory" name="struts"
class="com.opensymphony.xwork2.ognl.StrutsProxyCacheFactory" scope="singleton"/>
<bean class="com.opensymphony.xwork2.util.StrutsProxyCacheFactoryBean" scope="singleton"/>
<bean type="org.apache.struts2.url.QueryStringBuilder" name="strutsQueryStringBuilder"
class="org.apache.struts2.url.StrutsQueryStringBuilder" scope="singleton"/>
@@ -265,4 +271,23 @@
<bean type="org.apache.struts2.interceptor.exec.ExecutorProvider" name="struts"
class="org.apache.struts2.interceptor.exec.StrutsExecutorProvider"/>
<bean type="org.apache.struts2.interceptor.csp.CspNonceReader" name="struts"
class="org.apache.struts2.interceptor.csp.StrutsCspNonceReader"/>
<!-- WW-5537: InternalDestroyable beans for automatic cleanup during undeploy -->
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="componentCache"
class="org.apache.struts2.dispatcher.ComponentCacheDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="compoundRootAccessor"
class="com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="defaultFileManager"
class="com.opensymphony.xwork2.util.fs.DefaultFileManager"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="scopeInterceptorCache"
class="org.apache.struts2.dispatcher.ScopeInterceptorCacheDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="ognlCache"
class="org.apache.struts2.dispatcher.OgnlCacheDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="finalizableReferenceQueue"
class="org.apache.struts2.dispatcher.FinalizableReferenceQueueDestroyable"/>
<bean type="org.apache.struts2.dispatcher.InternalDestroyable" name="freemarkerCache"
class="org.apache.struts2.dispatcher.FreemarkerCacheDestroyable"/>
</struts>
@@ -165,14 +165,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">Set the value used to retrieve the option value.</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -117,14 +117,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">String that will be appended to the label</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -117,14 +117,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">String that will be appended to the label</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -18,15 +18,14 @@
*/
package com.opensymphony.xwork2;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.junit.Test;
public class DefaultActionProxyTest extends StrutsInternalTestCase {
@Test
public void testThorwExceptionOnNotAllowedMethod() throws Exception {
public void testThrowExceptionOnNotAllowedMethod() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "strict", "Default", "notAllowed", true, true);
@@ -35,8 +34,52 @@ public class DefaultActionProxyTest extends StrutsInternalTestCase {
try {
dap.prepare();
fail("Must throw exception!");
} catch (Exception e) {
assertEquals(e.getMessage(), "Method notAllowed for action Default is not allowed!");
} catch (ConfigurationException e) {
assertEquals("Method notAllowed for action Default is not allowed!", e.getMessage());
}
}
public void testMethodSpecifiedWhenPassedExplicitly() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "NoMethod", "onPostOnly", true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method passed explicitly should be marked as specified", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
public void testMethodSpecifiedWhenResolvedFromConfig() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "ConfigMethod", null, true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method resolved from action config should be marked as specified", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
public void testMethodNotSpecifiedWhenDefaultingToExecute() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "NoMethod", null, true, true);
container.inject(dap);
dap.prepare();
assertFalse("Method defaulting to execute should not be marked as specified", dap.isMethodSpecified());
assertEquals("execute", dap.getMethod());
}
public void testMethodSpecifiedWithWildcardAction() {
final String filename = "com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml";
loadConfigurationProviders(new StrutsXmlConfigurationProvider(filename));
DefaultActionProxy dap = new DefaultActionProxy(new MockActionInvocation(), "", "Wild-onPostOnly", null, true, true);
container.inject(dap);
dap.prepare();
assertTrue("Method resolved from wildcard should be marked as specified", dap.isMethodSpecified());
assertEquals("onPostOnly", dap.getMethod());
}
}
@@ -338,6 +338,26 @@ public class ConfigurationTest extends XWorkTestCase {
}
public void testDefaultActionRefWithWildcard() {
RuntimeConfiguration runtimeConfiguration = configurationManager.getConfiguration().getRuntimeConfiguration();
ActionConfig config = runtimeConfiguration.getActionConfig("/wildcard-default", "unmatchedAction");
assertNotNull("Wildcard default action ref should resolve via wildcard matching", config);
assertEquals("com.opensymphony.xwork2.SimpleAction", config.getClassName());
}
public void testDefaultActionRefWithExactMatch() {
RuntimeConfiguration runtimeConfiguration = configurationManager.getConfiguration().getRuntimeConfiguration();
ActionConfig config = runtimeConfiguration.getActionConfig("/exact-default", "unmatchedAction");
assertNotNull("Exact default action ref should resolve via exact matching", config);
assertEquals("com.opensymphony.xwork2.SimpleAction", config.getClassName());
}
public void testDefaultActionRefWithWildcardNoMatch() {
RuntimeConfiguration runtimeConfiguration = configurationManager.getConfiguration().getRuntimeConfiguration();
ActionConfig config = runtimeConfiguration.getActionConfig("/wildcard-default-nomatch", "unmatchedAction");
assertNull("Default action ref with no matching action should return null", config);
}
@Override
protected void setUp() throws Exception {
super.setUp();
@@ -42,7 +42,7 @@ public class XmlConfigurationProviderAllowedMethodsTest extends ConfigurationTes
Map actionConfigs = pkg.getActionConfigs();
// assertions
assertEquals(5, actionConfigs.size());
assertEquals(8, actionConfigs.size());
ActionConfig action = (ActionConfig) actionConfigs.get("Default");
assertEquals(1, action.getAllowedMethods().size());
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.opensymphony.xwork2.inject.util;
import org.junit.Test;
import java.lang.ref.ReferenceQueue;
import static org.junit.Assert.assertNull;
public class FinalizableReferenceQueueTest {
@Test
public void stopAndClearIsIdempotent() {
// Should not throw even when called multiple times
FinalizableReferenceQueue.stopAndClear();
FinalizableReferenceQueue.stopAndClear();
ReferenceQueue<Object> instance = FinalizableReferenceQueue.getInstance();
assertNull("FinalizableReferenceQueue instance should be null after stopAndClear", instance);
}
}
@@ -1233,6 +1233,34 @@ public class OgnlValueStackTest extends XWorkTestCase {
assertNull("accessed private field (result not null) ?", accessedValue);
}
public void testFindValueWithConstructorAndProxyChecks() {
Map<String, String> properties = new HashMap<>();
properties.put(StrutsConstants.STRUTS_DISALLOW_PROXY_OBJECT_ACCESS, Boolean.TRUE.toString());
properties.put(StrutsConstants.STRUTS_DISALLOW_PROXY_MEMBER_ACCESS, Boolean.TRUE.toString());
loadButSet(properties);
refreshContainerFields();
String value = "test";
String ognlResult = (String) vs.findValue(
"new com.opensymphony.xwork2.ognl.OgnlValueStackTest$ValueHolder('" + value + "').value", String.class);
assertEquals(value, ognlResult);
}
@SuppressWarnings({"unused"})
public static class ValueHolder {
// See testFindValueWithConstructorAndProxyChecks
private final String value;
public ValueHolder(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
static class BadJavaBean {
private int count;
private int count2;
@@ -24,17 +24,18 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import java.io.StringReader;
import java.util.Objects;
/**
* Test cases for {@link DomHelper}.
*/
public class DomHelperTest extends TestCase {
private final String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)>]>\n<foo>\n<bar/>\n</foo>\n";
public void testParse() {
String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)>]>\n<foo>\n<bar/>\n</foo>\n";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("foo://bar");
@@ -47,6 +48,7 @@ public class DomHelperTest extends TestCase {
}
public void testGetLocationObject() {
String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)>]>\n<foo>\n<bar/>\n</foo>\n";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("foo://bar");
@@ -61,7 +63,7 @@ public class DomHelperTest extends TestCase {
}
public void testExternalEntities() {
String dtdFile = getClass().getResource("/author.dtd").getPath();
String dtdFile = Objects.requireNonNull(getClass().getResource("/author.dtd")).getPath();
String xml = "<!DOCTYPE foo [<!ELEMENT foo (bar)><!ELEMENT bar (#PCDATA)><!ENTITY writer SYSTEM \"file://" + dtdFile + "\">]><foo><bar>&writer;</bar></foo>";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("foo://bar");
@@ -74,4 +76,34 @@ public class DomHelperTest extends TestCase {
assertEquals(1, nl.getLength());
assertNull(nl.item(0).getNodeValue());
}
/**
* Tests that the parser is protected against Billion Laughs (XML Entity Expansion) attack.
* The FEATURE_SECURE_PROCESSING flag and the JDK's built-in entity expansion limit (64K
* since JDK 7u45) both cap entity expansion to prevent DoS.
* See: <a href="https://en.wikipedia.org/wiki/Billion_laughs_attack">Billion laughs attack</a>
*/
public void testBillionLaughsProtection() {
String xml = "<?xml version=\"1.0\"?>" +
"<!DOCTYPE root [" +
"<!ENTITY lol0 \"lol\">" +
"<!ENTITY lol1 \"&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;&lol0;\">" +
"<!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">" +
"<!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">" +
"<!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">" +
"<!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">" +
"]>" +
"<root>&lol5;</root>";
InputSource in = new InputSource(new StringReader(xml));
in.setSystemId("test://billion-laughs");
try {
DomHelper.parse(in);
fail("Parser should reject excessive entity expansion");
} catch (Exception e) {
assertNotNull(e.getCause());
assertTrue(e.getCause() instanceof SAXParseException);
}
}
}
@@ -21,6 +21,7 @@ package org.apache.struts2.components;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.util.ValueStack;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.components.template.Template;
import org.apache.struts2.components.template.TemplateEngine;
@@ -32,6 +33,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsCheckerTest.NO_EXCLUSION_ACCEPT_ALL_PATTERNS_CHECKER;
@@ -236,6 +238,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setAccesskey(accesskeyValue);
txtFld.evaluateParams();
@@ -250,6 +254,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.addParameter("value", value);
txtFld.evaluateParams();
@@ -338,6 +344,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setCssClass(cssClass);
txtFld.evaluateParams();
@@ -352,6 +360,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setStyle(cssStyle);
txtFld.evaluateParams();
@@ -372,6 +382,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
actionContext.withSession(new SessionMap(req));
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
@@ -392,11 +404,35 @@ public class UIBeanTest extends StrutsInternalTestCase {
session.invalidate();
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertNull(dblSelect.getAttributes().get("nonce"));
}
public void testNonceOfRequestAttribute() {
Map<String, String> params = new HashMap<String, String>(){{
put(StrutsConstants.STRUTS_CSP_NONCE_SOURCE, "request");
}};
initDispatcher(params);
String nonceVal = "r4nd0m";
ValueStack stack = ActionContext.getContext().getValueStack();
MockHttpServletRequest req = new MockHttpServletRequest();
req.setAttribute("nonce", nonceVal);
MockHttpServletResponse res = new MockHttpServletResponse();
ActionContext actionContext = stack.getActionContext();
actionContext.withServletRequest(req);
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
}
public void testSetNullUiStaticContentPath() {
// given
ValueStack stack = ActionContext.getContext().getValueStack();
@@ -0,0 +1,69 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.conversion;
import com.opensymphony.xwork2.XWorkTestCase;
import com.opensymphony.xwork2.conversion.ConversionPropertiesProcessor;
import com.opensymphony.xwork2.conversion.TypeConverter;
import com.opensymphony.xwork2.conversion.TypeConverterHolder;
import java.io.File;
/**
* Tests for {@link StrutsConversionPropertiesProcessor} two-phase processing.
*
* @see <a href="https://issues.apache.org/jira/browse/WW-4291">WW-4291</a>
*/
public class StrutsConversionPropertiesProcessorTest extends XWorkTestCase {
private TypeConverterHolder converterHolder;
private StrutsConversionPropertiesProcessor processor;
@Override
protected void setUp() throws Exception {
super.setUp();
converterHolder = container.getInstance(TypeConverterHolder.class);
processor = (StrutsConversionPropertiesProcessor) container.getInstance(ConversionPropertiesProcessor.class);
}
/**
* Tests that default converters from struts-default-conversion.properties
* are registered during the early initialization phase.
* java.io.File -> UploadedFileConverter is defined in struts-default-conversion.properties.
*/
public void testDefaultConvertersRegisteredDuringEarlyPhase() {
// The java.io.File converter should be registered from struts-default-conversion.properties
// struts-default-conversion.properties defines: java.io.File=org.apache.struts2.conversion.UploadedFileConverter
TypeConverter fileConverter = converterHolder.getDefaultMapping(File.class.getName());
assertNotNull("java.io.File converter should be registered from default properties", fileConverter);
}
/**
* Tests that the init() method only processes the default conversion properties file.
* User conversion properties should be processed separately via initUserConversions().
*/
public void testInitOnlyProcessesDefaultProperties() {
// This test verifies the behavior is correct - default converters are available
// after bootstrap. The actual split behavior is validated by checking that the
// framework doesn't throw ClassNotFoundException for bean names.
assertNotNull("Processor should be available", processor);
assertNotNull("Converter holder should have default mappings", converterHolder);
}
}
@@ -40,6 +40,11 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class AttributeMapTest {
@@ -360,4 +365,21 @@ public class AttributeMapTest {
assertEquals("value", value);
}
}
@Test
public void get_whenPageContextHasNoRequest() {
PageContext pageContext = mock(PageContext.class);
when(pageContext.getRequest()).thenReturn(null);
HttpServletRequest req = new MockHttpServletRequest();
req.setAttribute("attr", "reqValue");
AttributeMap attributeMap = new AttributeMap(new HashMap<String, Object>() {{
put(StrutsStatics.PAGE_CONTEXT, pageContext);
put(DispatcherConstants.REQUEST, new RequestMap(req));
}});
assertEquals("reqValue", attributeMap.get("attr"));
verify(pageContext, never()).findAttribute(anyString());
}
}
@@ -0,0 +1,227 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.ognl.accessor.CompoundRootAccessor;
import com.opensymphony.xwork2.util.fs.DefaultFileManager;
import org.apache.struts2.StrutsJUnit4InternalTestCase;
import org.apache.struts2.components.Component;
import org.apache.struts2.interceptor.ScopeInterceptor;
import org.junit.Test;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.Collections.emptyMap;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* WW-5537: Verifies that Dispatcher.cleanup() properly clears all static state
* that could prevent classloader garbage collection during hot redeployment.
*/
public class DispatcherCleanupLeakTest extends StrutsJUnit4InternalTestCase {
@Test
public void cleanupDiscoversAllInternalDestroyableBeans() {
initDispatcher(emptyMap());
Container container = dispatcher.getConfigurationManager().getConfiguration().getContainer();
Set<String> names = container.getInstanceNames(InternalDestroyable.class);
Set<String> expected = new HashSet<>(Arrays.asList(
"componentCache", "compoundRootAccessor", "defaultFileManager",
"scopeInterceptorCache", "ognlCache", "finalizableReferenceQueue",
"freemarkerCache"
));
assertTrue("All core InternalDestroyable beans should be registered, missing: "
+ missing(expected, names),
names.containsAll(expected));
}
@Test
public void cleanupContinuesWhenDestroyableThrows() {
initDispatcher(emptyMap());
// Populate a cache to verify cleanup still runs after a failure
Field mapField;
try {
mapField = Component.class.getDeclaredField("standardAttributesMap");
mapField.setAccessible(true);
@SuppressWarnings("unchecked")
ConcurrentMap<Class<?>, Collection<String>> map =
(ConcurrentMap<Class<?>, Collection<String>>) mapField.get(null);
map.put(String.class, new ArrayList<>());
assertFalse("Precondition: standardAttributesMap should not be empty", map.isEmpty());
} catch (Exception e) {
throw new RuntimeException(e);
}
// Register a destroyable that throws before other cleanup runs
final AtomicBoolean secondCalled = new AtomicBoolean(false);
InternalDestroyable failing = () -> { throw new RuntimeException("test failure"); };
InternalDestroyable tracking = () -> secondCalled.set(true);
// Call cleanup the loop should catch the exception and continue
Container container = dispatcher.getConfigurationManager().getConfiguration().getContainer();
Set<String> names = container.getInstanceNames(InternalDestroyable.class);
// Simulate the loop with our test destroyables injected
List<InternalDestroyable> destroyables = new ArrayList<>();
destroyables.add(failing);
for (String name : names) {
destroyables.add(container.getInstance(InternalDestroyable.class, name));
}
destroyables.add(tracking);
for (InternalDestroyable d : destroyables) {
try {
d.destroy();
} catch (Exception e) {
// mirrors Dispatcher.cleanup() error handling
}
}
assertTrue("Destroyable after the failing one should still be called", secondCalled.get());
}
@Test
@SuppressWarnings("unchecked")
public void cleanupClearsComponentStandardAttributesMap() throws Exception {
initDispatcher(emptyMap());
Field mapField = Component.class.getDeclaredField("standardAttributesMap");
mapField.setAccessible(true);
ConcurrentMap<Class<?>, Collection<String>> map =
(ConcurrentMap<Class<?>, Collection<String>>) mapField.get(null);
map.put(String.class, new ArrayList<>());
assertFalse("Precondition: standardAttributesMap should not be empty", map.isEmpty());
dispatcher.cleanup();
assertTrue("standardAttributesMap should be empty after cleanup", map.isEmpty());
}
@Test
@SuppressWarnings("unchecked")
public void cleanupClearsCompoundRootAccessorCache() throws Exception {
initDispatcher(emptyMap());
Field field = CompoundRootAccessor.class.getDeclaredField("invalidMethods");
field.setAccessible(true);
Map<Object, Boolean> invalidMethods = (Map<Object, Boolean>) field.get(null);
// Seed with a dummy entry to ensure cleanup actually clears it
invalidMethods.put("testKey", Boolean.TRUE);
assertFalse("Precondition: invalidMethods should not be empty", invalidMethods.isEmpty());
dispatcher.cleanup();
assertTrue("invalidMethods should be empty after cleanup", invalidMethods.isEmpty());
}
@Test
public void cleanupClearsDefaultFileManagerFilesMap() throws Exception {
initDispatcher(emptyMap());
Field filesField = DefaultFileManager.class.getDeclaredField("files");
filesField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, Object> files = (Map<String, Object>) filesField.get(null);
files.put("test-key", new Object());
assertFalse("Precondition: files should not be empty", files.isEmpty());
dispatcher.cleanup();
assertTrue("DefaultFileManager.files should be empty after cleanup", files.isEmpty());
}
@Test
public void cleanupClearsDefaultFileManagerLazyCache() throws Exception {
initDispatcher(emptyMap());
Field lazyCacheField = DefaultFileManager.class.getDeclaredField("lazyMonitoredFilesCache");
lazyCacheField.setAccessible(true);
@SuppressWarnings("unchecked")
List<URL> lazyCache = (List<URL>) lazyCacheField.get(null);
lazyCache.add(new URL("file:///test"));
assertFalse("Precondition: lazyMonitoredFilesCache should not be empty", lazyCache.isEmpty());
dispatcher.cleanup();
assertTrue("DefaultFileManager.lazyMonitoredFilesCache should be empty after cleanup",
lazyCache.isEmpty());
}
@Test
@SuppressWarnings("unchecked")
public void cleanupClearsScopeInterceptorLocks() throws Exception {
initDispatcher(emptyMap());
Field locksField = ScopeInterceptor.class.getDeclaredField("locks");
locksField.setAccessible(true);
Map<Object, Object> locks = (Map<Object, Object>) locksField.get(null);
locks.put(new Object(), new Object());
assertFalse("Precondition: locks should not be empty", locks.isEmpty());
dispatcher.cleanup();
assertTrue("ScopeInterceptor.locks should be empty after cleanup", locks.isEmpty());
}
@Test
public void cleanupClearsDispatcherListeners() throws Exception {
initDispatcher(emptyMap());
DispatcherListener listener = new DispatcherListener() {
@Override
public void dispatcherInitialized(Dispatcher du) {}
@Override
public void dispatcherDestroyed(Dispatcher du) {}
};
Dispatcher.addDispatcherListener(listener);
dispatcher.cleanup();
Field listenersField = Dispatcher.class.getDeclaredField("dispatcherListeners");
listenersField.setAccessible(true);
List<?> listeners = (List<?>) listenersField.get(null);
assertTrue("dispatcherListeners should be empty after cleanup", listeners.isEmpty());
}
private Set<String> missing(Set<String> expected, Set<String> actual) {
Set<String> diff = new HashSet<>(expected);
diff.removeAll(actual);
return diff;
}
}
@@ -0,0 +1,183 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher.multipart;
import org.apache.commons.fileupload.FileItem;
import org.apache.struts2.StrutsInternalTestCase;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* Test cases for {@link JakartaMultiPartRequest} that verify security-related functionality,
* specifically comprehensive cleanup of temporary files.
*/
public class JakartaMultiPartRequestTest extends StrutsInternalTestCase {
private File tempDir;
private JakartaMultiPartRequest multiPartRequest;
@Override
protected void setUp() throws Exception {
super.setUp();
// Create a temporary directory for test files
Path tempPath = Files.createTempDirectory("struts-multipart-test");
tempDir = tempPath.toFile();
multiPartRequest = new TestableJakartaMultiPartRequest();
multiPartRequest.setMaxSize("2048");
multiPartRequest.setMaxFiles("10");
multiPartRequest.setMaxFileSize("1024");
}
@Override
protected void tearDown() throws Exception {
if (tempDir != null && tempDir.exists()) {
// Clean up test directory
File[] files = tempDir.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
tempDir.delete();
}
super.tearDown();
}
/**
* Test that comprehensive cleanup removes all temporary files created during multipart processing.
* This addresses the security vulnerability where temporary files could be leaked.
*/
public void testComprehensiveCleanupRemovesAllTempFiles() throws Exception {
// Create a mock multipart request with both file upload and form field
MockHttpServletRequest request = new MockHttpServletRequest();
request.setContentType("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW");
request.setMethod("POST");
String multipartContent = createMultipartContent();
request.setContent(multipartContent.getBytes(StandardCharsets.UTF_8));
// Count files before processing
int filesBefore = countTempFiles();
// Process the multipart request
multiPartRequest.parse(request, tempDir.getAbsolutePath());
// Count files after processing (should be more due to temp files)
int filesAfterProcessing = countTempFiles();
// Verify that temp files were created during processing
assertTrue("Temporary files should be created during multipart processing",
filesAfterProcessing > filesBefore);
// Perform cleanup
multiPartRequest.cleanUp();
// Count files after cleanup
int filesAfterCleanup = countTempFiles();
// Verify comprehensive cleanup removed all temporary files
assertEquals("All temporary files should be cleaned up", filesBefore, filesAfterCleanup);
}
/**
* Test that all FileItem instances are tracked for cleanup, including both file uploads and form fields.
*/
public void testAllFileItemsAreTrackedForCleanup() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setContentType("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW");
request.setMethod("POST");
String multipartContent = createMultipartContentWithMultipleFields();
request.setContent(multipartContent.getBytes(StandardCharsets.UTF_8));
// Process the multipart request
multiPartRequest.parse(request, tempDir.getAbsolutePath());
// Access the tracked items through our testable implementation
TestableJakartaMultiPartRequest testable = (TestableJakartaMultiPartRequest) multiPartRequest;
List<FileItem> trackedItems = testable.getAllFileItems();
// Verify that all items (both files and form fields) are tracked
assertTrue("Should track multiple FileItem instances", trackedItems.size() >= 3);
// Verify tracking includes both form fields and file uploads
boolean hasFormField = false;
boolean hasFileUpload = false;
for (FileItem item : trackedItems) {
if (item.isFormField()) {
hasFormField = true;
} else {
hasFileUpload = true;
}
}
assertTrue("Should track form field items", hasFormField);
assertTrue("Should track file upload items", hasFileUpload);
}
private String createMultipartContent() {
return "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"textField\"\r\n\r\n" +
"test value\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"fileField\"; filename=\"test.txt\"\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"file content\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n";
}
private String createMultipartContentWithMultipleFields() {
return "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"textField1\"\r\n\r\n" +
"value1\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"textField2\"\r\n\r\n" +
"value2\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"fileField\"; filename=\"test.txt\"\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"file content\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n";
}
private int countTempFiles() {
if (tempDir == null || !tempDir.exists()) {
return 0;
}
File[] files = tempDir.listFiles();
return files != null ? files.length : 0;
}
/**
* Testable subclass that exposes internal state for verification
*/
private static class TestableJakartaMultiPartRequest extends JakartaMultiPartRequest {
public List<FileItem> getAllFileItems() {
return allFileItems;
}
}
}
@@ -24,9 +24,6 @@ import com.opensymphony.xwork2.DefaultLocaleProvider;
import com.opensymphony.xwork2.ValidationAwareSupport;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.mock.MockActionProxy;
import com.opensymphony.xwork2.security.DefaultAcceptedPatternsChecker;
import com.opensymphony.xwork2.security.DefaultExcludedPatternsChecker;
import com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsChecker;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.struts2.ServletActionContext;
@@ -101,6 +98,53 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
}
};
public static final UploadedFile NULL_CONTENT = new UploadedFile() {
@Override
public Long length() {
return 0L;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getAbsolutePath() {
return null;
}
@Override
public File getContent() {
return null;
}
@Override
public String getOriginalName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public String getInputName() {
return null;
}
};
private ActionFileUploadInterceptor interceptor;
private File tempDir;
@@ -208,6 +252,22 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithNoContent() {
interceptor.setAllowedTypes("text/plain");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, NULL_CONTENT, "filename.html", "text/plain", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
List<String> errors = validation.getFieldErrors().get("inputName");
assertEquals(1, errors.size());
String msg = errors.get(0);
assertTrue(msg.startsWith("Error uploading:"));
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithMaxSize() throws Exception {
interceptor.setMaximumSize(10L);
@@ -666,68 +726,6 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe"));
}
public void testUnacceptedFieldName() throws Exception {
MockHttpServletRequest req = new MockHttpServletRequest();
req.setCharacterEncoding(StandardCharsets.UTF_8.name());
req.setMethod("post");
req.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
String content = ("-----1234\r\n" +
"Content-Disposition: form-data; name=\"top.file\"; filename=\"deleteme.txt\"\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"Unit test of ActionFileUploadInterceptor" +
"\r\n" +
"-----1234--\r\n");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
assertFalse(action.hasActionErrors());
assertNull(action.getUploadFiles());
}
public void testUnacceptedFileName() throws Exception {
MockHttpServletRequest req = new MockHttpServletRequest();
req.setCharacterEncoding(StandardCharsets.UTF_8.name());
req.setMethod("post");
req.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
String content = ("-----1234\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"../deleteme.txt\"\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"Unit test of ActionFileUploadInterceptor" +
"\r\n" +
"-----1234--\r\n");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
assertFalse(action.hasActionErrors());
assertNull(action.getUploadFiles());
}
private String encodeTextFile(String filename, String contentType, String content) {
return "\r\n" +
"--" +
@@ -312,7 +312,7 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
*/
public static class CustomDefaultCspSettings extends DefaultCspSettings {
protected String createPolicyFormat(HttpServletRequest request) {
protected String createPolicyFormat(String nonceValue) {
return "foo";
}
}
@@ -23,7 +23,6 @@ import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.DefaultLocaleProvider;
import com.opensymphony.xwork2.ValidationAwareSupport;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsChecker;
import com.opensymphony.xwork2.util.ClassLoaderUtil;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.struts2.ServletActionContext;
@@ -101,6 +100,53 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
}
};
public static final UploadedFile NULL_CONTENT = new UploadedFile() {
@Override
public Long length() {
return 0L;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getAbsolutePath() {
return null;
}
@Override
public File getContent() {
return null;
}
@Override
public String getOriginalName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public String getInputName() {
return null;
}
};
private FileUploadInterceptor interceptor;
private File tempDir;
@@ -203,6 +249,22 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithNoContent() {
interceptor.setAllowedTypes("text/plain");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, NULL_CONTENT, "filename.html", "text/plain", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
List<String> errors = validation.getFieldErrors().get("inputName");
assertEquals(1, errors.size());
String msg = errors.get(0);
assertTrue(msg.startsWith("Error uploading:"));
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithMaxSize() throws Exception {
interceptor.setMaximumSize(10L);
@@ -728,68 +790,6 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe"));
}
public void testUnacceptedFieldName() throws Exception {
MockHttpServletRequest req = new MockHttpServletRequest();
req.setCharacterEncoding(StandardCharsets.UTF_8.name());
req.setMethod("post");
req.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
String content = ("-----1234\r\n" +
"Content-Disposition: form-data; name=\"top.file\"; filename=\"deleteme.txt\"\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"Unit test of ActionFileUploadInterceptor" +
"\r\n" +
"-----1234--\r\n");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
ActionFileUploadInterceptorTest.MyFileUploadAction action = container.inject(ActionFileUploadInterceptorTest.MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
assertFalse(action.hasActionErrors());
assertNull(action.getUploadFiles());
}
public void testUnacceptedFileName() throws Exception {
MockHttpServletRequest req = new MockHttpServletRequest();
req.setCharacterEncoding(StandardCharsets.UTF_8.name());
req.setMethod("post");
req.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
String content = ("-----1234\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"../deleteme.txt\"\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"Unit test of ActionFileUploadInterceptor" +
"\r\n" +
"-----1234--\r\n");
req.setContent(content.getBytes(StandardCharsets.US_ASCII));
ActionFileUploadInterceptorTest.MyFileUploadAction action = container.inject(ActionFileUploadInterceptorTest.MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxSize(req, 2000));
interceptor.intercept(mai);
assertFalse(action.hasActionErrors());
assertNull(action.getUploadFiles());
}
private String encodeTextFile(String filename, String contentType, String content) {
return "\r\n" +
"--" +
@@ -205,7 +205,7 @@ public class I18nInterceptorTest extends TestCase {
}
public void testRealLocalesInParams() throws Exception {
Locale[] locales = new Locale[] { Locale.CANADA_FRENCH };
Locale[] locales = new Locale[]{Locale.CANADA_FRENCH};
assertTrue(locales.getClass().isArray());
prepare(I18nInterceptor.DEFAULT_PARAMETER, locales);
interceptor.intercept(mai);
@@ -294,6 +294,66 @@ public class I18nInterceptorTest extends TestCase {
assertEquals(Locale.US, mai.getInvocationContext().getLocale());
}
public void testRequestLocaleWithSupportedLocale() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "de");
// when
interceptor.intercept(mai);
// then
Locale german = new Locale("de");
assertEquals(german, session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
assertEquals(german, mai.getInvocationContext().getLocale());
}
public void testUnsupportedRequestLocaleRejected() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
prepare(I18nInterceptor.DEFAULT_PARAMETER, "fr");
// when
interceptor.intercept(mai);
// then - fr is not supported, should fall back to default
assertNull("unsupported locale should not be stored", session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE));
}
public void testStaleSessionLocaleRejected() throws Exception {
// given - session has a stored locale that is no longer supported
session.put(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE, Locale.FRENCH);
interceptor.setSupportedLocale("en,de");
// when
interceptor.intercept(mai);
// then - stored fr locale should be discarded since it's not in supportedLocale
assertFalse("stale session locale should be discarded",
Locale.FRENCH.equals(mai.getInvocationContext().getLocale()));
}
public void testCookieRequestLocaleWithSupportedLocale() throws Exception {
// given
interceptor.setSupportedLocale("en,de");
interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name());
prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "de");
final Cookie cookie = new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "de");
HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class);
response.addCookie(CookieMatcher.eqCookie(cookie));
EasyMock.replay(response);
ac.put(StrutsStatics.HTTP_RESPONSE, response);
// when
interceptor.intercept(mai);
// then
EasyMock.verify(response);
Locale german = new Locale("de");
assertEquals(german, mai.getInvocationContext().getLocale());
}
private void prepare(String key, Serializable value) {
Map<String, Serializable> params = new HashMap<>();
params.put(key, value);
@@ -308,9 +368,9 @@ public class I18nInterceptorTest extends TestCase {
session = new HashMap<>();
ac = ActionContext.of()
.bind()
.withSession(session)
.withParameters(HttpParameters.create().build());
.bind()
.withSession(session)
.withParameters(HttpParameters.create().build());
request = new MockHttpServletRequest();
request.setSession(new MockHttpSession());
@@ -348,8 +408,8 @@ public class I18nInterceptorTest extends TestCase {
public boolean matches(Object argument) {
Cookie cookie = ((Cookie) argument);
return
(cookie.getName().equals(expected.getName()) &&
cookie.getValue().equals(expected.getValue()));
(cookie.getName().equals(expected.getName()) &&
cookie.getValue().equals(expected.getValue()));
}
public static Cookie eqCookie(Cookie ck) {
@@ -359,10 +419,10 @@ public class I18nInterceptorTest extends TestCase {
public void appendTo(StringBuffer buffer) {
buffer
.append("Received")
.append(expected.getName())
.append("/")
.append(expected.getValue());
.append("Received")
.append(expected.getName())
.append("/")
.append(expected.getValue());
}
}
@@ -19,13 +19,17 @@
package org.apache.struts2.interceptor.httpmethod;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionProxy;
import com.opensymphony.xwork2.mock.MockActionInvocation;
import com.opensymphony.xwork2.mock.MockActionProxy;
import org.apache.struts2.HttpMethodsTestAction;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.TestAction;
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
import org.springframework.mock.web.MockHttpServletRequest;
import java.util.Map;
public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
private HttpMethodInterceptor interceptor;
@@ -217,6 +221,110 @@ public class HttpMethodInterceptorTest extends StrutsInternalTestCase {
assertEquals(HttpMethod.POST, action.getHttpMethod());
}
public void testWildcardResolvedMethodWithPostAnnotationRejectsGet() throws Exception {
// given
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("onPostOnly");
actionProxy.setMethodSpecified(true);
invocation.setResultCode("onPostOnly");
prepareRequest("GET");
// when
String resultName = interceptor.intercept(invocation);
// then
assertEquals("bad-request", resultName);
}
public void testWildcardResolvedMethodWithPostAnnotationAllowsPost() throws Exception {
// given
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("onPostOnly");
actionProxy.setMethodSpecified(true);
invocation.setResultCode("onPostOnly");
prepareRequest("POST");
// when
String resultName = interceptor.intercept(invocation);
// then
assertEquals("onPostOnly", resultName);
assertEquals(HttpMethod.POST, action.getHttpMethod());
}
/**
* Regression for wildcard-resolved methods with no method-level HTTP annotation:
* a class-level {@code @AllowedHttpMethod(POST)} must still cause GET to be rejected.
* Previously the interceptor's {@code if/else-if} structure made the class-level
* branch unreachable when {@code isMethodSpecified()=true} and the resolved method
* carried no annotation of its own.
*/
public void testWildcardResolvedUnannotatedMethodRespectsClassLevelAnnotation() throws Exception {
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("execute");
actionProxy.setMethodSpecified(true);
prepareRequest("get");
String resultName = interceptor.intercept(invocation);
assertEquals("bad-request", resultName);
}
/**
* Counterpart to the above: POST against a wildcard-resolved unannotated method must succeed
* when the class allows POST via {@code @AllowedHttpMethod(POST)}.
*/
public void testWildcardResolvedUnannotatedMethodAllowsPostWithClassLevelAnnotation() throws Exception {
HttpMethodsTestAction action = new HttpMethodsTestAction();
prepareActionInvocation(action);
actionProxy.setMethod("execute");
actionProxy.setMethodSpecified(true);
invocation.setResultCode("success");
prepareRequest("post");
String resultName = interceptor.intercept(invocation);
assertEquals("success", resultName);
}
/**
* Exercises the full wildcard resolution path through a real {@link com.opensymphony.xwork2.DefaultActionProxy}.
* <p>
* Config (from xwork-test-allowed-methods.xml):
* {@code <action name="Wild-*" class="HttpMethodsTestAction" method="{1}">}.
* URL {@code Wild-execute} resolves to {@code ActionSupport.execute()} no method-level
* HTTP annotation. {@code HttpMethodsTestAction} carries class-level
* {@code @AllowedHttpMethod(POST)}, so GET must be rejected end-to-end.
*/
public void testWildcardResolvedExecuteRejectsGetThroughRealProxy() throws Exception {
loadConfigurationProviders(new StrutsXmlConfigurationProvider(
"com/opensymphony/xwork2/config/providers/xwork-test-allowed-methods.xml"));
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/Wild-execute");
Map<String, Object> extraContext = ActionContext.of()
.withServletRequest(request)
.getContextMap();
ActionProxy proxy = actionProxyFactory.createActionProxy("", "Wild-execute", null, extraContext);
assertEquals("execute", proxy.getMethod());
assertTrue("Wildcard-resolved method must report isMethodSpecified()=true", proxy.isMethodSpecified());
HttpMethodInterceptor realInterceptor = new HttpMethodInterceptor();
String result = realInterceptor.intercept(proxy.getInvocation());
assertEquals("bad-request", result);
}
private void prepareActionInvocation(Object action) {
interceptor = new HttpMethodInterceptor();
invocation = new MockActionInvocation();
@@ -146,5 +146,108 @@ public class PostbackResultTest extends StrutsInternalTestCase {
}
}
/**
* WW-5623: Verify that HTML special characters in finalLocation are properly
* escaped in the rendered form action attribute.
*/
public void testFormActionHtmlEscaping() throws Exception {
ActionContext context = ActionContext.getContext();
ValueStack stack = context.getValueStack();
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
context.put(ServletActionContext.HTTP_REQUEST, req);
context.put(ServletActionContext.HTTP_RESPONSE, res);
// Push an object with a malicious property onto the value stack
stack.push(new Object() {
public String getTargetUrl() {
return "/test\"onmouseover=\"alert(1)";
}
});
PostbackResult result = new PostbackResult();
result.setLocation("/redirect?url=${targetUrl}");
result.setPrependServletContext(false);
IMocksControl control = createControl();
ActionInvocation mockInvocation = control.createMock(ActionInvocation.class);
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
expect(mockInvocation.getStack()).andReturn(stack).anyTimes();
control.replay();
result.setActionMapper(container.getInstance(ActionMapper.class));
// Call doExecute directly with a malicious location containing all critical chars
result.doExecute("/test\"onmouseover=\"alert(1)\"&param=<script>", mockInvocation);
String output = res.getContentAsString();
// The action attribute must contain escaped HTML entities
assertTrue("Double quote should be escaped to &quot;",
output.contains("action=\"/test&quot;onmouseover=&quot;alert(1)&quot;&amp;param=&lt;script&gt;\""));
// Must not contain unescaped double-quote that breaks out of the attribute
assertFalse("Raw double-quote must not appear in action value",
output.contains("action=\"/test\""));
control.verify();
}
/**
* WW-5623: Verify that each individual HTML special character is properly escaped.
*/
public void testFormActionEscapesAllHtmlSpecialChars() throws Exception {
ActionContext context = ActionContext.getContext();
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
context.put(ServletActionContext.HTTP_REQUEST, req);
context.put(ServletActionContext.HTTP_RESPONSE, res);
IMocksControl control = createControl();
ActionInvocation mockInvocation = control.createMock(ActionInvocation.class);
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
control.replay();
PostbackResult result = new PostbackResult();
result.setActionMapper(container.getInstance(ActionMapper.class));
result.doExecute("/path?a=1&b=2\"<>", mockInvocation);
String output = res.getContentAsString();
assertTrue("Ampersand should be escaped", output.contains("&amp;"));
assertTrue("Double-quote should be escaped", output.contains("&quot;"));
assertTrue("Less-than should be escaped", output.contains("&lt;"));
assertTrue("Greater-than should be escaped", output.contains("&gt;"));
control.verify();
}
/**
* WW-5623: Verify that a clean location (no special chars) renders unchanged.
*/
public void testFormActionCleanLocationUnchanged() throws Exception {
ActionContext context = ActionContext.getContext();
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
context.put(ServletActionContext.HTTP_REQUEST, req);
context.put(ServletActionContext.HTTP_RESPONSE, res);
IMocksControl control = createControl();
ActionInvocation mockInvocation = control.createMock(ActionInvocation.class);
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
control.replay();
PostbackResult result = new PostbackResult();
result.setActionMapper(container.getInstance(ActionMapper.class));
result.doExecute("/clean/path/action.do", mockInvocation);
String output = res.getContentAsString();
assertTrue("Clean location should render as-is in action attribute",
output.contains("action=\"/clean/path/action.do\""));
control.verify();
}
}
@@ -120,6 +120,16 @@ public class StreamResultTest extends StrutsInternalTestCase {
assertEquals("inline", response.getHeader("Content-disposition"));
}
public void testStreamResultWithNullCharSetExpression() throws Exception {
result.setParse(true);
result.setInputName("streamForImage");
result.setContentCharSet("${nullCharSetMethod}");
result.doExecute("helloworld", mai);
assertEquals("text/plain", response.getContentType());
}
public void testAllowCacheDefault() throws Exception {
result.setInputName("streamForImage");
@@ -310,6 +320,10 @@ public class StreamResultTest extends StrutsInternalTestCase {
public String getContentCharSetMethod() {
return "UTF-8";
}
public String getNullCharSetMethod() {
return null;
}
}
}
@@ -29,7 +29,7 @@
</action>
<action name="Boring">
<allowed-methods> </allowed-methods>
<allowed-methods></allowed-methods>
</action>
<action name="Foo">
@@ -43,6 +43,18 @@
<action name="Baz" method="baz">
<allowed-methods>foo,bar</allowed-methods>
</action>
<action name="Wild-*" class="org.apache.struts2.HttpMethodsTestAction" method="{1}">
<allowed-methods>regex:.*</allowed-methods>
</action>
<action name="ConfigMethod" class="org.apache.struts2.HttpMethodsTestAction" method="onPostOnly">
<allowed-methods>regex:.*</allowed-methods>
</action>
<action name="NoMethod" class="org.apache.struts2.HttpMethodsTestAction">
<allowed-methods>regex:.*</allowed-methods>
</action>
</package>
<package name="strict" strict-method-invocation="true">
+21
View File
@@ -295,5 +295,26 @@
<!-- default-class-ref is expected to be inherited -->
</package>
<package name="wildcardDefault" extends="default" namespace="/wildcard-default">
<default-action-ref name="movie-input" />
<action name="movie-*" class="com.opensymphony.xwork2.SimpleAction">
<result name="success" type="mock">/movie/{1}.jsp</result>
</action>
</package>
<package name="exactDefault" extends="default" namespace="/exact-default">
<default-action-ref name="home" />
<action name="home" class="com.opensymphony.xwork2.SimpleAction">
<result name="success" type="mock">/home.jsp</result>
</action>
</package>
<package name="wildcardDefaultNoMatch" extends="default" namespace="/wildcard-default-nomatch">
<default-action-ref name="nonExistentAction" />
<action name="movie-*" class="com.opensymphony.xwork2.SimpleAction">
<result name="success" type="mock">/movie/{1}.jsp</result>
</action>
</package>
<include file="includeTest.xml"/>
</struts>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<artifactId>struts2-async-plugin</artifactId>
+1 -7
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.1</version>
<version>6.10.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -68,12 +68,6 @@
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>

Some files were not shown because too many files have changed in this diff Show More