Compare commits

...

192 Commits

Author SHA1 Message Date
Lukasz Lenart 76c0887972 [maven-release-plugin] prepare for next development iteration 2026-08-01 12:28:23 +02:00
Lukasz Lenart 52628432a9 [maven-release-plugin] prepare release STRUTS_6_11_0 2026-08-01 12:28:14 +02:00
Lukasz Lenart 7ce27107e2 WW-5668 Make the localized-text provider caches size-bounded and align request-locale resolution (6.x) (#1823)
* WW-5668 Add remove(key) to the OgnlCache abstraction

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

* WW-5668 Bound the localized-text provider caches with configurable size

Converts bundlesMap, messageFormats and missingBundles to the existing
OgnlCache abstraction, configurable via struts.i18n.cacheType and
struts.i18n.cacheMaxSize (wtlfu / 10000 by default). The caches are kept
transient and rebuilt in readObject so the providers stay serializable,
and bundlesMap-related synchronization moves to a dedicated monitor since
the field is now reassignable.

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

* WW-5668 Add opt-in request-locale resolution consistency to Dispatcher

Adds struts.locale.validateRequestLocale (default false) so request-derived
locales can be restricted to the JVM's available-locale set, matching what
I18nInterceptor already applies to its own locale sources.

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

* WW-5668 Keep the localized-text providers deserializable across a version upgrade

Pins serialVersionUID to the value implicitly computed for the pre-6.11.0 class
shape instead of 1L, so a session serialized by a 6.10.0 node still loads on a
6.11.0 one during a rolling upgrade rather than failing with InvalidClassException.

Such a stream carries no value for the new cache settings, and field initialisers
do not run during deserialization, so readObject restores their defaults before
rebuilding the caches - without that guard it failed with a NullPointerException.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 11:55:00 +02:00
Lukasz Lenart a1c8af5574 WW-5666 Apply input length limits consistently when reading request bodies (6.x) (#1822)
* WW-5666 fix(json): apply the input length limit while reading

* WW-5666 fix(core): bound the CSP report body read and make the limit configurable

CspReportAction read the submitted report body with a single readLine() and had
no limit of its own. Read it up to a limit instead, defaulting to 8192
characters and configurable through struts.csp.report.maxSize. A body above the
limit is discarded with a warning rather than processed.

The limit is injected when the action is built, before the interceptor stack
runs, because withServletRequest is invoked by the servletConfig interceptor
ahead of staticParams and params. Values that are not usable as a buffer size
are ignored with a warning.
2026-08-01 10:04:57 +02:00
dependabot[bot] 28657e5f29 build(deps): bump jackson.version from 2.22.0 to 2.22.1 (#1790)
Bumps `jackson.version` from 2.22.0 to 2.22.1.

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

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

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

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.22.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-07-17 20:21:13 +02:00
dependabot[bot] 1b1981e987 build(deps-dev): bump commons-logging:commons-logging (#1763)
Bumps [commons-logging:commons-logging](https://github.com/apache/commons-logging) from 1.3.6 to 1.4.0.
- [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.6...rel/commons-logging-1.4.0)

---
updated-dependencies:
- dependency-name: commons-logging:commons-logging
  dependency-version: 1.4.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>
2026-07-12 11:38:07 +02:00
dependabot[bot] 1f16744114 build(deps): bump jackson.version from 2.21.4 to 2.22.0 (#1746)
Bumps `jackson.version` from 2.21.4 to 2.22.0.

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

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

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

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.22.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-07-12 11:35:01 +02:00
dependabot[bot] efda88b2bb build(deps): bump log4j2.version from 2.26.0 to 2.26.1 (#1772)
Bumps `log4j2.version` from 2.26.0 to 2.26.1.

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

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

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

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

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

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-api
  dependency-version: 2.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-jcl
  dependency-version: 2.26.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-slf4j-impl
  dependency-version: 2.26.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.logging.log4j:log4j-web
  dependency-version: 2.26.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-07-09 09:24:44 +02:00
dependabot[bot] 28db4ac276 build(deps-dev): bump org.apache.maven.plugins:maven-failsafe-plugin (#1764)
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.5 to 3.5.6.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.5...surefire-3.5.6)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-failsafe-plugin
  dependency-version: 3.5.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-07-01 12:56:48 +02:00
dependabot[bot] d46d581b07 build(deps): bump org.apache.maven.plugins:maven-dependency-plugin (#1754)
Bumps [org.apache.maven.plugins:maven-dependency-plugin](https://github.com/apache/maven-dependency-plugin) from 3.10.0 to 3.11.0.
- [Release notes](https://github.com/apache/maven-dependency-plugin/releases)
- [Commits](https://github.com/apache/maven-dependency-plugin/compare/maven-dependency-plugin-3.10.0...maven-dependency-plugin-3.11.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-dependency-plugin
  dependency-version: 3.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-06-26 07:17:57 +02:00
dependabot[bot] bc369df815 build(deps-dev): bump org.jacoco:jacoco-maven-plugin (#1750)
Bumps [org.jacoco:jacoco-maven-plugin](https://github.com/jacoco/jacoco) from 0.8.14 to 0.8.15.
- [Release notes](https://github.com/jacoco/jacoco/releases)
- [Commits](https://github.com/jacoco/jacoco/compare/v0.8.14...v0.8.15)

---
updated-dependencies:
- dependency-name: org.jacoco:jacoco-maven-plugin
  dependency-version: 0.8.15
  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-06-26 07:17:14 +02:00
dependabot[bot] d1b16a7657 build(deps): bump maven-surefire-plugin.version from 3.5.5 to 3.5.6 (#1748)
Bumps `maven-surefire-plugin.version` from 3.5.5 to 3.5.6.

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

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

---
updated-dependencies:
- dependency-name: org.apache.maven.surefire:surefire-junit47
  dependency-version: 3.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.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>
2026-06-26 07:16:42 +02:00
Lukasz Lenart e2fa549b12 WW-5630 test(core): streamline ConfigParseUtilTest and convert to JUnit 4 (#1741)
Port of #1740 to 6.x: 12 overlapping cache tests collapsed to 5 focused
ones, JUnit 3 -> JUnit 4, dropped the ~80-class literal, reflection kept
only in the two size-bound tests. Also reorders the caffeine imports in
ConfigParseUtil to match alphabetical ordering. Production logic unchanged.

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

Code generated by Copilot

* WW-5630 - Add additional UT

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

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

Assistance in coding using co-pilot

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

With co-pilot assitance

(cherry picked from commit 210dc86b88)
2026-06-14 13:27:11 +02:00
Lukasz Lenart f8d42eb023 Prepare for a new development cycle of Struts 6.11.0 (#1739)
* [maven-release-plugin] prepare release STRUTS_6_10_0

* [maven-release-plugin] prepare for next development iteration
2026-06-14 07:39:08 +00:00
dependabot[bot] f9f69482c7 build(deps): bump asm.version from 9.10 to 9.10.1 (#1728)
Bumps `asm.version` from 9.10 to 9.10.1.

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

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

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: 9.10.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: 9.10.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-06-13 08:29:17 +02:00
dependabot[bot] 0b14f39a2f build(deps): bump jackson.version from 2.21.3 to 2.21.4 (#1727)
Bumps `jackson.version` from 2.21.3 to 2.21.4.

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

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

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

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.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-06-10 13:21:25 +02:00
dependabot[bot] cf9b51669f build(deps): bump org.owasp:dependency-check-maven from 12.2.0 to 12.2.2 (#1726)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 12.2.0 to 12.2.2.
- [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.2.0...v12.2.2)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 12.2.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-06-10 13:21:00 +02:00
dependabot[bot] bf0a695dcc build(deps-dev): bump org.apache.maven.plugins:maven-site-plugin (#1718)
Bumps [org.apache.maven.plugins:maven-site-plugin](https://github.com/apache/maven-site-plugin) from 3.21.0 to 3.22.0.
- [Release notes](https://github.com/apache/maven-site-plugin/releases)
- [Commits](https://github.com/apache/maven-site-plugin/compare/maven-site-plugin-3.21.0...maven-site-plugin-3.22.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-site-plugin
  dependency-version: 3.22.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>
2026-05-28 07:19:41 +02:00
dependabot[bot] 8b8838a55a build(deps-dev): bump org.apache.maven.plugins:maven-assembly-plugin (#1717)
Bumps [org.apache.maven.plugins:maven-assembly-plugin](https://github.com/apache/maven-assembly-plugin) from 3.7.1 to 3.8.0.
- [Release notes](https://github.com/apache/maven-assembly-plugin/releases)
- [Commits](https://github.com/apache/maven-assembly-plugin/compare/maven-assembly-plugin-3.7.1...v3.8.0)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-assembly-plugin
  dependency-version: 3.8.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>
2026-05-28 07:19:29 +02:00
dependabot[bot] bbb111db20 build(deps): bump jackson.version from 2.21.2 to 2.21.3 (#1716)
Bumps `jackson.version` from 2.21.2 to 2.21.3.

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

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

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

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-core
  dependency-version: 2.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-version: 2.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.fasterxml.jackson.dataformat:jackson-dataformat-xml
  dependency-version: 2.21.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-05-28 07:19:16 +02:00
dependabot[bot] ab08c59737 build(deps-dev): bump org.apache.maven.plugins:maven-enforcer-plugin (#1715)
Bumps [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.6.2 to 3.6.3.
- [Release notes](https://github.com/apache/maven-enforcer/releases)
- [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.6.2...enforcer-3.6.3)

---
updated-dependencies:
- dependency-name: org.apache.maven.plugins:maven-enforcer-plugin
  dependency-version: 3.6.3
  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-28 07:19:03 +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
Lukasz Lenart 3227d5929a [maven-release-plugin] prepare release STRUTS_6_7_1 2025-02-02 10:16:20 +01:00
Lukasz Lenart 4a8ddbb274 Merge pull request #1202 from apache/fix/bom-version
Defines proper snapshot version for BOM
2025-02-02 10:10:35 +01:00
Lukasz Lenart 67ba458d03 Defines proper snapshot version for BOM 2025-02-02 10:00:09 +01:00
Lukasz Lenart 959b2182b2 Merge pull request #1195 from apache/fix/proper-version
Defines proper snapshot version
2025-01-24 08:28:32 +01:00
Lukasz Lenart 6de93e77db Defines proper snapshot version 2025-01-24 07:58:39 +01:00
Lukasz Lenart 59dc9301e0 Merge pull request #1194 from apache/fix/jenkinsfile-67x
Simplifies Jenkinsfile to build Struts 6.x
2025-01-22 08:18:56 +01:00
Lukasz Lenart 2fc4a822a4 Simplifies Jenkinsfile to build Struts 6.x 2025-01-22 07:52:34 +01:00
Kusal Kithul-Godage 6a6270f62a Merge pull request #1190 from apache/WW-5516-fix-temp-theme-fallback
WW-5516 Fix TemplateDir and Theme fallback to Request/Session/Application attributes
2025-01-22 13:24:09 +11:00
Lukasz Lenart 9153228b6a Merge pull request #1192 from apache/fix/nightlies-67x
Fixes nightlies builds
2025-01-21 20:31:59 +01:00
Lukasz Lenart 5713a74edc Fixes nightlies builds 2025-01-21 18:06:11 +01:00
Kusal Kithul-Godage 32071c71ad WW-5516 Fix TemplateDir and Theme fallback to Request/Session/Application attributes 2025-01-21 20:05:24 +11:00
Lukasz Lenart 7e4791985b Merge pull request #1175 from apache/feature/WW-5512-optional-inject
WW-5512 Extends the container to support injecting optional parameters into constructor
2025-01-21 07:25:58 +01:00
Lukasz Lenart 3c1e44ca2d WW-5512 Supports mixing required and optional parameters in any order 2025-01-19 09:53:01 +01:00
Lukasz Lenart 2ba8d97ad0 WW-5512 Supports mixing required and optional parameters 2025-01-19 09:36:27 +01:00
Lukasz Lenart cba0db0b07 Merge pull request #1181 from apache/fix/WW-5501-exclude
WW-5501 Only exclude malicious file names
2025-01-16 07:28:46 +01:00
Lukasz Lenart 688162cae2 WW-5501 Only exclude malicious file names 2025-01-14 07:47:39 +01:00
Lukasz Lenart a33c162f53 WW-5512 Extends the container to support injecting optional parameters into constructor 2025-01-05 17:45:11 +01:00
Lukasz Lenart c7a6daf5ce Merge pull request #1169 from apache/fix/WW-5500
WW-5500 Extends pattern to validate multipart uploads
2025-01-05 09:20:33 +01:00
Lukasz Lenart 370c7ce17a Merge pull request #1168 from apache/fix/WW-5498
WW-5498 Adds devMode errors as action messages to avoid breaking validation logic
2025-01-05 09:16:44 +01:00
Lukasz Lenart 55989befce Merge pull request #1167 from apache/fix/excluded-packages
Excludes new & old packages
2025-01-04 14:37:47 +01:00
Lukasz Lenart 2e4a4b5e36 Excludes new & old packages 2025-01-04 13:56:31 +01:00
Lukasz Lenart 9d07c3b4c3 WW-5498 Adds devMode errors as action messages to avoid breaking validation logic 2025-01-03 17:14:12 +01:00
Lukasz Lenart 575e4e8c9b WW-5500 Extends pattern to validate multipart uploads 2025-01-03 17:10:59 +01:00
Lukasz Lenart 74388dfba7 Merge pull request #1158 from apache/fix/updates-asf-yaml
Updates .asf.yaml to match main
2024-12-23 16:11:55 +01:00
Lukasz Lenart 4ee8b02262 Updates .asf.yaml to match main 2024-12-23 15:24:10 +01:00
Lukasz Lenart c8febc2050 Merge pull request #1156 from apache/feature/WW-5501-exclude
WW-5501 Exclude malicious names
2024-12-23 15:08:58 +01:00
Lukasz Lenart 3ea126388c WW-5501 Uses StringUtils.normalizeSpace instead of sanitizeNewlines 2024-12-23 14:13:26 +01:00
Lukasz Lenart 09993e3476 WW-5501 Exclude malicious names 2024-12-23 12:05:15 +01:00
Lukasz Lenart f5688e680b Merge pull request #1150 from apache/feature/prepare-development
Sets version to 6.7.1-SNAPSHOT to prepare for another development cycle
2024-12-18 15:42:21 +01:00
Lukasz Lenart 60a3db0a63 Sets version to 6.7.1-SNAPSHOT to prepare for another development cycle 2024-12-18 14:43:29 +01:00
Lukasz Lenart 4714d27678 Merge pull request #1141 from apache/fix/WW-5493-parameters
WW-5493 Restores parameters and uses getAttributes()
2024-12-18 07:42:47 +01:00
Lukasz Lenart 4feceee5f1 WW-5493 Restores parameters and uses getAttributes()
Instead of using parameters field directly, this PR uses getAttributes() instead to avoid coupling with internal Component state
2024-12-04 08:19:34 +01:00
211 changed files with 6198 additions and 842 deletions
+24 -3
View File
@@ -11,12 +11,33 @@ notifications:
jira_options: link label worklog
github:
description: "Apache Struts is a free, open-source, MVC framework for creating elegant, modern Java web applications"
homepage: https://struts.apache.org/
del_branch_on_merge: true
protected_branches:
master:
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
support/struts-6-x-x:
# 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
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
+29 -54
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
@@ -69,7 +87,7 @@ pipeline {
stages {
stage('Test & Coverage') {
steps {
sh './mvnw -B verify -Pcoverage -DskipAssembly --no-transfer-progress'
sh './mvnw -B verify -Pcoverage -DskipAssembly'
}
post {
always {
@@ -81,58 +99,15 @@ pipeline {
stage('Code Quality') {
when {
anyOf {
branch 'master'; branch 'release/struts-7-0-x'
branch 'support/struts-6-x-x'
}
}
steps {
withCredentials([string(credentialsId: 'asf-struts-sonarcloud', variable: 'SONARCLOUD_TOKEN')]) {
sh './mvnw -B -Pcoverage -DskipAssembly -Dsonar.login=${SONARCLOUD_TOKEN} verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar --no-transfer-progress'
sh './mvnw -B -Pcoverage -DskipAssembly -Dsonar.login=${SONARCLOUD_TOKEN} verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar'
}
}
}
stage('Build Source & JavaDoc') {
when {
branch 'release/struts-7-0-x'
}
steps {
dir("local-snapshots-dir/") {
deleteDir()
}
sh './mvnw -B source:jar javadoc:jar -DskipTests -DskipAssembly --no-transfer-progress'
}
}
stage('Deploy Snapshot') {
when {
branch 'release/struts-7-0-x'
}
steps {
withCredentials([file(credentialsId: 'lukaszlenart-repository-access-token', variable: 'CUSTOM_SETTINGS')]) {
sh './mvnw -s \${CUSTOM_SETTINGS} deploy -DskipTests -DskipAssembly --no-transfer-progress'
}
}
}
stage('Upload nightlies') {
when {
branch 'release/struts-7-0-x'
}
steps {
sh './mvnw -B package -DskipTests --no-transfer-progress'
sshPublisher(publishers: [
sshPublisherDesc(
configName: 'Nightlies',
transfers: [
sshTransfer(
remoteDirectory: '/struts/snapshot',
removePrefix: 'assembly/target/assembly/out',
sourceFiles: 'assembly/target/assembly/out/struts-*.zip',
cleanRemote: true
)
],
verbose: true
)
])
}
}
}
post {
always {
@@ -154,12 +129,12 @@ pipeline {
stages {
stage('Build') {
steps {
sh './mvnw -B clean install -DskipTests -DskipAssembly --no-transfer-progress'
sh './mvnw -B clean install -DskipTests -DskipAssembly'
}
}
stage('Test') {
steps {
sh './mvnw -B verify --no-transfer-progress'
sh './mvnw -B verify -DskipAssembly'
}
post {
always {
@@ -170,31 +145,31 @@ pipeline {
}
stage('Build Source & JavaDoc') {
when {
branch 'master'
branch 'support/struts-x-x-x'
}
steps {
dir("local-snapshots-dir/") {
deleteDir()
}
sh './mvnw -B source:jar javadoc:jar -DskipTests -DskipAssembly --no-transfer-progress'
sh './mvnw -B source:jar javadoc:jar -DskipTests -DskipAssembly'
}
}
stage('Deploy Snapshot') {
when {
branch 'master'
branch 'support/struts-6-x-x'
}
steps {
withCredentials([file(credentialsId: 'lukaszlenart-repository-access-token', variable: 'CUSTOM_SETTINGS')]) {
sh './mvnw -s \${CUSTOM_SETTINGS} deploy -DskipTests -DskipAssembly --no-transfer-progress'
sh './mvnw -s \${CUSTOM_SETTINGS} deploy -DskipTests -DskipAssembly'
}
}
}
stage('Upload nightlies') {
when {
branch 'master'
branch 'support/struts-6-x-x'
}
steps {
sh './mvnw -B package -DskipTests --no-transfer-progress'
sh './mvnw -B package -DskipTests'
sshPublisher(publishers: [
sshPublisherDesc(
configName: 'Nightlies',
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.12.0-SNAPSHOT</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.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>6.7.0</version>
<version>6.12.0-SNAPSHOT</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.0</version>
<version>6.12.0-SNAPSHOT</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.6</version>
<configuration>
<includes>
<include>it.org.apache.struts2.showcase.*Test</include>
+2 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-assembly</artifactId>
@@ -106,7 +106,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<version>3.8.0</version>
<executions>
<execution>
<id>make-assembly</id>
+3 -4
View File
@@ -25,11 +25,10 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.12.0-SNAPSHOT</version>
</parent>
<artifactId>struts2-bom</artifactId>
<version>6.7.0</version>
<packaging>pom</packaging>
<name>Struts 2 Bill of Materials</name>
@@ -44,7 +43,7 @@
</licenses>
<properties>
<struts-version.version>6.7.0</struts-version.version>
<struts-version.version>6.12.0-SNAPSHOT</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_0</tag>
<tag>STRUTS_6_9_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.0</version>
<version>6.12.0-SNAPSHOT</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.0</version>
<version>6.12.0-SNAPSHOT</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.0</version>
<version>6.12.0-SNAPSHOT</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.0</version>
<version>6.12.0-SNAPSHOT</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);
}
}
@@ -242,6 +242,41 @@ class ContainerImpl implements Container {
return toArray(parameterInjectors);
}
/**
* Gets parameter injectors with nulls for optional dependencies.
*
* @param member to which the parameters belong
* @param annotations on the parameters
* @param parameterTypes parameter types
* @return injections
*/
<M extends AccessibleObject & Member> ParameterInjector<?>[] getParametersInjectorsWithNulls(
M member,
Annotation[][] annotations,
Class<?>[] parameterTypes,
String defaultName
) throws MissingDependencyException {
final List<ParameterInjector<?>> parameterInjectors = new ArrayList<>();
final Iterator<Annotation[]> annotationsIterator = Arrays.asList(annotations).iterator();
for (Class<?> parameterType : parameterTypes) {
Inject annotation = findInject(annotationsIterator.next());
String name = annotation == null ? defaultName : annotation.value();
Key<?> key = Key.newInstance(parameterType, name);
try {
parameterInjectors.add(createParameterInjector(key, member));
} catch (MissingDependencyException e) {
if (annotation != null && annotation.required()) {
throw e;
} else {
parameterInjectors.add(createNullParameterInjector(key, member));
}
}
}
return toArray(parameterInjectors);
}
<T> ParameterInjector<T> createParameterInjector(Key<T> key, Member member) throws MissingDependencyException {
final InternalFactory<? extends T> factory = getFactory(key);
if (factory == null) {
@@ -252,6 +287,23 @@ class ContainerImpl implements Container {
return new ParameterInjector<>(externalContext, factory);
}
<T> ParameterInjector<T> createNullParameterInjector(Key<T> key, Member member) throws MissingDependencyException {
final InternalFactory<? extends T> factory = new InternalFactory<T>() {
@Override
public T create(InternalContext context) {
return null;
}
@Override
public Class<? extends T> type() {
return key.getType();
}
};
final ExternalContext<T> externalContext = ExternalContext.newInstance(member, key, this);
return new ParameterInjector<>(externalContext, factory);
}
private ParameterInjector<?>[] toArray(List<ParameterInjector<?>> parameterInjections) {
return parameterInjections.toArray(new ParameterInjector[0]);
}
@@ -361,7 +413,7 @@ class ContainerImpl implements Container {
Inject inject, ContainerImpl container, Constructor<T> constructor) throws MissingDependencyException {
return constructor.getParameterTypes().length == 0
? null // default constructor.
: container.getParametersInjectors(
: container.getParametersInjectorsWithNulls(
constructor,
constructor.getParameterAnnotations(),
constructor.getParameterTypes(),
@@ -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));
}
}
@@ -31,6 +31,15 @@ public interface OgnlCache<Key, Value> {
void putIfAbsent(Key key, Value value);
/**
* Removes the mapping for the given key, if present.
*
* @param key the key to remove
* @return the previous value associated with the key, or {@code null} if none
* @since 6.11.0
*/
Value remove(Key key);
int size();
void clear();
@@ -56,6 +56,11 @@ public class OgnlCaffeineCache<K, V> implements OgnlCache<K, V> {
cache.asMap().putIfAbsent(key, value);
}
@Override
public V remove(K key) {
return cache.asMap().remove(key);
}
@Override
public int size() {
return cache.asMap().size();
@@ -57,6 +57,11 @@ public class OgnlDefaultCache<K, V> implements OgnlCache<K, V> {
this.clearIfEvictionLimitExceeded();
}
@Override
public V remove(K key) {
return ognlCache.remove(key);
}
@Override
public int size() {
return ognlCache.size();
@@ -64,6 +64,11 @@ public class OgnlLRUCache<K, V> implements OgnlCache<K, V> {
ognlLRUCache.putIfAbsent(key, value);
}
@Override
public V remove(K key) {
return ognlLRUCache.remove(key);
}
@Override
public int size() {
return ognlLRUCache.size();
@@ -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;
@@ -36,6 +36,7 @@ public class DefaultExcludedPatternsChecker implements ExcludedPatternsChecker {
private static final Logger LOG = LogManager.getLogger(DefaultExcludedPatternsChecker.class);
public static final String[] EXCLUDED_PATTERNS = {
"(^|\\%\\{)(#?top\\.)[^\\s]*",
"(^|\\%\\{)((#?)(top(\\.|\\['|\\[\")|\\[\\d\\]\\.)?)(dojo|struts|session|request|response|application|servlet(Request|Response|Context)|parameters|context|_memberAccess)(\\.|\\[).*",
".*(^|\\.|\\[|\\'|\"|get)class(\\(\\.|\\[|\\'|\").*",
"actionErrors|actionMessages|fieldErrors"
@@ -21,6 +21,10 @@ package com.opensymphony.xwork2.util;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.LocalizedTextProvider;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.ognl.DefaultOgnlCacheFactory;
import com.opensymphony.xwork2.ognl.OgnlCache;
import com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -45,6 +49,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, so sessions serialized by
// an older node still deserialize here during a rolling upgrade. The caches this change made transient
// are simply discarded from such a stream and rebuilt by readObject.
private static final long serialVersionUID = -4563130226985473584L;
private static final Logger LOG = LogManager.getLogger(AbstractLocalizedTextProvider.class);
public static final String XWORK_MESSAGES_BUNDLE = "com/opensymphony/xwork2/xwork-messages";
@@ -56,16 +65,37 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
private static final String TOMCAT_WEBAPP_CLASSLOADER_BASE = "org.apache.catalina.loader.WebappClassLoaderBase";
private static final String RELOADED = "com.opensymphony.xwork2.util.LocalizedTextProvider.reloaded";
protected final ConcurrentMap<String, ResourceBundle> bundlesMap = new ConcurrentHashMap<>();
protected boolean devMode = false;
protected boolean reloadBundles = false;
protected boolean searchDefaultBundlesFirst = false; // Search default resource bundles first. Note: This flag may not be meaningful to all implementations.
private final ConcurrentMap<MessageFormatKey, MessageFormat> messageFormats = new ConcurrentHashMap<>();
private final ConcurrentMap<Integer, List<String>> classLoaderMap = new ConcurrentHashMap<>();
private final Set<String> missingBundles = ConcurrentHashMap.newKeySet();
private final ConcurrentMap<Integer, ClassLoader> delegatedClassLoaderMap = new ConcurrentHashMap<>();
// Dedicated monitor for bundlesMap-related synchronization: bundlesMap is reassigned by
// rebuildI18nCaches(), so locking on it directly would lock on a monitor that can change identity.
// transient + reinitialised in readObject: a bare Object is not Serializable.
private transient Object bundlesMapLock = new Object();
private static final int DEFAULT_I18N_CACHE_MAX_SIZE = 10000;
private volatile CacheType i18nCacheType = CacheType.WTLFU;
private volatile int i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
private <K, V> OgnlCache<K, V> buildI18nCache() {
return new DefaultOgnlCacheFactory<K, V>(i18nCacheMaxSize, i18nCacheType).buildOgnlCache();
}
// The OgnlCache implementations are themselves thread-safe; volatile only safely publishes the
// reference when rebuildI18nCaches() replaces a cache (during injection / readObject), so S3077
// ("volatile is not enough") does not apply here.
@SuppressWarnings("java:S3077")
protected transient volatile OgnlCache<String, ResourceBundle> bundlesMap = buildI18nCache();
@SuppressWarnings("java:S3077")
private transient volatile OgnlCache<MessageFormatKey, MessageFormat> messageFormats = buildI18nCache();
@SuppressWarnings("java:S3077")
private transient volatile OgnlCache<String, Boolean> missingBundles = buildI18nCache();
/**
* Adds the bundle to the internal list of default bundles.
* If the bundle already exists in the list it will be re-added.
@@ -99,6 +129,21 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
return Thread.currentThread().getContextClassLoader();
}
/** Test-support accessor: current number of cached resource bundles. */
protected int bundlesMapSize() {
return bundlesMap.size();
}
/** Test-support accessor: current number of cached missing-bundle markers. */
protected int missingBundlesSize() {
return missingBundles.size();
}
/** Test-support accessor: current number of cached message formats. */
protected int messageFormatsSize() {
return messageFormats.size();
}
@Inject(value = StrutsConstants.STRUTS_CUSTOM_I18N_RESOURCES, required = false)
public void setCustomI18NResources(String bundles) {
if (bundles != null && bundles.length() > 0) {
@@ -221,7 +266,7 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
* @param classLoader a {@link ClassLoader} to look up the bundle from if none can be found on the current thread's classloader
*/
public void setDelegatedClassLoader(final ClassLoader classLoader) {
synchronized (bundlesMap) {
synchronized (bundlesMapLock) {
delegatedClassLoaderMap.put(getCurrentThreadContextClassLoader().hashCode(), classLoader);
}
}
@@ -443,6 +488,52 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
this.searchDefaultBundlesFirst = Boolean.parseBoolean(searchDefaultBundlesFirst);
}
/**
* @param cacheType the type of cache to use for the localized-text caches
*
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_I18N_CACHE_TYPE, required = false)
public void setI18nCacheType(String cacheType) {
this.i18nCacheType = EnumUtils.getEnumIgnoreCase(CacheType.class, cacheType, CacheType.WTLFU);
rebuildI18nCaches();
}
/**
* @param cacheMaxSize the maximum size of each localized-text cache
*
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_I18N_CACHE_MAXSIZE, required = false)
public void setI18nCacheMaxSize(String cacheMaxSize) {
this.i18nCacheMaxSize = Integer.parseInt(cacheMaxSize);
rebuildI18nCaches();
}
/**
* Rebuilds the localized-text caches from the current type/size. Called during dependency injection
* (single-threaded startup, before the provider serves lookups); discards any warm-up entries.
*/
private void rebuildI18nCaches() {
bundlesMap = buildI18nCache();
messageFormats = buildI18nCache();
missingBundles = buildI18nCache();
}
private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException {
in.defaultReadObject();
bundlesMapLock = new Object();
// Field initialisers do not run during deserialization, so a stream written before these settings
// existed (an older node in a rolling upgrade) leaves them at null/0. Restore the defaults.
if (i18nCacheType == null) {
i18nCacheType = CacheType.WTLFU;
}
if (i18nCacheMaxSize <= 0) {
i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
}
rebuildI18nCaches();
}
/**
* Finds the given resource bundle by it's name.
* <p>
@@ -458,34 +549,32 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
ClassLoader classLoader = getCurrentThreadContextClassLoader();
String key = createMissesKey(String.valueOf(classLoader.hashCode()), aBundleName, locale);
if (missingBundles.contains(key)) {
if (missingBundles.get(key) != null) {
return null;
}
ResourceBundle bundle = null;
try {
if (bundlesMap.containsKey(key)) {
bundle = bundlesMap.get(key);
} else {
bundle = bundlesMap.get(key);
if (bundle == null) {
bundle = ResourceBundle.getBundle(aBundleName, locale, classLoader);
bundlesMap.putIfAbsent(key, bundle);
}
} catch (MissingResourceException ex) {
if (delegatedClassLoaderMap.containsKey(classLoader.hashCode())) {
try {
if (bundlesMap.containsKey(key)) {
bundle = bundlesMap.get(key);
} else {
bundle = bundlesMap.get(key);
if (bundle == null) {
bundle = ResourceBundle.getBundle(aBundleName, locale, delegatedClassLoaderMap.get(classLoader.hashCode()));
bundlesMap.putIfAbsent(key, bundle);
}
} catch (MissingResourceException e) {
LOG.debug("Missing resource bundle [{}]!", aBundleName, e);
missingBundles.add(key);
missingBundles.put(key, Boolean.TRUE);
}
} else {
LOG.debug("Missing resource bundle [{}]!", aBundleName);
missingBundles.add(key);
missingBundles.put(key, Boolean.TRUE);
}
}
return bundle;
@@ -18,6 +18,8 @@
*/
package com.opensymphony.xwork2.util;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.ognl.OgnlUtil;
@@ -33,6 +35,17 @@ import static java.util.stream.Collectors.toSet;
import static org.apache.commons.lang3.StringUtils.strip;
public class ConfigParseUtil {
// Size the cache to prevent excessive memory usage in environments with many classloaders and/or large numbers of classes being validated.
// While still providing a reasonable caching benefit for common cases (e.g. multiple Struts instances in the same container, or multiple calls to validate the same class across different containers).
// The cache is sized to allow for some level of caching across multiple classloaders, while still allowing for a reasonable number of classes to be cached per classloader.
private static final int MAX_CLASSLOADER_CACHE_SIZE = 25;
// The cache for validated classes is a two-level cache, with the first level keyed by ClassLoader and the second level keyed by class name.
private static final int MAX_CLASS_CACHE_PER_LOADER_SIZE = 50;
private static final Cache<ClassLoader, Cache<String, Class<?>>> VALIDATED_CLASS_CACHE = Caffeine.newBuilder()
.weakKeys()
.maximumSize(MAX_CLASSLOADER_CACHE_SIZE)
.build();
private ConfigParseUtil() {
}
@@ -73,7 +86,7 @@ public class ConfigParseUtil {
Set<Class<?>> classes = new HashSet<>();
for (String className : classNames) {
try {
classes.add(validatingClassLoader.loadClass(className));
classes.add(loadAndCacheClass(validatingClassLoader, className));
} catch (ClassNotFoundException e) {
throw new ConfigurationException("Cannot load class for exclusion/exemption configuration: " + className, e);
}
@@ -81,6 +94,35 @@ public class ConfigParseUtil {
return classes;
}
private static Class<?> loadAndCacheClass(ClassLoader validatingClassLoader, String className) throws ClassNotFoundException {
Cache<String, Class<?>> classLoaderCache = VALIDATED_CLASS_CACHE.get(validatingClassLoader,
key -> Caffeine.newBuilder().weakValues().maximumSize(MAX_CLASS_CACHE_PER_LOADER_SIZE).build());
try {
return classLoaderCache.get(className, key -> {
try {
return validatingClassLoader.loadClass(key);
} catch (ClassNotFoundException e) {
throw new ClassLookupException(e);
}
});
} catch (ClassLookupException e) {
// The ClassLookupException only serves to wrap the checked ClassNotFoundException thrown by ClassLoader.loadClass.
throw (ClassNotFoundException) e.getCause();
}
}
/**
* This is a wrapper class to allow the checked ClassNotFoundException thrown by ClassLoader.loadClass to be propagated
* We should always be able to unwrap this exception without risk of ClassCastException since the only code that can throw it is the mapping function passed to the cache
* and it only ever throws this wrapper with a ClassNotFoundException cause.
*/
private static final class ClassLookupException extends RuntimeException {
private ClassLookupException(ClassNotFoundException cause) {
super(cause);
}
}
public static Set<String> toPackageNamesSet(String newDelimitedPackageNames) throws ConfigurationException {
Set<String> packageNames = commaDelimitedStringToSet(newDelimitedPackageNames)
.stream().map(s -> strip(s, ".")).collect(toSet());
@@ -35,7 +35,7 @@ public final class DebugUtils {
log.error(message);
if (action instanceof ValidationAware) {
ValidationAware validationAware = (ValidationAware) action;
validationAware.addActionError(message);
validationAware.addActionMessage(message);
}
}
@@ -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);
}
@@ -33,6 +33,9 @@ import java.util.ResourceBundle;
*/
public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider {
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, see AbstractLocalizedTextProvider.
private static final long serialVersionUID = 7569216885652454296L;
private static final Logger LOG = LogManager.getLogger(GlobalLocalizedTextProvider.class);
public GlobalLocalizedTextProvider() {
@@ -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) {
@@ -36,6 +36,9 @@ import java.util.ResourceBundle;
*/
public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider {
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, see AbstractLocalizedTextProvider.
private static final long serialVersionUID = -4377984952850818176L;
private static final Logger LOG = LogManager.getLogger(StrutsLocalizedTextProvider.class);
/**
@@ -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();
@@ -100,6 +100,14 @@ public final class StrutsConstants {
/** The default locale for the Struts application */
public static final String STRUTS_LOCALE = "struts.locale";
/**
* When enabled, request-derived locales (from {@code Accept-Language}, used when {@code struts.locale} is
* unset) are restricted to the JVM's available-locale set; unavailable values fall back to the default.
*
* @since 6.11.0
*/
public static final String STRUTS_LOCALE_VALIDATE_REQUEST = "struts.locale.validateRequestLocale";
/** Whether to use a Servlet request parameter workaround necessary for some versions of WebLogic */
public static final String STRUTS_DISPATCHER_PARAMETERSWORKAROUND = "struts.dispatcher.parametersWorkaround";
@@ -288,6 +296,22 @@ public final class StrutsConstants {
*/
public static final String STRUTS_OGNL_BEANINFO_CACHE_FACTORY = "struts.ognl.beanInfoCacheFactory";
/**
* Specifies the type of cache to use for the localized-text provider caches. Valid values defined in
* {@link com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType}.
*
* @since 6.11.0
*/
public static final String STRUTS_I18N_CACHE_TYPE = "struts.i18n.cacheType";
/**
* Specifies the maximum size of each localized-text provider cache. Configure based on the cache type
* chosen and application-specific needs.
*
* @since 6.11.0
*/
public static final String STRUTS_I18N_CACHE_MAXSIZE = "struts.i18n.cacheMaxSize";
/**
* Specifies the type of cache to use for BeanInfo objects.
* @since 6.4.0
@@ -411,6 +435,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 +534,32 @@ 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";
/**
* See {@link org.apache.struts2.action.CspReportAction}
*
* @since 6.11.0
*/
public static final String STRUTS_CSP_REPORT_MAX_SIZE = "struts.csp.report.maxSize";
/**
* 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";
}
@@ -19,11 +19,16 @@
package org.apache.struts2.action;
import com.opensymphony.xwork2.ActionSupport;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import static org.apache.struts2.interceptor.csp.CspSettings.CSP_REPORT_TYPE;
@@ -51,7 +56,58 @@ import static org.apache.struts2.interceptor.csp.CspSettings.CSP_REPORT_TYPE;
* @see DefaultCspReportAction
*/
public abstract class CspReportAction extends ActionSupport implements ServletRequestAware, ServletResponseAware {
private static final Logger LOG = LogManager.getLogger(CspReportAction.class);
/**
* Default upper bound, in characters, on the report body accepted by {@link #withServletRequest}.
* CSP violation reports are small JSON documents; anything larger is not treated as a report.
*/
public static final int DEFAULT_MAX_REPORT_SIZE = 8192;
/**
* Largest value accepted for {@code struts.csp.report.maxSize}. A configured value above this is
* ignored, so that a mistyped setting cannot size a per-request buffer large enough to exhaust
* memory.
*/
private static final int MAX_REPORT_SIZE_LIMIT = 1024 * 1024;
private HttpServletRequest request;
private int maxReportSize = DEFAULT_MAX_REPORT_SIZE;
/**
* Sets the upper bound, in characters, on an accepted report body. A body exceeding this size is
* discarded and not passed to {@link #processReport(String)}.
* <p>
* The value is injected from {@code struts.csp.report.maxSize} when the action is built, which is
* before the interceptor stack runs. It is deliberately not an action property: the report body is
* read by {@link #withServletRequest(HttpServletRequest)}, which the {@code servletConfig}
* interceptor invokes ahead of {@code staticParams} and {@code params}, so a value applied by
* either of those would arrive too late to have any effect.
*
* @param maxReportSize maximum accepted report size in characters
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE, required = false)
public void setMaxReportSize(String maxReportSize) {
if (StringUtils.isBlank(maxReportSize)) {
return;
}
int size;
try {
size = Integer.parseInt(maxReportSize.trim());
} catch (NumberFormatException e) {
LOG.warn("Ignoring non-numeric {} value: {}, keeping {}",
StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE, maxReportSize, this.maxReportSize);
return;
}
if (size < 1 || size > MAX_REPORT_SIZE_LIMIT) {
LOG.warn("Ignoring out-of-range {} value: {}, expected 1..{}, keeping {}",
StrutsConstants.STRUTS_CSP_REPORT_MAX_SIZE, size, MAX_REPORT_SIZE_LIMIT, this.maxReportSize);
return;
}
this.maxReportSize = size;
}
@Override
public void withServletRequest(HttpServletRequest request) {
@@ -60,13 +116,36 @@ public abstract class CspReportAction extends ActionSupport implements ServletRe
}
try {
BufferedReader reader = request.getReader();
String cspReport = reader.readLine();
String cspReport = readReport(request.getReader());
if (cspReport == null) {
LOG.warn("Discarding CSP report larger than the configured limit of {} characters", maxReportSize);
return;
}
processReport(cspReport);
} catch (IOException ignored) {
}
}
/**
* Reads at most {@link #maxReportSize} characters from the report body.
*
* @param reader reader over the report body
* @return the report body, or {@code null} if it exceeds the limit
* @throws IOException if the body cannot be read
*/
private String readReport(Reader reader) throws IOException {
char[] buffer = new char[maxReportSize];
int total = 0;
int read;
while (total < buffer.length && (read = reader.read(buffer, total, buffer.length - total)) != -1) {
total += read;
}
if (total == buffer.length && reader.read() != -1) {
return null;
}
return new String(buffer, 0, total);
}
private boolean isCspReportRequest(HttpServletRequest request) {
if (!"POST".equals(request.getMethod()) || request.getContentLength() <= 0){
return false;
@@ -215,8 +215,8 @@ public class ActionComponent extends ContextBean {
HttpParameters.Builder builder = HttpParameters.create().withParent(parentParams);
if (attributes != null) {
builder = builder.withExtraParams(attributes);
if (getAttributes() != null) {
builder = builder.withExtraParams(getAttributes());
}
return builder.build();
}
@@ -68,10 +68,21 @@ 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;
protected Map<String, Object> attributes;
/**
* @deprecated use {@link #getAttributes} instead of directly depending on this field
*/
@Deprecated
protected Map<String, Object> parameters;
protected ActionMapper actionMapper;
protected boolean throwExceptionOnELFailure;
protected boolean performClearTagStateForTagPoolingServers = false;
@@ -86,7 +97,7 @@ public class Component {
*/
public Component(ValueStack stack) {
this.stack = stack;
this.attributes = new LinkedHashMap<>();
this.parameters = new LinkedHashMap<>();
getComponentStack().push(this);
}
@@ -279,7 +290,7 @@ public class Component {
*/
protected StrutsException fieldError(String field, String errorMsg, Exception e) {
String msg = "tag '" + getComponentName() + "', field '" + field +
(attributes != null && attributes.containsKey("name") ? "', name '" + attributes.get("name") : "") +
(getAttributes() != null && getAttributes().containsKey("name") ? "', name '" + getAttributes().get("name") : "") +
"': " + errorMsg;
throw new StrutsException(msg, e);
}
@@ -457,7 +468,7 @@ public class Component {
* @param params the parameters to copy.
*/
public void copyParams(Map<String, Object> params) {
stack.push(attributes);
stack.push(getAttributes());
stack.push(this);
try {
for (Map.Entry<String, Object> entry : params.entrySet()) {
@@ -467,7 +478,7 @@ public class Component {
// UI component attributes may contain hypens (e.g. data-ajax), but ognl
// can't handle that, and there can't be a component property with a hypen
// so into the parameters map it goes. See WW-4493
attributes.put(key, entry.getValue());
getAttributes().put(key, entry.getValue());
} else {
stack.setValue(key, entry.getValue());
}
@@ -500,7 +511,7 @@ public class Component {
*/
@Deprecated
public Map<String, Object> getParameters() {
return attributes;
return parameters;
}
/**
@@ -509,7 +520,7 @@ public class Component {
* @return the parameters. Is never <tt>null</tt>.
*/
public Map<String, Object> getAttributes() {
return attributes;
return parameters;
}
/**
@@ -518,7 +529,7 @@ public class Component {
* @param params the parameters to add.
*/
public void addAllParameters(Map<String, Object> params) {
attributes.putAll(params);
getAttributes().putAll(params);
}
/**
@@ -204,7 +204,7 @@ public class Form extends ClosingUIBean {
// keep a collection of the tag names for anything special the templates might want to do (such as pure client
// side validation)
if (!attributes.containsKey("tagNames")) {
if (!getAttributes().containsKey("tagNames")) {
// we have this if check so we don't do this twice (on open and close of the template)
addParameter("tagNames", new ArrayList());
}
@@ -137,14 +137,13 @@ public class Include extends Component {
urlBuf.append(page);
// Add request parameters
if (attributes.size() > 0) {
if (!getAttributes().isEmpty()) {
urlBuf.append('?');
String concat = "";
// Set parameters
for (Object next : attributes.entrySet()) {
Map.Entry entry = (Map.Entry) next;
for (Map.Entry<String, Object> entry : getAttributes().entrySet()) {
Object name = entry.getKey();
List values = (List) entry.getValue();
@@ -234,11 +233,11 @@ public class Include extends Component {
// instead, include tag requires that each parameter be a list of objects,
// just like the HTTP servlet interfaces are (String[])
if (value != null) {
List currentValues = (List) attributes.get(key);
List currentValues = (List) getAttributes().get(key);
if (currentValues == null) {
currentValues = new ArrayList();
attributes.put(key, currentValues);
getAttributes().put(key, currentValues);
}
currentValues.add(value);
@@ -81,8 +81,8 @@ public class Label extends UIBean {
if (value != null) {
addParameter("nameValue", findString(value));
} else if (key != null) {
Object nameValue = attributes.get("nameValue");
if (nameValue == null || nameValue.toString().length() == 0) {
Object nameValue = getAttributes().get("nameValue");
if (nameValue == null || nameValue.toString().isEmpty()) {
// get the label from a TextProvider (default value is the key)
String providedLabel = TextProviderHelper.getText(key, key, stack);
addParameter("nameValue", providedLabel);
@@ -67,7 +67,7 @@ public abstract class ListUIBean extends UIBean {
Object value = null;
if (list == null) {
list = attributes.get("list");
list = getAttributes().get("list");
}
if (list instanceof String) {
@@ -174,7 +174,7 @@ public class ServletUrlRenderer implements UrlRenderer {
namespace, actionName);
if (actionConfig != null) {
ActionMapping mapping = new ActionMapping(actionName, namespace, actionMethod, formComponent.attributes);
ActionMapping mapping = new ActionMapping(actionName, namespace, actionMethod, formComponent.getAttributes());
String result = urlHelper.buildUrl(formComponent.actionMapper.getUriFromActionMapping(mapping),
formComponent.request, formComponent.response, queryStringResult.getQueryParams(), scheme, formComponent.includeContext, true, false, false);
formComponent.addParameter("action", result);
@@ -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;
}
@@ -23,7 +23,6 @@ import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.ValueStack;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
@@ -32,7 +31,9 @@ import org.apache.struts2.components.template.Template;
import org.apache.struts2.components.template.TemplateEngine;
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;
@@ -48,6 +48,10 @@ import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static java.util.Collections.emptyMap;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.struts2.dispatcher.DispatcherConstants.ATTRIBUTES;
/**
* <p>
* UIBean is the standard superclass of all Struts UI components.
@@ -440,6 +444,9 @@ public abstract class UIBean extends Component {
private static final Logger LOG = LogManager.getLogger(UIBean.class);
static final String TEMPLATE_DIR = "templateDir";
static final String THEME = "theme";
protected static final String ATTR_FIELD_VALUE = "fieldValue";
protected static final String ATTR_NAME_VALUE = "nameValue";
protected static final String ATTR_VALUE = "value";
@@ -521,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;
@@ -546,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();
@@ -602,13 +616,18 @@ public abstract class UIBean extends Component {
result = findString(this.templateDir);
}
// Check Request, Session, Application scopes
if (isBlank(result)) {
result = (String) getAttrMap().get(TEMPLATE_DIR);
}
// Default template set
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
result = defaultTemplateDir;
}
// Defaults to 'template'
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
result = "template";
}
@@ -622,26 +641,36 @@ public abstract class UIBean extends Component {
result = findString(this.theme);
}
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
Form form = (Form) findAncestor(Form.class);
if (form != null) {
result = form.getTheme();
}
}
// Check Request, Session, Application scopes
if (isBlank(result)) {
result = (String) getAttrMap().get(THEME);
}
// Default theme set
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
result = defaultUITheme;
}
return result;
}
private Map<String, Object> getAttrMap() {
AttributeMap attrMap = (AttributeMap) getStack().getContext().get(ATTRIBUTES);
return attrMap != null ? attrMap : emptyMap();
}
public void evaluateParams() {
String gotTheme = getTheme();
addParameter("templateDir", getTemplateDir());
addParameter("theme", gotTheme);
addParameter(TEMPLATE_DIR, getTemplateDir());
addParameter(THEME, gotTheme);
addParameter("template", template != null ? findString(template) : getDefaultTemplate());
addParameter("dynamicAttributes", dynamicAttributes);
addParameter("themeExpansionToken", uiThemeExpansionToken);
@@ -864,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();
@@ -882,8 +910,8 @@ public abstract class UIBean extends Component {
*/
protected void applyValueParameter(String translatedName) {
// see if the value has been specified as a parameter already
if (attributes.containsKey(ATTR_VALUE)) {
attributes.put(ATTR_NAME_VALUE, attributes.get(ATTR_VALUE));
if (getAttributes().containsKey(ATTR_VALUE)) {
getAttributes().put(ATTR_NAME_VALUE, getAttributes().get(ATTR_VALUE));
} else {
if (evaluateNameValue()) {
final Class<?> valueClazz = getValueClassType();
@@ -111,7 +111,7 @@ public class URL extends ContextBean {
public URL(ValueStack stack, HttpServletRequest req, HttpServletResponse res) {
super(stack);
urlProvider = new ComponentUrlProvider(this, this.attributes);
urlProvider = new ComponentUrlProvider(this, this.getAttributes());
urlProvider.setHttpServletRequest(req);
urlProvider.setHttpServletResponse(res);
}
@@ -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
}
}
@@ -112,7 +112,7 @@ public class Dispatcher {
*/
public static final String REQUEST_POST_METHOD = "POST";
public static final String MULTIPART_FORM_DATA_REGEX = "^multipart/form-data(?:\\s*;\\s*boundary=[0-9a-zA-Z'()+_,\\-./:=?]{1,70})?(?:\\s*;\\s*charset=[a-zA-Z\\-0-9]{3,14})?";
public static final String MULTIPART_FORM_DATA_REGEX = "^multipart/form-data(?:\\s*;\\s*boundary=[0-9a-zA-Z'\"()+_,\\-./:=?]{1,70})?(?:\\s*;\\s*charset=[a-zA-Z\\-0-9]{3,14})?";
private static final String CONFIG_SPLIT_REGEX = "\\s*,\\s*";
@@ -152,6 +152,11 @@ public class Dispatcher {
*/
private String defaultLocale;
/**
* Store state of {@link StrutsConstants#STRUTS_LOCALE_VALIDATE_REQUEST} setting.
*/
private boolean validateRequestLocale = false;
/**
* Store state of StrutsConstants.STRUTS_MULTIPART_SAVEDIR setting.
*/
@@ -311,6 +316,18 @@ public class Dispatcher {
defaultLocale = val;
}
/**
* Modify state of {@link StrutsConstants#STRUTS_LOCALE_VALIDATE_REQUEST} setting.
*
* @param val New setting
*
* @since 6.11.0
*/
@Inject(value = StrutsConstants.STRUTS_LOCALE_VALIDATE_REQUEST, required = false)
public void setValidateRequestLocale(String val) {
validateRequestLocale = Boolean.parseBoolean(val);
}
/**
* Modify state of StrutsConstants.STRUTS_I18N_ENCODING setting.
*
@@ -451,7 +468,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 +506,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 +550,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 {
@@ -885,7 +967,7 @@ public class Dispatcher {
locale = LocaleUtils.toLocale(defaultLocale);
} catch (IllegalArgumentException e) {
try {
locale = request.getLocale();
locale = resolveRequestLocale(request);
LOG.warn(new ParameterizedMessage("Cannot convert 'struts.locale' = [{}] to proper locale, defaulting to request locale [{}]",
defaultLocale, locale), e);
} catch (RuntimeException rex) {
@@ -896,7 +978,7 @@ public class Dispatcher {
}
} else {
try {
locale = request.getLocale();
locale = resolveRequestLocale(request);
} catch (RuntimeException rex) {
LOG.warn("Cannot get locale from HTTP Request, falling back to system default locale", rex);
locale = Locale.getDefault();
@@ -905,6 +987,33 @@ public class Dispatcher {
return locale;
}
/**
* Resolves the request locale. When {@code struts.locale.validateRequestLocale} is enabled and the
* request locale is not part of the JVM's available-locale set, falls back to the configured
* {@code struts.locale} when set and parseable, otherwise the JVM default. When disabled (default),
* returns the request locale unchanged.
*
* @param request the current request
* @return the locale to use for this request
*
* @since 6.11.0
*/
protected Locale resolveRequestLocale(HttpServletRequest request) {
Locale locale = request.getLocale();
if (!validateRequestLocale || LocaleUtils.isAvailableLocale(locale)) {
return locale;
}
if (defaultLocale != null) {
try {
return LocaleUtils.toLocale(defaultLocale);
} catch (IllegalArgumentException e) {
LOG.debug("Configured 'struts.locale' = [{}] is not parseable; falling back to system default", defaultLocale);
}
}
LOG.debug("Request locale [{}] is not available; falling back to system default locale", locale);
return Locale.getDefault();
}
/**
* Return the path to save uploaded files to (this is configurable).
*
@@ -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,6 +20,7 @@ package org.apache.struts2.dispatcher.multipart;
import com.opensymphony.xwork2.LocaleProviderFactory;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
@@ -157,16 +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;
return FilenameUtils.getName(originalFileName);
}
}
@@ -42,7 +42,8 @@ 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;
/**
* Multipart form data request adapter for Jakarta Commons Fileupload package.
@@ -57,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).
@@ -99,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)) {
LOG.debug("Found file item: [{}]", sanitizeNewlines(item.getFieldName()));
// 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,7 +122,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
// 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: {}", sanitizeNewlines(item.getFieldName()));
LOG.debug("No file has been uploaded for the field: {}", normalizeSpace(item.getFieldName()));
return;
}
@@ -143,7 +150,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
long size = item.getSize();
if (maxStringLength != null && size > maxStringLength) {
LOG.debug("Form field {} of size {} bytes exceeds limit of {}.", sanitizeNewlines(item.getFieldName()), size, maxStringLength);
LOG.debug("Form field [{}] of size [{}] bytes exceeds limit of [{}].", normalizeSpace(item.getFieldName()), size, maxStringLength);
String errorKey = "struts.messages.upload.error.parameter.too.long";
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(), errorKey, null,
new Object[]{item.getFieldName(), maxStringLength, size});
@@ -238,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);
}
@@ -355,19 +366,36 @@ 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 {} {}", name, item);
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();
}
}
private String sanitizeNewlines(String before) {
return before.replaceAll("[\n\r]", "_");
}
}
@@ -45,6 +45,8 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
/**
* Multi-part form data request adapter for Jakarta Commons FileUpload package that
* leverages the streaming API rather than the traditional non-streaming API.
@@ -74,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);
}
}
}
@@ -252,7 +266,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
// prevent processing file field item if request size not allowed.
if (!requestSizePermitted) {
addFileSkippedError(itemStream.getName(), request);
LOG.debug("Skipped stream '{}', request maximum size ({}) exceeded.", itemStream.getName(), maxSize);
LOG.debug("Skipped stream [{}], request maximum size ({}) exceeded.", normalizeSpace(itemStream.getName()), maxSize);
continue;
}
@@ -296,7 +310,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
* @param request the servlet request
*/
protected void addFileSkippedError(String fileName, HttpServletRequest request) {
String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded.";
String exceptionMessage = "Skipped file " + normalizeSpace(fileName) + "; request size limit exceeded.";
long allowedMaxSize = maxSize != null ? maxSize : -1;
FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException(exceptionMessage, getRequestSize(request), allowedMaxSize);
LocalizedMessage message = buildErrorMessage(exception, new Object[]{fileName, getRequestSize(request), allowedMaxSize});
@@ -323,7 +337,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
values.add(fieldValue);
} catch (IOException e) {
LOG.warn("Failed to handle form field '{}'.", fieldName, e);
LOG.warn("Failed to handle form field [{}]", normalizeSpace(fieldName), e);
}
}
@@ -336,7 +350,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
protected void processFileItemStreamAsFileField(FileItemStream itemStream, String location) {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (itemStream.getName() == null || itemStream.getName().trim().isEmpty()) {
LOG.debug("No file has been uploaded for the field: {}", itemStream.getFieldName());
LOG.debug("No file has been uploaded for the field: {}", normalizeSpace(itemStream.getFieldName()));
return;
}
@@ -353,7 +367,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
try {
file.delete();
} catch (SecurityException se) {
LOG.warn("Failed to delete '{}' due to security exception above.", file.getName(), se);
LOG.warn("Failed to delete [{}] due to security exception above.", normalizeSpace(file.getName()), se);
}
}
}
@@ -385,7 +399,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
File file = File.createTempFile(prefix + "_", suffix, new File(location));
LOG.debug("Creating temporary file '{}' (originally '{}').", file.getName(), 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;
}
@@ -29,6 +29,8 @@ import org.apache.struts2.dispatcher.AttributeMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.PageContext;
import static org.apache.struts2.dispatcher.DispatcherConstants.ATTRIBUTES;
public class TagUtils {
private static final Logger LOG = LogManager.getLogger(TagUtils.class);
@@ -46,8 +48,8 @@ public class TagUtils {
} else {
LOG.trace("Adds the current PageContext to ActionContext");
stack.getActionContext()
.withPageContext(pageContext)
.with("attr", new AttributeMap(stack.getContext()));
.withPageContext(pageContext)
.with(ATTRIBUTES, new AttributeMap(stack.getContext()));
}
return stack;
@@ -24,6 +24,9 @@
### This can be used to set your default locale and encoding scheme
# struts.locale=en_US
### When true, restrict request-derived locales (Accept-Language, used when struts.locale is unset) to the
### JVM's available-locale set; unavailable values fall back to the default locale. Defaults to false.
struts.locale.validateRequestLocale=false
struts.i18n.encoding=UTF-8
### if specified, the default object factory can be overridden here
@@ -240,6 +243,13 @@ struts.ognl.expressionCacheType=wtlfu
### chosen and application-specific needs.
struts.ognl.expressionCacheMaxSize=10000
### Specifies the type of cache to use for the localized-text provider caches. See StrutsConstants for details.
struts.i18n.cacheType=wtlfu
### Specifies the maximum size of each localized-text provider cache. This should be configured based on the
### cache type chosen and application-specific needs.
struts.i18n.cacheMaxSize=10000
### Specifies the type of cache to use for BeanInfo objects. See StrutsConstants class for further information.
struts.ognl.beanInfoCacheType=wtlfu
@@ -247,6 +257,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 +297,11 @@ 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
### Maximum size, in characters, of a CSP violation report accepted by CspReportAction
### Reports larger than this are discarded. Values outside 1..1048576 are ignored.
struts.csp.report.maxSize=8192
### 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>
@@ -29,6 +29,7 @@
<constant name="struts.excludedClasses"
value="
com.opensymphony.xwork2.ActionContext,
org.apache.struts2.ActionContext,
java.lang.Class,
java.lang.ClassLoader,
java.lang.InheritableThreadLocal,
@@ -75,10 +76,15 @@
value="
antlr.build,
com.opensymphony.xwork2.config,
org.apache.struts2.config,
com.opensymphony.xwork2.inject,
org.apache.struts2.inject,
com.opensymphony.xwork2.ognl,
org.apache.struts2.ognl,
com.opensymphony.xwork2.security,
org.apache.struts2.security,
com.opensymphony.xwork2.util,
org.apache.struts2.util,
freemarker.core,
freemarker.ext.jsp,
freemarker.ext.rhino,
@@ -108,10 +114,15 @@
value="
antlr.build,
com.opensymphony.xwork2.config,
org.apache.struts2.config,
com.opensymphony.xwork2.inject,
org.apache.struts2.inject,
com.opensymphony.xwork2.ognl,
org.apache.struts2.ognl,
com.opensymphony.xwork2.security,
org.apache.struts2.security,
com.opensymphony.xwork2.util,
org.apache.struts2.util,
freemarker.core,
freemarker.ext.jsp,
freemarker.ext.rhino,
@@ -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());
}
}

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