Compare commits

...

170 Commits

Author SHA1 Message Date
Lukasz Lenart 02858b7ed5 [maven-release-plugin] prepare release STRUTS_7_1_0 2025-09-24 09:44:54 +02:00
Lukasz Lenart 8fcab78c5d [maven-release-plugin] rollback the release of STRUTS_7_1_0 2025-09-24 09:39:45 +02:00
Lukasz Lenart d50cfba32e [maven-release-plugin] prepare release STRUTS_7_1_0 2025-09-24 09:39:08 +02:00
Lukasz Lenart 1b43b53c6b WW-5504 Allows to use request instead of session attribute to store nonce (#1352) 2025-09-24 07:32:13 +02:00
Lukasz Lenart 5e5795559b Merge pull request #1350 from apache/dependabot/maven/main/org.assertj-assertj-core-3.27.4
Bump org.assertj:assertj-core from 3.27.3 to 3.27.4
2025-09-15 07:08:35 +02:00
Lukasz Lenart 9f424a94f7 Merge pull request #1349 from apache/dependabot/maven/main/org.apache.maven.plugins-maven-javadoc-plugin-3.11.3
Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.11.2 to 3.11.3
2025-09-15 07:08:17 +02:00
Lukasz Lenart 32a763b958 Merge pull request #1348 from apache/dependabot/maven/main/org.springframework-spring-framework-bom-6.2.11
Bump org.springframework:spring-framework-bom from 6.2.9 to 6.2.11
2025-09-15 07:08:00 +02:00
Lukasz Lenart 66b2dc0117 Merge pull request #1347 from apache/dependabot/maven/main/org.eclipse.jetty-jetty-maven-plugin-11.0.26
Bump org.eclipse.jetty:jetty-maven-plugin from 11.0.18 to 11.0.26
2025-09-15 07:07:40 +02:00
Lukasz Lenart 11c83d9484 Merge pull request #1341 from apache/dependabot/github_actions/github/codeql-action-3.30.3
Bump github/codeql-action from 3.30.2 to 3.30.3
2025-09-15 07:07:09 +02:00
dependabot[bot] a9d804ebed Bump org.assertj:assertj-core from 3.27.3 to 3.27.4
Bumps [org.assertj:assertj-core](https://github.com/assertj/assertj) from 3.27.3 to 3.27.4.
- [Release notes](https://github.com/assertj/assertj/releases)
- [Commits](https://github.com/assertj/assertj/compare/assertj-build-3.27.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-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:38:06 +00:00
dependabot[bot] 80eacb3400 Bump org.apache.maven.plugins:maven-javadoc-plugin from 3.11.2 to 3.11.3
Bumps [org.apache.maven.plugins:maven-javadoc-plugin](https://github.com/apache/maven-javadoc-plugin) from 3.11.2 to 3.11.3.
- [Release notes](https://github.com/apache/maven-javadoc-plugin/releases)
- [Commits](https://github.com/apache/maven-javadoc-plugin/compare/maven-javadoc-plugin-3.11.2...maven-javadoc-plugin-3.11.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:37:56 +00:00
dependabot[bot] e685523d19 Bump org.springframework:spring-framework-bom from 6.2.9 to 6.2.11
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.9 to 6.2.11.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.9...v6.2.11)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-version: 6.2.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:37:55 +00:00
dependabot[bot] 88d828f216 Bump org.eclipse.jetty:jetty-maven-plugin from 11.0.18 to 11.0.26
Bumps org.eclipse.jetty:jetty-maven-plugin from 11.0.18 to 11.0.26.

---
updated-dependencies:
- dependency-name: org.eclipse.jetty:jetty-maven-plugin
  dependency-version: 11.0.26
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:37:45 +00:00
dependabot[bot] 75e84a25e9 Bump github/codeql-action from 3.30.2 to 3.30.3
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.2 to 3.30.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.30.2...v3.30.3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 01:05:27 +00:00
Lukasz Lenart 2bd358ce1d Merge pull request #1338 from apache/dependabot/maven/main/org.apache.commons-commons-compress-1.28.0
WW-5569 Bump org.apache.commons:commons-compress from 1.27.1 to 1.28.0
2025-09-10 11:23:20 +02:00
dependabot[bot] ef63030c86 Bump org.apache.rat:apache-rat-plugin from 0.15 to 0.16.1 (#1339)
* Bump org.apache.rat:apache-rat-plugin from 0.15 to 0.16.1

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

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

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

* Fixes ASF licence reference

---------

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-09-10 10:50:53 +02:00
Lukasz Lenart d11153e942 Merge pull request #1333 from apache/dependabot/github_actions/github/codeql-action-3.30.2
Bump github/codeql-action from 3.30.1 to 3.30.2
2025-09-10 10:23:20 +02:00
Lukasz Lenart f278f4bbdb Merge pull request #1332 from apache/dependabot/maven/main/org.htmlunit-htmlunit-4.16.0
Bump org.htmlunit:htmlunit from 4.13.0 to 4.16.0
2025-09-10 10:23:00 +02:00
Lukasz Lenart 4031ab0c01 Merge pull request #1331 from apache/dependabot/maven/main/byte-buddy.version-1.17.7
Bump byte-buddy.version from 1.17.6 to 1.17.7
2025-09-10 10:22:42 +02:00
Lukasz Lenart 3bade0c8ce Merge pull request #1336 from apache/dependabot/maven/main/org.apache.logging.log4j-log4j-bom-2.25.1
WW-5567 Bump org.apache.logging.log4j:log4j-bom from 2.24.3 to 2.25.1
2025-09-10 10:22:07 +02:00
Lukasz Lenart 69cf6551e5 Merge pull request #1325 from apache/dependabot/maven/commons-validator-commons-validator-1.10.0
WW-5566 Bump commons-validator:commons-validator from 1.9.0 to 1.10.0
2025-09-10 10:19:05 +02:00
Lukasz Lenart 5443805ac1 Merge pull request #1323 from apache/dependabot/maven/org.apache.commons-commons-collections4-4.5.0
WW-5565 Bump org.apache.commons:commons-collections4 from 4.4 to 4.5.0
2025-09-10 10:16:54 +02:00
dependabot[bot] a599f0b6ed 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-10 06:03:38 +00:00
dependabot[bot] 4d375cc0bc Bump org.apache.logging.log4j:log4j-bom from 2.24.3 to 2.25.1
Bumps [org.apache.logging.log4j:log4j-bom](https://github.com/apache/logging-log4j2) from 2.24.3 to 2.25.1.
- [Release notes](https://github.com/apache/logging-log4j2/releases)
- [Changelog](https://github.com/apache/logging-log4j2/blob/2.x/RELEASE-NOTES.adoc)
- [Commits](https://github.com/apache/logging-log4j2/compare/rel/2.24.3...rel/2.25.1)

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-bom
  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:33 +00:00
dependabot[bot] ae58d5cea2 Bump github/codeql-action from 3.30.1 to 3.30.2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.1 to 3.30.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.30.1...v3.30.2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 06:03:23 +00:00
dependabot[bot] dcb58e1b73 Bump org.htmlunit:htmlunit from 4.13.0 to 4.16.0
Bumps [org.htmlunit:htmlunit](https://github.com/HtmlUnit/htmlunit) from 4.13.0 to 4.16.0.
- [Release notes](https://github.com/HtmlUnit/htmlunit/releases)
- [Commits](https://github.com/HtmlUnit/htmlunit/compare/4.13.0...4.16.0)

---
updated-dependencies:
- dependency-name: org.htmlunit:htmlunit
  dependency-version: 4.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 06:03:22 +00:00
dependabot[bot] e15ed665b2 Bump byte-buddy.version from 1.17.6 to 1.17.7
Bumps `byte-buddy.version` from 1.17.6 to 1.17.7.

Updates `net.bytebuddy:byte-buddy` from 1.17.6 to 1.17.7
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.6...byte-buddy-1.17.7)

Updates `net.bytebuddy:byte-buddy-agent` from 1.17.6 to 1.17.7
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.6...byte-buddy-1.17.7)

---
updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-version: 1.17.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-version: 1.17.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-10 06:03:18 +00:00
Lukasz Lenart fd469c8c3b Merge pull request #1327 from apache/fix/dependabot-branches
Enables Dependabot to support all the main branches
2025-09-10 08:02:23 +02:00
Lukasz Lenart 28b33b3210 Enables Dependabot to support all the main branches 2025-09-10 07:31:46 +02:00
Lukasz Lenart 4f4b4ec8a6 Merge pull request #1326 from apache/dependabot/github_actions/github/codeql-action-3.30.1
Bump github/codeql-action from 3.29.11 to 3.30.1
2025-09-08 16:31:06 +02:00
dependabot[bot] 8e5c692d36 Bump github/codeql-action from 3.29.11 to 3.30.1
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.11 to 3.30.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.29.11...v3.30.1)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 01:05:13 +00:00
Lukasz Lenart f0c4545f41 Merge pull request #1321 from apache/dependabot/github_actions/actions/setup-java-5
Bump actions/setup-java from 4 to 5
2025-09-02 12:16:12 +02:00
Lukasz Lenart d305c1d3df Merge pull request #1320 from apache/dependabot/github_actions/github/codeql-action-3.29.11
Bump github/codeql-action from 3.29.9 to 3.29.11
2025-09-02 12:15:50 +02:00
Lukasz Lenart d4bce051ed Merge pull request #1318 from apache/feature/WW-5511-javadoc
WW-5511 Adds missing JavaDocs to addCspHeaders method
2025-09-02 12:15:25 +02:00
Lukasz Lenart 16525f0ce2 Merge pull request #1319 from apache/feature/WW-5502-removes-sanitizeNewlines
WW-5502 Removes deprecated sanitizeNewlines method
2025-09-02 08:17:20 +02:00
dependabot[bot] ccaa61431a Bump commons-validator:commons-validator from 1.9.0 to 1.10.0
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>
2025-08-25 02:18:45 +00:00
dependabot[bot] c3877c2cf0 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-08-25 02:16:10 +00:00
dependabot[bot] cf215315e0 Bump actions/setup-java from 4 to 5
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 01:55:59 +00:00
dependabot[bot] 1bc3ebcb73 Bump github/codeql-action from 3.29.9 to 3.29.11
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.9 to 3.29.11.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.29.9...v3.29.11)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 01:51:10 +00:00
Lukasz Lenart b553129914 WW-5502 Removes deprecated sanitizeNewlines method 2025-08-22 08:39:40 +02:00
Lukasz Lenart 687f762507 WW-5511 Adds missing JavaDocs to addCspHeaders method 2025-08-22 08:37:52 +02:00
Lukasz Lenart 79c6bf94b3 Merge pull request #1309 from patientsknowbest/fix-struts-converter-factory
WW-5524 Fixup StrutsConverterFactory
2025-08-20 06:43:56 +02:00
Lukasz Lenart 0bc1a4391c Merge pull request #1307 from apache/fix/WW-5366-empty-file
WW-5366 Rejects empty files during upload
2025-08-19 07:25:02 +02:00
Lukasz Lenart 0ecd95a16b WW-5366 Rejects empty files during upload 2025-08-19 07:07:37 +02:00
Lukasz Lenart 014c2bd5dd Merge pull request #1302 from apache/dependabot/maven/byte-buddy.version-1.17.6
Bump byte-buddy.version from 1.17.5 to 1.17.6
2025-08-19 06:52:40 +02:00
Lukasz Lenart 1599d5dab5 Merge pull request #1312 from apache/dependabot/maven/org.apache.commons-commons-text-1.14.0
WW-5561 Bump org.apache.commons:commons-text from 1.13.1 to 1.14.0
2025-08-19 06:51:05 +02:00
Lukasz Lenart 3085ab2894 Merge pull request #1315 from apache/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-18 16:59:54 +02:00
Lukasz Lenart ed55bfc0be Merge pull request #1314 from apache/dependabot/github_actions/github/codeql-action-3.29.9
Bump github/codeql-action from 3.29.5 to 3.29.9
2025-08-18 16:59:38 +02:00
Lukasz Lenart e912492edb Merge pull request #1313 from apache/dependabot/maven/slf4j.version-2.0.17
Bump slf4j.version from 2.0.16 to 2.0.17
2025-08-18 16:59:24 +02:00
Lukasz Lenart 3507422559 Merge pull request #1311 from apache/dependabot/maven/org.apache.struts-struts-annotations-2.0
WW-5554 Bump org.apache.struts:struts-annotations from 1.0.8 to 2.0
2025-08-18 16:58:08 +02:00
dependabot[bot] ffd699682d Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 02:35:23 +00:00
dependabot[bot] e9f98f02ec Bump github/codeql-action from 3.29.5 to 3.29.9
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.5 to 3.29.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.29.5...v3.29.9)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 02:33:43 +00:00
dependabot[bot] e1e7c41344 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-08-11 02:36:39 +00:00
dependabot[bot] 3f138e3c0a Bump org.apache.commons:commons-text from 1.13.1 to 1.14.0
Bumps [org.apache.commons:commons-text](https://github.com/apache/commons-text) from 1.13.1 to 1.14.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.13.1...rel/commons-text-1.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 02:36:31 +00:00
dependabot[bot] f30cf63d43 Bump org.apache.struts:struts-annotations from 1.0.8 to 2.0
Bumps [org.apache.struts:struts-annotations](https://github.com/apache/struts-annotations) from 1.0.8 to 2.0.
- [Release notes](https://github.com/apache/struts-annotations/releases)
- [Commits](https://github.com/apache/struts-annotations/commits)

---
updated-dependencies:
- dependency-name: org.apache.struts:struts-annotations
  dependency-version: '2.0'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 02:33:13 +00:00
Martin Ashby d721f3fb65 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-07 22:42:23 +01:00
Lukasz Lenart aaa4984eac Merge pull request #1308 from apache/fix/build-check-6x
Fixes build check for Struts 6.x
2025-08-07 19:29:39 +02:00
Lukasz Lenart fd82d6354f Fixes build check for Struts 6.x 2025-08-07 18:54:21 +02:00
Lukasz Lenart dd68723a47 Merge pull request #1283 from apache/dependabot/maven/org.apache.commons-commons-fileupload2-jakarta-servlet6-2.0.0-M4
Bump org.apache.commons:commons-fileupload2-jakarta-servlet6 from 2.0.0-M2 to 2.0.0-M4
2025-08-06 10:16:27 +02:00
Lukasz Lenart fa78ec43a8 Merge pull request #1305 from apache/dependabot/github_actions/github/codeql-action-3.29.5
Bump github/codeql-action from 3.29.2 to 3.29.5
2025-08-06 09:58:03 +02:00
Lukasz Lenart 1579e62e8a Merge pull request #1304 from apache/dependabot/maven/org.springframework-spring-framework-bom-6.2.9
Bump org.springframework:spring-framework-bom from 6.2.3 to 6.2.9
2025-08-06 09:57:40 +02:00
Lukasz Lenart 5238e9c1cc Merge pull request #1301 from apache/dependabot/maven/org.codehaus.mojo-exec-maven-plugin-3.5.1
Bump org.codehaus.mojo:exec-maven-plugin from 3.5.0 to 3.5.1
2025-08-06 09:57:12 +02:00
Lukasz Lenart f16abd171b Adds missing test cases of temporary files 2025-08-06 09:42:56 +02:00
Lukasz Lenart c8ab33e3ce Uses lambda
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-06 09:00:43 +02:00
Lukasz Lenart 37144a03f7 Adds missing test cases of temporary files 2025-08-06 08:58:14 +02:00
Lukasz Lenart 824e7121b0 Reuses logic to create temporary file 2025-08-06 08:58:14 +02:00
Lukasz Lenart 63f2c8bdec Adds missing JavaDocs 2025-08-06 08:58:14 +02:00
Lukasz Lenart 359b6549ef Fixes readStream method to avoid to memory leaks 2025-08-06 08:58:14 +02:00
Lukasz Lenart 201b9e860e Cleans up temporary files 2025-08-06 08:58:13 +02:00
Lukasz Lenart a1c4cb60a2 Uses a dedicated RequestContext to avoid NPE 2025-08-06 08:58:13 +02:00
dependabot[bot] b796bab432 Bump org.apache.commons:commons-fileupload2-jakarta-servlet6
Bumps org.apache.commons:commons-fileupload2-jakarta-servlet6 from 2.0.0-M2 to 2.0.0-M4.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-06 08:58:13 +02:00
Lukasz Lenart 7d19767f71 Merge pull request #1295 from apache/festure/claude-code
Defines basic set of files to work with Claude Code
2025-08-06 08:55:51 +02:00
dependabot[bot] 436ce3560b Bump github/codeql-action from 3.29.2 to 3.29.5
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.2 to 3.29.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.29.2...v3.29.5)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-04 02:15:09 +00:00
dependabot[bot] 1d6b05b829 Bump org.springframework:spring-framework-bom from 6.2.3 to 6.2.9
Bumps [org.springframework:spring-framework-bom](https://github.com/spring-projects/spring-framework) from 6.2.3 to 6.2.9.
- [Release notes](https://github.com/spring-projects/spring-framework/releases)
- [Commits](https://github.com/spring-projects/spring-framework/compare/v6.2.3...v6.2.9)

---
updated-dependencies:
- dependency-name: org.springframework:spring-framework-bom
  dependency-version: 6.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 02:08:44 +00:00
dependabot[bot] 2487d583f3 Bump byte-buddy.version from 1.17.5 to 1.17.6
Bumps `byte-buddy.version` from 1.17.5 to 1.17.6.

Updates `net.bytebuddy:byte-buddy` from 1.17.5 to 1.17.6
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.5...byte-buddy-1.17.6)

Updates `net.bytebuddy:byte-buddy-agent` from 1.17.5 to 1.17.6
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.5...byte-buddy-1.17.6)

---
updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-version: 1.17.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-version: 1.17.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 02:07:01 +00:00
dependabot[bot] 3c5c7e5b7b Bump org.codehaus.mojo:exec-maven-plugin from 3.5.0 to 3.5.1
Bumps [org.codehaus.mojo:exec-maven-plugin](https://github.com/mojohaus/exec-maven-plugin) from 3.5.0 to 3.5.1.
- [Release notes](https://github.com/mojohaus/exec-maven-plugin/releases)
- [Commits](https://github.com/mojohaus/exec-maven-plugin/compare/3.5.0...3.5.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 02:06:37 +00:00
Lukasz Lenart 7fef6c1ee0 Merge pull request #1299 from apache/dependabot/maven/org.jfree-jfreechart-1.5.6
Bump org.jfree:jfreechart from 1.5.5 to 1.5.6
2025-07-22 07:19:28 +02:00
Lukasz Lenart 73e05ef978 Merge pull request #1298 from apache/dependabot/maven/org.htmlunit-htmlunit-4.13.0
Bump org.htmlunit:htmlunit from 4.11.1 to 4.13.0
2025-07-22 07:19:08 +02:00
Lukasz Lenart 3a6ee8aacc Merge pull request #1297 from apache/dependabot/maven/com.github.ben-manes.caffeine-caffeine-3.2.2
Bump com.github.ben-manes.caffeine:caffeine from 3.2.1 to 3.2.2
2025-07-22 07:18:52 +02:00
Lukasz Lenart a654da4db3 Merge pull request #1296 from apache/dependabot/maven/org.apache.maven.plugins-maven-enforcer-plugin-3.6.1
Bump org.apache.maven.plugins:maven-enforcer-plugin from 3.5.0 to 3.6.1
2025-07-22 07:18:34 +02:00
dependabot[bot] de0b6f13b6 Bump org.jfree:jfreechart from 1.5.5 to 1.5.6
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>
2025-07-21 02:10:26 +00:00
dependabot[bot] f640012618 Bump org.htmlunit:htmlunit from 4.11.1 to 4.13.0
Bumps [org.htmlunit:htmlunit](https://github.com/HtmlUnit/htmlunit) from 4.11.1 to 4.13.0.
- [Release notes](https://github.com/HtmlUnit/htmlunit/releases)
- [Commits](https://github.com/HtmlUnit/htmlunit/compare/4.11.1...4.13.0)

---
updated-dependencies:
- dependency-name: org.htmlunit:htmlunit
  dependency-version: 4.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 02:09:31 +00:00
dependabot[bot] be3d57a725 Bump com.github.ben-manes.caffeine:caffeine from 3.2.1 to 3.2.2
Bumps [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/ben-manes/caffeine/releases)
- [Commits](https://github.com/ben-manes/caffeine/compare/v3.2.1...v3.2.2)

---
updated-dependencies:
- dependency-name: com.github.ben-manes.caffeine:caffeine
  dependency-version: 3.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 02:09:15 +00:00
dependabot[bot] ae178a0190 Bump org.apache.maven.plugins:maven-enforcer-plugin from 3.5.0 to 3.6.1
Bumps [org.apache.maven.plugins:maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.5.0 to 3.6.1.
- [Release notes](https://github.com/apache/maven-enforcer/releases)
- [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.5.0...enforcer-3.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 02:08:38 +00:00
Lukasz Lenart d3812548b7 Defines basic set of files to work with Claude Code 2025-07-19 10:50:30 +02:00
Lukasz Lenart 85a4be3402 Merge pull request #1293 from apache/dependabot/maven/org.apache.commons-commons-text-1.13.1
Bump org.apache.commons:commons-text from 1.13.0 to 1.13.1
2025-07-19 10:33:24 +02:00
Lukasz Lenart 5b420899f5 Merge pull request #1292 from apache/dependabot/maven/org.apache.maven.plugins-maven-failsafe-plugin-3.5.3
Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.2 to 3.5.3
2025-07-19 10:32:59 +02:00
Lukasz Lenart 0c45954dcd Merge pull request #1291 from apache/dependabot/maven/maven-surefire-plugin.version-3.5.3
Bump maven-surefire-plugin.version from 3.5.2 to 3.5.3
2025-07-19 10:32:42 +02:00
Lukasz Lenart 00c26a7a19 Merge pull request #1290 from apache/dependabot/maven/org.owasp-dependency-check-maven-12.1.3
Bump org.owasp:dependency-check-maven from 12.1.1 to 12.1.3
2025-07-19 10:25:40 +02:00
Lukasz Lenart cc4d05f6bf Merge pull request #1289 from apache/dependabot/maven/parent/org.apache.commons-commons-lang3-3.18.0
WW-5557 Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0 in /parent
2025-07-19 09:41:37 +02:00
dependabot[bot] bf9d1548ea Bump org.apache.commons:commons-text from 1.13.0 to 1.13.1
Bumps org.apache.commons:commons-text from 1.13.0 to 1.13.1.

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 02:12:30 +00:00
dependabot[bot] b87ec9804c Bump org.apache.maven.plugins:maven-failsafe-plugin from 3.5.2 to 3.5.3
Bumps [org.apache.maven.plugins:maven-failsafe-plugin](https://github.com/apache/maven-surefire) from 3.5.2 to 3.5.3.
- [Release notes](https://github.com/apache/maven-surefire/releases)
- [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.5.2...surefire-3.5.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 02:10:35 +00:00
dependabot[bot] 530f3157f9 Bump maven-surefire-plugin.version from 3.5.2 to 3.5.3
Bumps `maven-surefire-plugin.version` from 3.5.2 to 3.5.3.

Updates `org.apache.maven.surefire:surefire-junit47` from 3.5.2 to 3.5.3

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

---
updated-dependencies:
- dependency-name: org.apache.maven.surefire:surefire-junit47
  dependency-version: 3.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: org.apache.maven.plugins:maven-surefire-plugin
  dependency-version: 3.5.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 02:08:12 +00:00
dependabot[bot] 6b9e6998b0 Bump org.owasp:dependency-check-maven from 12.1.1 to 12.1.3
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 12.1.1 to 12.1.3.
- [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.1...v12.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 02:06:15 +00:00
dependabot[bot] e8e8e66580 Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0 in /parent
Bumps org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-lang3
  dependency-version: 3.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-12 01:36:29 +00:00
Lukasz Lenart 12814e7a7f Merge pull request #1287 from apache/dependabot/maven/org.easymock-easymock-5.6.0
Bump org.easymock:easymock from 5.4.0 to 5.6.0
2025-07-07 08:16:11 +02:00
dependabot[bot] 2bfa4b6335 Bump org.easymock:easymock from 5.4.0 to 5.6.0
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>
2025-07-07 06:04:25 +00:00
Lukasz Lenart 607401c2d6 Merge pull request #1284 from apache/dependabot/maven/weld.version-6.0.3.Final
Bump weld.version from 6.0.2.Final to 6.0.3.Final
2025-07-07 08:03:26 +02:00
dependabot[bot] 281aa0004f Bump weld.version from 6.0.2.Final to 6.0.3.Final
Bumps `weld.version` from 6.0.2.Final to 6.0.3.Final.

Updates `org.jboss.weld:weld-core-impl` from 6.0.2.Final to 6.0.3.Final
- [Commits](https://github.com/weld/core/compare/6.0.2.Final...6.0.3.Final)

Updates `org.jboss.weld.se:weld-se-core` from 6.0.2.Final to 6.0.3.Final

---
updated-dependencies:
- dependency-name: org.jboss.weld:weld-core-impl
  dependency-version: 6.0.3.Final
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: org.jboss.weld.se:weld-se-core
  dependency-version: 6.0.3.Final
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 05:42:00 +00:00
Lukasz Lenart 033b55c13d Merge pull request #1281 from apache/dependabot/maven/com.fasterxml.jackson-jackson-bom-2.19.1
WW-5553 Bump com.fasterxml.jackson:jackson-bom from 2.18.3 to 2.19.1
2025-07-07 07:40:45 +02:00
dependabot[bot] e043d41451 Bump com.fasterxml.jackson:jackson-bom from 2.18.3 to 2.19.1
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.3 to 2.19.1.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.3...jackson-bom-2.19.1)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  dependency-version: 2.19.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 05:32:28 +00:00
Lukasz Lenart 6b80a1cf50 Merge pull request #1288 from apache/dependabot/github_actions/github/codeql-action-3.29.2
Bump github/codeql-action from 3.29.0 to 3.29.2
2025-07-07 07:20:27 +02:00
dependabot[bot] b70f2aed1f Bump github/codeql-action from 3.29.0 to 3.29.2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.0 to 3.29.2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.29.0...v3.29.2)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 02:07:17 +00:00
dependabot[bot] 6466bb120d Bump com.github.ben-manes.caffeine:caffeine from 3.2.0 to 3.2.1 (#1282)
Bumps [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/ben-manes/caffeine/releases)
- [Commits](https://github.com/ben-manes/caffeine/compare/v3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: com.github.ben-manes.caffeine:caffeine
  dependency-version: 3.2.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-06-23 07:15:58 +02:00
dependabot[bot] e2f6bc287e Bump ossf/scorecard-action from 2.4.1 to 2.4.2 (#1278)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/f49aabe0b5af0936a0987cfb85d86b75731b0186...05b42c624433fc40578a4040d5cf5e36ddca8cde)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.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-06-16 07:13:26 +02:00
dependabot[bot] c57a989584 Bump jasperreports7.version from 7.0.1 to 7.0.3 (#1275)
* Bump jasperreports7.version from 6.21.3 to 7.0.3

Bumps `jasperreports7.version` from 6.21.3 to 7.0.3.

Updates `net.sf.jasperreports:jasperreports` from 6.21.3 to 7.0.3
- [Release notes](https://github.com/Jaspersoft/jasperreports/releases)
- [Changelog](https://github.com/Jaspersoft/jasperreports/blob/master/changes.txt)
- [Commits](https://github.com/Jaspersoft/jasperreports/compare/6.21.3...7.0.3)

Updates `net.sf.jasperreports:jasperreports-pdf` from 7.0.1 to 7.0.3
- [Release notes](https://github.com/Jaspersoft/jasperreports/releases)
- [Changelog](https://github.com/Jaspersoft/jasperreports/blob/master/changes.txt)
- [Commits](https://github.com/Jaspersoft/jasperreports/compare/7.0.1...7.0.3)

---
updated-dependencies:
- dependency-name: net.sf.jasperreports:jasperreports
  dependency-version: 7.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: net.sf.jasperreports:jasperreports-pdf
  dependency-version: 7.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

* Reverts to 6.21.3 to support old plugin

---------

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-06-16 07:13:06 +02:00
dependabot[bot] 548c70a021 Bump weld.version from 5.1.2.Final to 6.0.2.Final (#1270)
Bumps `weld.version` from 5.1.2.Final to 6.0.2.Final.

Updates `org.jboss.weld:weld-core-impl` from 5.1.2.Final to 6.0.2.Final
- [Commits](https://github.com/weld/core/compare/5.1.2.Final...6.0.2.Final)

Updates `org.jboss.weld.se:weld-se-core` from 5.1.2.Final to 6.0.2.Final

---
updated-dependencies:
- dependency-name: org.jboss.weld:weld-core-impl
  dependency-version: 6.0.2.Final
  dependency-type: direct:development
  update-type: version-update:semver-major
- dependency-name: org.jboss.weld.se:weld-se-core
  dependency-version: 6.0.2.Final
  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-06-16 07:01:08 +02:00
dependabot[bot] 66f8e0fe0a Bump github/codeql-action from 3.28.17 to 3.29.0 (#1280)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.17 to 3.29.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.28.17...v3.29.0)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.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-06-16 06:58:34 +02:00
dependabot[bot] 8124e6cfa4 WW-5551 Bump commons-beanutils:commons-beanutils from 1.9.4 to 1.11.0 in /parent (#1277)
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
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 06:54:22 +02:00
dependabot[bot] 5b7991412b Bump org.awaitility:awaitility from 4.2.2 to 4.3.0 (#1276)
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 06:52:47 +02:00
dependabot[bot] 90692cf919 WW-5550 Bump asm.version from 9.7.1 to 9.8 (#1274)
Bumps `asm.version` from 9.7.1 to 9.8.

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

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

---
updated-dependencies:
- dependency-name: org.ow2.asm:asm
  dependency-version: '9.8'
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: org.ow2.asm:asm-commons
  dependency-version: '9.8'
  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-06-16 06:51:58 +02:00
dependabot[bot] f06bc517ab Bump byte-buddy.version from 1.17.2 to 1.17.5 (#1268)
Bumps `byte-buddy.version` from 1.17.2 to 1.17.5.

Updates `net.bytebuddy:byte-buddy` from 1.17.2 to 1.17.5
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.2...byte-buddy-1.17.5)

Updates `net.bytebuddy:byte-buddy-agent` from 1.17.2 to 1.17.5
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.2...byte-buddy-1.17.5)

---
updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-version: 1.17.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-version: 1.17.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>
2025-05-12 14:27:05 +02:00
Lukasz Lenart d27d3fba3a WW-5548 Defines proper request attributes when forwarding or including final path (#1265)
* WW-5548 Defines proper request attributes when forwarding or including final path

* Fies typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* WW-5548 Drops RequestDispatcher parameters as they should be defined by Servlet container

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-12 14:26:42 +02:00
dependabot[bot] f66e078f10 Bump org.apache.maven.plugins:maven-project-info-reports-plugin (#1269)
Bumps [org.apache.maven.plugins:maven-project-info-reports-plugin](https://github.com/apache/maven-project-info-reports-plugin) from 3.8.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.8.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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 14:25:40 +02:00
dependabot[bot] 1331e99742 Bump org.apache.felix:maven-bundle-plugin from 5.1.9 to 6.0.0 (#1271)
Bumps org.apache.felix:maven-bundle-plugin from 5.1.9 to 6.0.0.

---
updated-dependencies:
- dependency-name: org.apache.felix:maven-bundle-plugin
  dependency-version: 6.0.0
  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-05-12 14:25:27 +02:00
Lukasz Lenart 9c40e2d0df Uses proper name of check to pass (#1266) 2025-05-12 14:24:02 +02:00
Lukasz Lenart a7d915d627 Adjusts required checks to the new structure (#1264)
more details can be found here https://github.com/apache/infrastructure-asfyaml/blob/main/README.md#branch-protection
2025-05-08 06:52:33 +02:00
Lukasz Lenart d9c9d2b030 WW-5546 Fixes NPE when uploaded file is empty (#1263) 2025-05-07 07:18:15 +02:00
dependabot[bot] d4f1adbb05 Bump com.github.ben-manes.caffeine:caffeine from 3.1.8 to 3.2.0 (#1257)
Bumps [com.github.ben-manes.caffeine:caffeine](https://github.com/ben-manes/caffeine) from 3.1.8 to 3.2.0.
- [Release notes](https://github.com/ben-manes/caffeine/releases)
- [Commits](https://github.com/ben-manes/caffeine/compare/v3.1.8...v3.2.0)

---
updated-dependencies:
- dependency-name: com.github.ben-manes.caffeine:caffeine
  dependency-version: 3.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>
2025-05-06 20:47:20 +02:00
dependabot[bot] 1ab2fcc620 Bump org.htmlunit:htmlunit from 4.9.0 to 4.11.1 (#1258)
Bumps [org.htmlunit:htmlunit](https://github.com/HtmlUnit/htmlunit) from 4.9.0 to 4.11.1.
- [Release notes](https://github.com/HtmlUnit/htmlunit/releases)
- [Commits](https://github.com/HtmlUnit/htmlunit/compare/4.9.0...4.11.1)

---
updated-dependencies:
- dependency-name: org.htmlunit:htmlunit
  dependency-version: 4.11.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-05-06 20:44:55 +02:00
dependabot[bot] b22022c25f Bump github/codeql-action from 3.28.15 to 3.28.17 (#1261)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.15 to 3.28.17.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.28.15...v3.28.17)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.17
  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-05-06 20:40:16 +02:00
dependabot[bot] 101dc02f77 Bump org.owasp:dependency-check-maven from 10.0.4 to 12.1.1 (#1259)
Bumps [org.owasp:dependency-check-maven](https://github.com/dependency-check/DependencyCheck) from 10.0.4 to 12.1.1.
- [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.1)

---
updated-dependencies:
- dependency-name: org.owasp:dependency-check-maven
  dependency-version: 12.1.1
  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-05-06 20:39:52 +02:00
dependabot[bot] 84a97741ed Bump org.apache.maven.plugins:maven-site-plugin from 3.20.0 to 3.21.0 (#1256)
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-05-06 20:39:19 +02:00
Lukasz Lenart e326799532 WW-5544 Marks ReflectionContextFactory as deprecated and uses ActionContext instead (#1254) (#1255) 2025-04-27 18:43:36 +02:00
Lukasz Lenart 285d2d3681 Merge pull request #1244 from apache/dependabot/maven/byte-buddy.version-1.17.2
Bump byte-buddy.version from 1.17.1 to 1.17.2
2025-04-27 16:10:30 +02:00
Lukasz Lenart b650adb539 Merge pull request #1245 from apache/dependabot/maven/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-04-27 16:10:09 +02:00
Lukasz Lenart 993620eb67 Merge pull request #1247 from apache/dependabot/github_actions/actions/upload-artifact-4.6.2
Bump actions/upload-artifact from 4.6.1 to 4.6.2
2025-04-27 16:09:47 +02:00
Lukasz Lenart d564829a82 Merge pull request #1253 from apache/dependabot/github_actions/github/codeql-action-3.28.15
Bump github/codeql-action from 3.28.10 to 3.28.15
2025-04-27 16:09:14 +02:00
dependabot[bot] eb4a5a0086 Bump github/codeql-action from 3.28.10 to 3.28.15
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.10 to 3.28.15.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.28.10...v3.28.15)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-14 01:09:45 +00:00
Lukasz Lenart bcb3d91293 Uses new url for Maven Badges app (#1252) 2025-04-06 10:36:41 +02:00
dependabot[bot] 6f753fb8d9 Bump actions/upload-artifact from 4.6.1 to 4.6.2
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1...ea165f8d65b6e75b540449e92b4886f43607fa02)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 01:16:29 +00:00
dependabot[bot] ab55fda4ae 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-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 01:19:56 +00:00
dependabot[bot] 33bef8e665 Bump byte-buddy.version from 1.17.1 to 1.17.2
Bumps `byte-buddy.version` from 1.17.1 to 1.17.2.

Updates `net.bytebuddy:byte-buddy` from 1.17.1 to 1.17.2
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.1...byte-buddy-1.17.2)

Updates `net.bytebuddy:byte-buddy-agent` from 1.17.1 to 1.17.2
- [Release notes](https://github.com/raphw/byte-buddy/releases)
- [Changelog](https://github.com/raphw/byte-buddy/blob/master/release-notes.md)
- [Commits](https://github.com/raphw/byte-buddy/compare/byte-buddy-1.17.1...byte-buddy-1.17.2)

---
updated-dependencies:
- dependency-name: net.bytebuddy:byte-buddy
  dependency-type: direct:development
  update-type: version-update:semver-patch
- dependency-name: net.bytebuddy:byte-buddy-agent
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 01:19:55 +00:00
Kusal Kithul-Godage d47143c689 Merge pull request #1243 from apache/WW-5534-model-driven-struts-param 2025-03-07 01:14:15 +11:00
Kusal Kithul-Godage 96f838df1e WW-5534 Proper fix ModelDriven parameter injection and allowlisting 2025-03-07 00:07:53 +11:00
dependabot[bot] 7ae52ccfcc Bump org.eclipse.transformer:transformer-maven-plugin (#1216)
Bumps [org.eclipse.transformer:transformer-maven-plugin](https://github.com/eclipse/transformer) from 0.5.0 to 1.0.0.
- [Release notes](https://github.com/eclipse/transformer/releases)
- [Commits](https://github.com/eclipse/transformer/compare/0.5.0...1.0.0)

---
updated-dependencies:
- dependency-name: org.eclipse.transformer:transformer-maven-plugin
  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-03-06 06:51:40 +01:00
Lukasz Lenart 1c3219e7f9 WW-5455 Defines a new plugin to support Jasper Reports 7 (#1124)
* WW-5455 Defines a new plugin to support Jasper Reports 7

* WW-5455 Allows action to modify result behaviour

* WW-5455 Defines exporter as an extension point

* WW-5455 Extracts logic to create connection or dataSource

* WW-5455 Uses defaultDelimiter as more meaningful name

* WW-5455 Fixes parent version after re-basing

* WW-5455 Updates pom to match the latest SNAPSHOT version

* WW-5455 Cleans up code

* WW-5455 Adds the new plugin to BOM

* WW-5455 Fixes broken test
2025-03-06 06:50:38 +01:00
bill-humblcloud 9861b834ab add conversion handling for OffsetDateTime (#1241) 2025-03-06 06:48:52 +01:00
Kusal Kithul-Godage 747859b72a Merge pull request #1237 from apache/WW-5534-annotation-allowlist-proxy
WW-5534 Allow @StrutsParameter recognition and OGNL allowlist for Spring proxies
2025-03-06 12:24:07 +11:00
Kusal Kithul-Godage 433c4837fd WW-5534 Add coverage for proxy resolution 2025-03-06 12:14:47 +11:00
Kusal Kithul-Godage 4cc874d426 Merge pull request #1236 from apache/WW-5534-proxyutil
WW-5534 Simplify ProxyUtil, add OgnlCache#computeIfAbsent
2025-03-05 21:48:25 +11:00
gregh3269 f35c808efc Sync tag with main ftl template. (#1212)
Co-authored-by: Greg Huber <ghuber@apache.org>
2025-03-04 16:45:45 +01:00
Lukasz Lenart 6bb71caa90 Uses proper config to avoid failing a build when generating JavaDocs (#1240)
* Uses proper config to avoid failing a build when generating JavaDocs

* Removes unneeded source option
2025-03-03 13:22:50 +01:00
Kusal Kithul-Godage aeaa4f26cf WW-5534 Allow @StrutsParameter recognition and OGNL allowlist for Spring proxies 2025-03-03 21:09:36 +11:00
Kusal Kithul-Godage 117b9c741b WW-5534 Add coverage for OgnlUtil#getBeanInfo exception propagation 2025-03-03 21:05:18 +11:00
dependabot[bot] e09572f3b0 Bump com.fasterxml.jackson:jackson-bom from 2.18.2 to 2.18.3 (#1238)
Bumps [com.fasterxml.jackson:jackson-bom](https://github.com/FasterXML/jackson-bom) from 2.18.2 to 2.18.3.
- [Commits](https://github.com/FasterXML/jackson-bom/compare/jackson-bom-2.18.2...jackson-bom-2.18.3)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson:jackson-bom
  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-03-03 10:50:19 +01:00
Kusal Kithul-Godage 8579a10f59 WW-5534 Simplify ProxyUtil, add OgnlCache#computeIfAbsent 2025-02-28 04:38:15 +11:00
bill-humblcloud 0d2d11071b WW-5530 make DateConverter work for LocalDate and LocalTime (#1223)
* make DateConverter work for LocalDate and LocalTime
* add date tests
2025-02-26 08:29:36 +01:00
Kusal Kithul-Godage 1100a34e9d Merge pull request #1233 from apache/WW-5533-jee11-supp
WW-5533 Add compilation support for Jakarta EE 11
2025-02-25 23:46:20 +11:00
Kusal Kithul-Godage fa951718dc WW-5533 Add compilation support for Jakarta EE 11 2025-02-25 23:34:33 +11:00
Kusal Kithul-Godage 2ab0a3c843 Merge pull request #1234 from apache/WW-5376-bom-fix
WW-5376 Fix BOM leaking unrelated dependencies
2025-02-25 22:16:05 +11:00
Kusal Kithul-Godage 15ee2ac4a0 Merge pull request #1232 from apache/WW-5532-upg-deps
WW-5532 Upgrade and align various dependencies
2025-02-25 19:34:39 +11:00
Kusal Kithul-Godage d2cb444bcf WW-5376 Fix BOM leaking unrelated dependencies 2025-02-25 19:32:44 +11:00
dependabot[bot] 84cf666041 Bump ossf/scorecard-action from 2.4.0 to 2.4.1 (#1229)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/62b2cac7ed8198b15735ed49ab1e5cf35480ba46...f49aabe0b5af0936a0987cfb85d86b75731b0186)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  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-02-25 07:00:25 +01:00
dependabot[bot] 31cb558fc6 Bump github/codeql-action from 3.28.9 to 3.28.10 (#1228)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.9 to 3.28.10.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.28.9...v3.28.10)

---
updated-dependencies:
- dependency-name: github/codeql-action
  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-02-25 07:00:00 +01:00
dependabot[bot] 8b66b575a1 Bump actions/upload-artifact from 4.6.0 to 4.6.1 (#1227)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4.6.0 to 4.6.1.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08...4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  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-02-25 06:59:24 +01:00
Kusal Kithul-Godage 077e985899 WW-5532 Upgrade and align various dependencies 2025-02-25 12:25:47 +11:00
Lukasz Lenart 5c22c0da74 Merge pull request #1224 from apache/fix/WW-5529-maxlength-s7
WW-5529 Adds autogenerated files with updated desc
2025-02-19 14:06:11 +01:00
Lukasz Lenart e9116ae3da WW-5529 Adds autogenerated files with updated desc 2025-02-18 16:28:22 +01:00
Lukasz Lenart d727fbf6be [maven-release-plugin] prepare for next development iteration 2025-02-17 10:41:25 +01:00
Lukasz Lenart 4603706b40 [maven-release-plugin] prepare release STRUTS_7_0_3 2025-02-17 10:41:17 +01:00
Lukasz Lenart 556522e9b2 WW-5529 Drops unused misleading setter setMaxLength in favour of setMaxlength (#1221) 2025-02-17 10:09:14 +01:00
Lukasz Lenart 1db8a72bb5 WW-5525 Fixes NPE when checking if expressions is acceptable (#1201)
* WW-5525 Fixes NPE when checking if expressions is acceptable

* WW-5525 Fixes bugs introduced by previous commit
2025-02-17 08:53:42 +01:00
Kusal Kithul-Godage 31c3fc50ed Merge pull request #1214 from apache/WW-5525-proxyutil-npe
WW-5525 Fix NPE in ProxyUtil for SecurityMemberAccess originating static members
2025-02-17 17:10:48 +11:00
dependabot[bot] adcd1df0ca Bump github/codeql-action from 3.28.8 to 3.28.9 (#1217)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.8 to 3.28.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Commits](https://github.com/github/codeql-action/compare/v3.28.8...v3.28.9)

---
updated-dependencies:
- dependency-name: github/codeql-action
  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-02-16 12:06:16 +01:00
Lukasz Lenart 568b292bd4 WW-5501 Reverts changes related to WW-5501 (#1219) 2025-02-16 11:42:09 +01:00
Kusal Kithul-Godage 02e17f5912 Merge pull request #1213 from apache/WW-5528-multipart-illegal-char-errors
WW-5528 Ensure multipart upload illegal characters reported as error
2025-02-06 18:36:41 +11:00
Kusal Kithul-Godage 3c856c92a1 WW-5525 Fix NPE in ProxyUtil for SecurityMemberAccess originating static members 2025-02-06 12:23:42 +11:00
Kusal Kithul-Godage ff249c64cd WW-5528 Ensure multipart upload illegal characters reported as error 2025-02-06 11:48:42 +11:00
Lukasz Lenart a1de1cfdeb [maven-release-plugin] prepare for next development iteration 2025-02-04 07:07:24 +01:00
149 changed files with 5807 additions and 1781 deletions
+7 -5
View File
@@ -17,16 +17,18 @@ github:
protected_branches:
main:
# contexts are the names of checks that must pass.
contexts:
- build
required_status_checks:
contexts:
- "Build and Test (JDK 17)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
release/struts-6-7-x:
release/*:
# contexts are the names of checks that must pass.
contexts:
- build
required_status_checks:
contexts:
- "Build and Test (JDK 8)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
+6 -1
View File
@@ -8,4 +8,9 @@ updates:
directory: "/"
schedule:
interval: "weekly"
ignore: []
target-branch: "main"
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
target-branch: "release/struts-6-7-x"
+5 -5
View File
@@ -44,20 +44,20 @@ jobs:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Setup Java JDK
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
cache: 'maven'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.8
uses: github/codeql-action/init@v3.30.3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3.28.8
uses: github/codeql-action/autobuild@v3.30.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.8
uses: github/codeql-action/analyze@v3.30.3
with:
category: "/language:${{matrix.language}}"
+13 -6
View File
@@ -30,18 +30,25 @@ env:
jobs:
build:
name: Build and Test
name: Build and Test (JDK ${{ matrix.java }})${{ matrix.profile == '-Pjakartaee11' && ' with Jakarta EE 11' || matrix.profile }}
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17', '21' ]
include:
- java: '17'
profile: ''
- java: '21'
profile: ''
- java: '21'
profile: '-Pjakartaee11'
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: actions/setup-java@v4
uses: actions/checkout@v5
- name: Setup Java ${{ matrix.java }}
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ matrix.java }}
cache: 'maven'
- name: Build with Maven on Java ${{ matrix.java }}
run: mvn -B -V -DskipAssembly verify --no-transfer-progress
- name: Maven Verify on Java ${{ matrix.java }}${{ matrix.profile == '-Pjakartaee11' && ' (Jakarta EE 11)' || matrix.profile }}
run: mvn -B -V -DskipAssembly verify ${{ matrix.profile }} --no-transfer-progress
+4 -4
View File
@@ -41,12 +41,12 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@v4 # 3.1.0
uses: actions/checkout@v5 # 3.1.0
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # 2.4.0
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # 2.4.2
with:
results_file: results.sarif
results_format: sarif
@@ -58,13 +58,13 @@ jobs:
publish_results: true
- name: "Upload artifact"
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # 4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@0701025a8b1600e416be4f3bb5a830b1aa6af01e # 2.22.11
uses: github/codeql-action/upload-sarif@aa90e97ad2ed17cde6a43e89f70138299e64f837 # 2.22.11
with:
sarif_file: results.sarif
+2 -2
View File
@@ -33,10 +33,10 @@ jobs:
runs-on: ubuntu-latest
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: 21
+3
View File
@@ -46,3 +46,6 @@ test-output
# Tidelift CLI scanner
.tidelift
# Claude Code local settings
.claude/
+210
View File
@@ -0,0 +1,210 @@
# Claude Code Best Practices for Apache Struts
This document outlines essential practices for working with Claude Code on the Apache Struts project, based on security improvements and testing implementations.
## Project Context
- **Framework**: Apache Struts 2 (Java-based web framework)
- **Technology Stack**: Jakarta EE, Maven, Java 17
- **Key Libraries**: OGNL, Commons FileUpload2, Log4j2, JUnit, AssertJ
- **Build System**: Maven with standard lifecycle
## Security-First Development
### Critical Security Principles
1. **Never create files in system temp directories** - always use controlled application directories
2. **Use UUID-based naming** for temporary files to prevent collisions and path traversal
3. **Implement proper resource cleanup** with try-with-resources and finally blocks
4. **Track all temporary resources** for explicit cleanup (security critical)
5. **Validate all user inputs** and sanitize filenames before processing
### Security Implementation Patterns
```java
// GOOD: Secure temporary file creation
protected File createTemporaryFile(String fileName, Path location) {
String uid = UUID.randomUUID().toString().replace("-", "_");
File file = location.resolve("upload_" + uid + ".tmp").toFile();
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
return file;
}
// BAD: Insecure system temp usage
File tempFile = File.createTempFile("struts_upload_", "_" + item.getName());
```
### Resource Management
- Always use tracking collections for cleanup: `List<File> temporaryFiles`, `List<DiskFileItem> diskFileItems`
- Implement protected cleanup methods for extensibility
- Make cleanup idempotent and exception-safe
- Use try-with-resources for streams and I/O operations
## Testing Implementation
### Test Structure & Coverage
- **Unit Tests**: Test individual methods with mocked dependencies
- **Integration Tests**: Test complete workflows with real file I/O
- **Security Tests**: Verify directory traversal prevention, secure naming
- **Error Handling Tests**: Test exception scenarios and error reporting
- **Cleanup Tests**: Verify resource cleanup and tracking
### Testing Commands
```bash
# Run all tests
mvn test -DskipAssembly
# Run specific test class
mvn test -Dtest=JakartaMultiPartRequestTest
# Run tests with specific method pattern
mvn test -Dtest=*MultiPartRequestTest#temporal*
```
use `-DskipAssembly` to avoid building zip files with docs, examples, etc.
### Test Implementation Patterns
```java
@Test
public void securityTestExample() throws Exception {
// given - malicious input
String maliciousFilename = "malicious../../../etc/passwd";
// when - process input
processFile(maliciousFilename);
// then - verify security measures
assertThat(tempFile.getParent()).isEqualTo(saveDir);
assertThat(tempFile.getName()).doesNotContain("..");
}
```
### Reflection-Based Testing for Private Members
```java
Field privateField = ClassName.class.getDeclaredField("fieldName");
privateField.setAccessible(true);
@SuppressWarnings("unchecked")
List<Type> values = (List<Type>) privateField.get(instance);
```
## JavaDoc Documentation Standards
### Class-Level Documentation
```java
/**
* Brief description of the class purpose and functionality.
*
* <p>Detailed description with multiple paragraphs explaining:</p>
* <ul>
* <li>Key features and capabilities</li>
* <li>Security considerations</li>
* <li>Resource management approach</li>
* <li>Usage patterns and examples</li>
* </ul>
*
* <p>Usage example:</p>
* <pre>
* ClassName instance = new ClassName();
* try {
* instance.process(data);
* } finally {
* instance.cleanUp(); // Always clean up resources
* }
* </pre>
*
* @see RelatedClass
* @see org.apache.package.ImportantInterface
*/
```
### Method-Level Documentation
```java
/**
* Brief description of what the method does.
*
* <p>Detailed description explaining:</p>
* <ol>
* <li>Step-by-step process</li>
* <li>Security considerations</li>
* <li>Error handling behavior</li>
* <li>Resource management</li>
* </ol>
*
* <p>Security note: This method creates files in controlled directory
* to prevent security vulnerabilities.</p>
*
* @param paramName description of parameter and constraints
* @param saveDir the directory where files will be created (must exist)
* @return description of return value
* @throws IOException if file creation fails or I/O error occurs
* @see #relatedMethod(Type)
* @see #cleanUpMethod()
*/
```
### Documentation Best Practices
- **Always document security implications** in methods handling files/user input
- **Include usage examples** for complex methods and classes
- **Document exception conditions** and error handling behavior
- **Reference related methods** using `@see` tags
- **Explain resource management** responsibilities
- **Use `<p>`, `<ol>`, `<ul>`, `<li>` for structured content
- **Include `<pre>` blocks** for code examples
## Error Handling & Logging
### Error Message Patterns
```java
// Localized error messages
LocalizedMessage errorMessage = buildErrorMessage(
e.getClass(),
e.getMessage(),
new Object[]{fileName}
);
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
```
### Logging Best Practices
```java
// Use parameterized logging for performance
LOG.debug("Processing file: {} in directory: {}",
normalizeSpace(fileName), saveDir);
// Log security-relevant operations
LOG.warn("Failed to delete temporary file: {}", tempFile.getAbsolutePath());
// Use appropriate log levels
LOG.debug() // Development details
LOG.info() // General information
LOG.warn() // Potential issues
LOG.error() // Serious problems
```
## Code Quality Standards
### Method Scope & Extensibility
- Use `protected` for methods that subclasses might override
- Implement cleanup methods as separate `protected` methods
- Make core functionality extensible while maintaining security
### Exception Handling
- Catch specific exceptions rather than generic `Exception`
- Log exceptions with context but continue cleanup operations
- Use try-finally blocks to ensure cleanup always occurs
### Code Organization
- Group related methods together (processing, cleanup, utilities)
- Keep security-critical code in dedicated methods
- Use clear, descriptive method and variable names
- Follow existing project conventions and patterns
## Common Pitfalls to Avoid
1. **File Security**: Never use `File.createTempFile()` without directory control
2. **Resource Leaks**: Always track and clean up temporary files
3. **Test Coverage**: Don't forget to test error conditions and cleanup
4. **Documentation**: Always document security implications
5. **Exception Handling**: Don't let cleanup failures affect main operations
6. **Path Validation**: Always validate and sanitize file paths
7. **Reflection Testing**: Use `@SuppressWarnings("unchecked")` appropriately
This document should be updated as new patterns and practices emerge during development.
+1 -1
View File
@@ -19,7 +19,7 @@ The Apache Struts web framework
[![Build Status](https://ci-builds.apache.org/buildStatus/icon?job=Struts%2FStruts+Core%2Fmain)](https://ci-builds.apache.org/job/Struts/job/Struts%20Core/job/main/)
[![Java Build](https://github.com/apache/struts/actions/workflows/maven.yml/badge.svg)](https://github.com/apache/struts/actions/workflows/maven.yml)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.struts/struts2-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.apache.struts/struts2-core/)
[![Maven Central](https://maven-badges.sml.io/maven-central/org.apache.struts/struts2-core/badge.svg)](https://maven-badges.sml.io/maven-central/org.apache.struts/struts2-core/)
[![Javadocs](https://javadoc.io/badge/org.apache.struts/struts2-core.svg)](https://javadoc.io/doc/org.apache.struts/struts2-core)
[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=apache_struts&metric=coverage)](https://sonarcloud.io/summary/new_code?id=apache_struts)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/apache/struts/badge)](https://deps.dev/maven/org.apache.struts%3Astruts2-core)
+2 -4
View File
@@ -24,7 +24,8 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>struts2-apps</artifactId>
<packaging>pom</packaging>
@@ -93,13 +94,10 @@
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.platformVersion}</version>
<scope>test</scope>
</dependency>
</dependencies>
+3 -3
View File
@@ -24,12 +24,12 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>7.0.2</version>
<version>7.1.0</version>
<name>Struts 2 Rest Showcase Webapp</name>
<description>Struts 2 Rest Showcase Example</description>
@@ -107,7 +107,7 @@
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>11.0.18</version>
<version>11.0.26</version>
<configuration>
<stopKey>CTRL+C</stopKey>
<stopPort>8999</stopPort>
+3 -3
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-showcase</artifactId>
@@ -207,7 +207,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.2</version>
<version>3.5.3</version>
<configuration>
<includes>
<include>it.org.apache.struts2.showcase.*Test</include>
@@ -234,7 +234,7 @@
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>11.0.18</version>
<version>11.0.26</version>
<configuration>
<stopKey>CTRL+C</stopKey>
<stopPort>8999</stopPort>
@@ -0,0 +1,63 @@
/*
* 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 it.org.apache.struts2.showcase;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.html.HtmlSubmitInput;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class ModelDrivenTest {
private WebClient webClient;
@Before
public void setUp() throws Exception {
webClient = new WebClient();
}
@After
public void tearDown() throws Exception {
webClient.close();
}
@Test
public void submit() throws Exception {
HtmlPage page = webClient.getPage(ParameterUtils.getBaseUrl() + "/modelDriven/modelDriven.action");
HtmlForm form = page.getForms().get(0);
form.getInputByName("name").setValue("Johannes");
form.getInputByName("age").setValue("21");
form.getInputByName("bustedBefore").setChecked(true);
form.getTextAreaByName("description").setText("Deals bugs");
HtmlSubmitInput button = form.getInputByValue("Submit");
page = button.click();
assertThat(page.getElementById("name").asNormalizedText()).isEqualTo("Johannes");
assertThat(page.getElementById("age").asNormalizedText()).isEqualTo("21");
assertThat(page.getElementById("bustedBefore").asNormalizedText()).isEqualTo("true");
assertThat(page.getElementById("description").asNormalizedText()).isEqualTo("Deals bugs");
}
}
@@ -229,7 +229,8 @@ public class StrutsParametersTest {
}
private void assertText(Map<String, String> params, String text) throws IOException {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(ParameterUtils.getBaseUrl()).path("/paramsannotation/test.action");
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(ParameterUtils.getBaseUrl())
.path("/paramsannotation/test.action");
params.forEach(builder::queryParam);
String url = builder.toUriString();
HtmlPage page = webClient.getPage(url);
+7 -1
View File
@@ -24,7 +24,8 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>struts2-assembly</artifactId>
@@ -171,6 +172,11 @@
<artifactId>struts2-jasperreports-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jasperreports7-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jfreechart-plugin</artifactId>
+30 -44
View File
@@ -21,30 +21,18 @@
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.0.2</version>
<artifactId>struts2-project</artifactId>
<version>7.1.0</version>
</parent>
<artifactId>struts2-bom</artifactId>
<version>7.0.2</version>
<packaging>pom</packaging>
<name>Struts 2 Bill of Materials</name>
<description>Struts 2 Bill of Materials</description>
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
<name>Struts BOM</name>
<description>Struts Bill of Materials (BOM)</description>
<properties>
<struts-version.version>7.0.2</struts-version.version>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
@@ -54,115 +42,113 @@
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-core</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-async-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-bean-validation-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-cdi-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-convention-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-config-browser-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-embeddedjsp-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-gxp-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jasperreports-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jasperreports7-plugin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-javatemplates-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jfreechart-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-json-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-junit-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plexus-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-rest-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-spring-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-testng-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-tiles-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-velocity-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-xslt-plugin</artifactId>
<version>${struts-version.version}</version>
<version>${project.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<scm>
<tag>STRUTS_7_0_2</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>
</scm>
</project>
+10 -2
View File
@@ -24,7 +24,8 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>struts2-core</artifactId>
<packaging>jar</packaging>
@@ -186,13 +187,19 @@
<artifactId>commons-text</artifactId>
</dependency>
<!-- Optional used in org.apache.struts2.util.ProxyUtil to detect if object is HibernateProxy -->
<dependency>
<!-- Optional used in org.apache.struts2.util.ProxyUtil to detect if object is HibernateProxy -->
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.6.15.Final</version>
<optional>true</optional>
</dependency>
<dependency>
<!-- Optional used in org.apache.struts2.util.ProxyUtil to detect if object is Spring proxy -->
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
@@ -229,6 +236,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>compile</scope>
<optional>true</optional>
</dependency>
@@ -18,8 +18,6 @@
*/
package org.apache.struts2;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
/**
* ModelDriven Actions provide a model object to be pushed onto the ValueStack
* in addition to the Action itself, allowing a FormBean type approach like Struts.
@@ -36,7 +34,6 @@ public interface ModelDriven<T> {
*
* @return the model
*/
@StrutsParameter(depth = Integer.MAX_VALUE)
T getModel();
}
@@ -673,4 +673,11 @@ 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";
}
@@ -18,18 +18,17 @@
*/
package org.apache.struts2.components;
import org.apache.commons.lang3.ClassUtils;
import org.apache.struts2.StrutsException;
import org.apache.struts2.dispatcher.PrepareOperations;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.reflection.ReflectionProvider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.struts2.StrutsException;
import org.apache.struts2.dispatcher.PrepareOperations;
import org.apache.struts2.views.annotations.StrutsTag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
@@ -94,9 +93,7 @@ public class Debug extends UIBean {
}
private void allowListClass(Object o) {
threadAllowlist.allowClass(o.getClass());
ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass);
ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass);
threadAllowlist.allowClassHierarchy(o.getClass());
}
@Override
@@ -18,12 +18,12 @@
*/
package org.apache.struts2.components;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.ValueStack;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.util.MakeIterator;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.views.annotations.StrutsTag;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
import org.apache.struts2.views.jsp.IteratorStatus;
@@ -307,7 +307,7 @@ public class IteratorComponent extends ContextBean {
stack.push(currentValue);
if (currentValue != null) {
threadAllowlist.allowClass(currentValue.getClass());
threadAllowlist.allowClassHierarchy(currentValue.getClass());
}
String var = getVar();
@@ -18,13 +18,12 @@
*/
package org.apache.struts2.components;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.views.annotations.StrutsTag;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* <!-- START SNIPPET: javadoc -->
* <p>Render an HTML input field of type text</p>
@@ -50,17 +49,16 @@ import jakarta.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 +93,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;
}
@@ -36,6 +36,7 @@ import org.apache.struts2.components.template.TemplateEngineManager;
import org.apache.struts2.components.template.TemplateRenderingContext;
import org.apache.struts2.dispatcher.AttributeMap;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.util.ComponentUtils;
import org.apache.struts2.util.TextProviderHelper;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
@@ -528,6 +529,8 @@ public abstract class UIBean extends Component {
protected TemplateEngineManager templateEngineManager;
protected CspNonceReader cspNonceReader;
@Inject(StrutsConstants.STRUTS_UI_TEMPLATEDIR)
public void setDefaultTemplateDir(String dir) {
this.defaultTemplateDir = dir;
@@ -553,6 +556,11 @@ public abstract class UIBean extends Component {
this.templateEngineManager = mgr;
}
@Inject
public void setCspNonceReader(CspNonceReader cspNonceReader) {
this.cspNonceReader = cspNonceReader;
}
@Override
public boolean end(Writer writer, String body) {
evaluateParams();
@@ -886,13 +894,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();
@@ -55,6 +55,7 @@ import org.apache.struts2.factory.UnknownHandlerFactory;
import org.apache.struts2.factory.ValidatorFactory;
import org.apache.struts2.inject.ContainerBuilder;
import org.apache.struts2.inject.Scope;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.ognl.BeanInfoCacheFactory;
import org.apache.struts2.ognl.ExpressionCacheFactory;
@@ -450,6 +451,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);
}
@@ -68,6 +68,8 @@ import org.apache.struts2.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()));
@@ -33,6 +33,7 @@ import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
@@ -116,6 +117,14 @@ public class DateConverter extends DefaultTypeConverter {
throw new TypeConversionException("Could not parse date", e);
}
} else if (OffsetDateTime.class == toType) {
DateTimeFormatter dtf = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
try {
return OffsetDateTime.parse(sa, dtf);
} catch (DateTimeParseException e) {
throw new TypeConversionException("Could not parse OffsetDateTime", e);
}
}
// final fallback for dates without time
@@ -25,7 +25,10 @@ import org.apache.struts2.StrutsConstants;
import org.apache.struts2.conversion.TypeConversionException;
import java.lang.reflect.Member;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
@@ -100,8 +103,14 @@ public class XWorkBasicConverter extends DefaultTypeConverter {
result = doConvertToArray(context, o, member, propertyName, value, toType);
} else if (Date.class.isAssignableFrom(toType)) {
result = doConvertToDate(context, value, toType);
} else if (LocalDate.class.isAssignableFrom(toType)) {
result = doConvertToDate(context, value, toType);
} else if (LocalDateTime.class.isAssignableFrom(toType)) {
result = doConvertToDate(context, value, toType);
} else if (LocalTime.class.isAssignableFrom(toType)) {
result = doConvertToDate(context, value, toType);
} else if (OffsetDateTime.class.isAssignableFrom(toType)) {
result = doConvertToDate(context, value, toType);
} else if (Calendar.class.isAssignableFrom(toType)) {
result = doConvertToCalendar(context, value);
} else if (Collection.class.isAssignableFrom(toType)) {
@@ -19,22 +19,23 @@
package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadContentTypeException;
import org.apache.commons.fileupload2.core.FileUploadException;
import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadSizeException;
import org.apache.commons.fileupload2.core.RequestContext;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.io.FilenameUtils;
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.dispatcher.LocalizedMessage;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.security.DefaultExcludedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@@ -44,6 +45,9 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
/**
* Abstract class with some helper methods, it should be used
@@ -55,9 +59,6 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
private static final Logger LOG = LogManager.getLogger(AbstractMultiPartRequest.class);
private static final String EXCLUDED_FILE_PATTERN = "^(.*[<>&\"'|;\\\\/?*:]+.*|.*\\.\\..*)$";
private static final String EXCLUDED_FILE_PATTERN_WITH_DMI_SUPPORT = "^(?!action:[^<>&\"'|;\\\\/?*:]+(![^<>&\"'|;\\\\/?*:]+)?$)(.*[<>&\"'|;\\\\/?*:]+.*|.*\\.\\..*)$\n";
/**
* Defines the internal buffer size used during streaming operations.
*/
@@ -113,18 +114,6 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
*/
protected Map<String, List<String>> parameters = new HashMap<>();
private final ExcludedPatternsChecker patternsChecker;
protected AbstractMultiPartRequest(String dmiValue) {
patternsChecker = new DefaultExcludedPatternsChecker();
if (BooleanUtils.toBoolean(dmiValue)) {
((DefaultExcludedPatternsChecker) patternsChecker).setAdditionalExcludePatterns(EXCLUDED_FILE_PATTERN_WITH_DMI_SUPPORT);
} else {
((DefaultExcludedPatternsChecker) patternsChecker).setAdditionalExcludePatterns(EXCLUDED_FILE_PATTERN);
}
}
/**
* @param bufferSize Sets the buffer size to be used.
*/
@@ -204,7 +193,21 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
* @param charset used charset from incoming request
* @param saveDir a temporary folder to store uploaded files (not always needed)
*/
protected abstract JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir);
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir) {
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
LOG.debug("Using file save directory: {}", saveDir);
builder.setPath(saveDir);
LOG.debug("Sets buffer size: {}", bufferSize);
builder.setBufferSize(bufferSize);
LOG.debug("Using charset: {}", charset);
builder.setCharset(charset);
DiskFileItemFactory factory = builder.get();
return new JakartaServletDiskFileUpload(factory);
}
protected JakartaServletDiskFileUpload prepareServletFileUpload(Charset charset, Path saveDir) {
JakartaServletDiskFileUpload servletFileUpload = createJakartaFileUpload(charset, saveDir);
@@ -224,11 +227,15 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
return servletFileUpload;
}
protected RequestContext createRequestContext(HttpServletRequest request) {
return new StrutsRequestContext(request);
}
protected boolean exceedsMaxStringLength(String fieldName, String fieldValue) {
if (maxStringLength != null && fieldValue.length() > maxStringLength) {
if (LOG.isDebugEnabled()) {
LOG.debug("Form field: {} of size: {} bytes exceeds limit of: {}.",
sanitizeNewlines(fieldName), fieldValue.length(), maxStringLength);
normalizeSpace(fieldName), fieldValue.length(), maxStringLength);
}
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(),
STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY, null,
@@ -251,7 +258,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
try {
processUpload(request, saveDir);
} catch (FileUploadException e) {
LOG.debug("Error parsing the multi-part request!", e);
LOG.warn("Error parsing the multi-part request!", e);
Class<? extends Throwable> exClass = FileUploadException.class;
Object[] args = new Object[]{};
@@ -274,7 +281,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
errors.add(errorMessage);
}
} catch (IOException e) {
LOG.debug("Unable to parse request", e);
LOG.warn("Unable to parse request", e);
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(), new Object[]{});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
@@ -302,25 +309,9 @@ 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);
}
/**
* @deprecated since 7.0.1, use {@link StringUtils#normalizeSpace(String)} instead
*/
@Deprecated
protected String sanitizeNewlines(String before) {
return before.replaceAll("\\R", "_");
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#getErrors()
@@ -410,6 +401,61 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
return values.toArray(new String[0]);
}
/**
* Creates a secure temporary file in the specified directory using UUID-based naming.
* This method ensures files are created in a controlled location rather than the
* system temporary directory, reducing security risks.
*
* @param fileName the original filename for logging purposes
* @param location the directory where the temporary file should be created
* @return a new temporary file in the specified location
*/
protected File createTemporaryFile(String fileName, Path location) {
String uid = UUID.randomUUID().toString().replace("-", "_");
File file = location.resolve("upload_" + uid + ".tmp").toFile();
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
return file;
}
/**
* Validates that an uploaded file is not empty (0 bytes) and adds an error if it is.
*
* <p>Empty file uploads are rejected as they are not considered valid uploads.
* This validation ensures consistent behavior across all multipart implementations
* and provides proper user feedback when empty files are uploaded.</p>
*
* <p>When an empty file is detected:</p>
* <ul>
* <li>A debug log message is written with field name and filename</li>
* <li>A localized error message is created and added to the errors list</li>
* <li>The method returns true to indicate the file should be rejected</li>
* </ul>
*
* @param fileSize the size of the uploaded file in bytes
* @param fileName the original filename of the uploaded file
* @param fieldName the form field name containing the file upload
* @return true if the file is empty and should be rejected, false otherwise
* @see #buildErrorMessage(Class, String, Object[])
*/
protected boolean rejectEmptyFile(long fileSize, String fileName, String fieldName) {
if (fileSize == 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("Rejecting empty file upload for field: {} with filename: {}",
normalizeSpace(fieldName), normalizeSpace(fileName));
}
LocalizedMessage errorMessage = buildErrorMessage(
IllegalArgumentException.class,
"Empty files are not allowed",
new Object[]{fileName, fieldName}
);
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
return true;
}
return false;
}
/* (non-Javadoc)
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
*/
@@ -435,12 +481,4 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
}
}
/**
* @param fileName file name to check
* @return true if the file name is excluded
*/
protected boolean isExcluded(String fileName) {
return patternsChecker.isExcluded(fileName).isExcluded();
}
}
@@ -20,16 +20,17 @@ package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.core.RequestContext;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
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.inject.Inject;
import org.apache.struts2.dispatcher.LocalizedMessage;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -38,20 +39,73 @@ import static org.apache.commons.lang3.StringUtils.normalizeSpace;
/**
* Multipart form data request adapter for Jakarta Commons FileUpload package.
*
* <p>This implementation provides secure handling of multipart requests with proper
* resource management and cleanup. It tracks all temporary files created during
* the upload process and ensures they are properly cleaned up to prevent
* resource leaks and security vulnerabilities.</p>
*
* <p>Key features:</p>
* <ul>
* <li>Automatic tracking and cleanup of temporary files</li>
* <li>Proper error handling with user-friendly error messages</li>
* <li>Support for both in-memory and disk-based file uploads</li>
* <li>Extensible cleanup mechanisms for customization</li>
* </ul>
*
* <p>Usage example:</p>
* <pre>
* JakartaMultiPartRequest multipartRequest = new JakartaMultiPartRequest();
* try {
* multipartRequest.parse(request, "/tmp/uploads");
* // Process uploaded files
* for (String fieldName : multipartRequest.getFileParameterNames()) {
* List&lt;UploadedFile&gt; files = multipartRequest.getFile(fieldName);
* // Handle files
* }
* } finally {
* multipartRequest.cleanUp(); // Always clean up resources
* }
* </pre>
*
* @see AbstractMultiPartRequest
* @see org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload
*/
public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
private static final Logger LOG = LogManager.getLogger(JakartaMultiPartRequest.class);
public JakartaMultiPartRequest() {
super(Boolean.FALSE.toString());
}
@Inject(value = StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, required = false)
public JakartaMultiPartRequest(String dmiValue) {
super(dmiValue);
}
/**
* List to track all DiskFileItem instances for proper cleanup
*/
private final List<DiskFileItem> diskFileItems = new ArrayList<>();
/**
* List to track temporary files created for in-memory uploads
*/
private final List<File> temporaryFiles = new ArrayList<>();
/**
* Processes the multipart upload request using Jakarta Commons FileUpload.
*
* <p>This method handles the core upload processing by:</p>
* <ol>
* <li>Reading the character encoding from the request</li>
* <li>Preparing the Jakarta servlet file upload handler</li>
* <li>Creating a request context for processing</li>
* <li>Iterating through all form items (fields and files)</li>
* <li>Processing each item appropriately based on its type</li>
* </ol>
*
* <p>All {@link org.apache.commons.fileupload2.core.DiskFileItem} instances
* are automatically tracked for proper cleanup.</p>
*
* @param request the HTTP servlet request containing the multipart data
* @param saveDir the directory where uploaded files will be stored
* @throws IOException if an error occurs during upload processing
* @see #processNormalFormField(DiskFileItem, Charset)
* @see #processFileField(DiskFileItem, String)
*/
@Override
protected void processUpload(HttpServletRequest request, String saveDir) throws IOException {
Charset charset = readCharsetEncoding(request);
@@ -59,49 +113,54 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
JakartaServletDiskFileUpload servletFileUpload =
prepareServletFileUpload(charset, Path.of(saveDir));
for (DiskFileItem item : servletFileUpload.parseRequest(request)) {
RequestContext requestContext = createRequestContext(request);
for (DiskFileItem item : servletFileUpload.parseRequest(requestContext)) {
// Track all DiskFileItem instances for cleanup - this is critical for security
// as it ensures temporary files are properly cleaned up even if processing fails
diskFileItems.add(item);
LOG.debug(() -> "Processing a form field: " + normalizeSpace(item.getFieldName()));
if (item.isFormField()) {
// Process regular form fields (text inputs, checkboxes, etc.)
processNormalFormField(item, charset);
} else {
// Process file upload fields
LOG.debug(() -> "Processing a file: " + normalizeSpace(item.getFieldName()));
processFileField(item);
processFileField(item, saveDir);
}
}
}
@Override
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path saveDir) {
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
LOG.debug("Using file save directory: {}", saveDir);
builder.setPath(saveDir);
LOG.debug("Sets minimal buffer size to always write file to disk");
builder.setBufferSize(1);
LOG.debug("Using charset: {}", charset);
builder.setCharset(charset);
DiskFileItemFactory factory = builder.get();
return new JakartaServletDiskFileUpload(factory);
}
/**
* Processes a normal form field (non-file) from the multipart request.
*
* <p>This method handles text form fields by:</p>
* <ol>
* <li>Validating the field name is not null</li>
* <li>Extracting the field value using the specified charset</li>
* <li>Checking if the field value exceeds maximum string length</li>
* <li>Adding the value to the parameters map</li>
* </ol>
*
* <p>Fields with null names are skipped with a warning log message.</p>
* <p>Empty form fields are stored as empty strings.</p>
*
* @param item the disk file item representing the form field
* @param charset the character set to use for decoding the field value
* @throws IOException if an error occurs reading the field value
* @see #exceedsMaxStringLength(String, String)
*/
protected void processNormalFormField(DiskFileItem item, Charset charset) throws IOException {
LOG.debug("Item: {} is a normal form field", item.getName());
LOG.debug("Item: {} is a normal form field", normalizeSpace(item.getName()));
if (isExcluded(item.getFieldName())) {
LOG.warn(() -> "Form field [%s] is rejected!".formatted(normalizeSpace(item.getFieldName())));
String fieldName = item.getFieldName();
if (fieldName == null) {
LOG.warn("Form field has null fieldName, skipping");
return;
}
List<String> values;
String fieldName = item.getFieldName();
if (parameters.get(fieldName) != null) {
values = parameters.get(fieldName);
} else {
values = new ArrayList<>();
}
List<String> values = parameters.computeIfAbsent(fieldName, k -> new ArrayList<>());
String fieldValue = item.getString(charset);
if (exceedsMaxStringLength(fieldName, fieldValue)) {
@@ -115,32 +174,81 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
parameters.put(fieldName, values);
}
protected void processFileField(DiskFileItem item) {
if (isExcluded(item.getName())) {
LOG.warn(() -> "File name [%s] is not accepted".formatted(normalizeSpace(item.getName())));
return;
}
if (isExcluded(item.getFieldName())) {
LOG.warn(() -> "Field name [%s] is not accepted".formatted(normalizeSpace(item.getFieldName())));
return;
}
/**
* Processes a file field from the multipart request.
*
* <p>This method handles file uploads by:</p>
* <ol>
* <li>Validating the file name and field name are not null/empty</li>
* <li>Determining if the file is stored in memory or on disk</li>
* <li>For in-memory files: creating a temporary file and copying content</li>
* <li>For disk files: using the existing file directly</li>
* <li>Creating an {@link UploadedFile} abstraction</li>
* <li>Adding the file to the uploaded files collection</li>
* </ol>
*
* <p>Temporary files created for in-memory uploads are automatically
* tracked for cleanup. Any errors during temporary file creation are
* logged and added to the error list for user feedback.</p>
*
* @param item the disk file item representing the uploaded file
* @see #cleanUpTemporaryFiles()
*/
protected void processFileField(DiskFileItem item, String saveDir) {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (item.getName() == null || item.getName().trim().isEmpty()) {
LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(item.getFieldName()));
return;
}
List<UploadedFile> values;
if (uploadedFiles.get(item.getFieldName()) != null) {
values = uploadedFiles.get(item.getFieldName());
} else {
values = new ArrayList<>();
String fieldName = item.getFieldName();
if (fieldName == null) {
LOG.warn("File field has null fieldName, skipping");
return;
}
// Reject empty files (0 bytes) as they are not considered valid uploads
if (rejectEmptyFile(item.getSize(), item.getName(), fieldName)) {
return;
}
List<UploadedFile> values = uploadedFiles.computeIfAbsent(fieldName, k -> new ArrayList<>());
if (item.isInMemory()) {
LOG.warn(() -> "Storing uploaded files just in memory isn't supported currently, skipping file: %s!".formatted(normalizeSpace(item.getName())));
LOG.debug(() -> "Creating temporary file representing in-memory uploaded item: " + normalizeSpace(item.getFieldName()));
try {
File tempFile = createTemporaryFile(item.getName(), Path.of(saveDir));
// Track the temporary file for explicit cleanup
temporaryFiles.add(tempFile);
// Write the in-memory content to the temporary file
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(tempFile)) {
fos.write(item.get());
}
UploadedFile uploadedFile = StrutsUploadedFile.Builder
.create(tempFile)
.withOriginalName(item.getName())
.withContentType(item.getContentType())
.withInputName(item.getFieldName())
.build();
values.add(uploadedFile);
if (LOG.isDebugEnabled()) {
LOG.debug("Created temporary file for in-memory uploaded item: {} at {}",
normalizeSpace(item.getName()), tempFile.getAbsolutePath());
}
} catch (IOException e) {
LOG.warn("Failed to create temporary file for in-memory uploaded item: {}",
normalizeSpace(item.getName()), e);
// Add the error to the errors list for proper user feedback
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(), new Object[]{item.getName()});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
} else {
UploadedFile uploadedFile = StrutsUploadedFile.Builder
.create(item.getPath().toFile())
@@ -151,7 +259,118 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
values.add(uploadedFile);
}
uploadedFiles.put(item.getFieldName(), values);
uploadedFiles.put(fieldName, values);
}
/**
* Cleans up disk file items by deleting associated temporary files.
*
* <p>This method iterates through all tracked {@link DiskFileItem} instances
* and performs cleanup operations:</p>
* <ul>
* <li>For in-memory items: logs cleanup (no files to delete)</li>
* <li>For disk items: deletes the associated temporary file</li>
* </ul>
*
* <p>This method is called automatically during {@link #cleanUp()} but can
* be overridden by subclasses to customize cleanup behavior. All exceptions
* are caught and logged to prevent cleanup failures from affecting the
* overall cleanup process.</p>
*
* @see #cleanUp()
* @see #cleanUpTemporaryFiles()
*/
protected void cleanUpDiskFileItems() {
LOG.debug("Clean up all DiskFileItem instances (both form fields and file uploads");
for (DiskFileItem item : diskFileItems) {
try {
if (item.isInMemory()) {
LOG.debug(() -> "Cleaning up in-memory item: " + normalizeSpace(item.getFieldName()));
} else {
Path itemPath = item.getPath();
if (LOG.isDebugEnabled()) {
LOG.debug("Cleaning up disk item: {} at {}", normalizeSpace(item.getFieldName()), itemPath);
}
if (!Files.deleteIfExists(itemPath)) {
LOG.warn("There was a problem attempting to delete uploaded file: {}", itemPath);
}
}
} catch (Exception e) {
LOG.warn("Error cleaning up DiskFileItem: {}", normalizeSpace(item.getFieldName()), e);
}
}
}
/**
* Cleans up temporary files created for in-memory uploads.
*
* <p>This method deletes all temporary files that were created when
* processing in-memory uploads. These files are created in
* {@link #processFileField(DiskFileItem, String)} when an uploaded file is
* stored in memory and needs to be written to disk.</p>
*
* <p>The cleanup process:</p>
* <ol>
* <li>Iterates through all tracked temporary files</li>
* <li>Checks if each file still exists</li>
* <li>Attempts to delete existing files</li>
* <li>Logs warnings for files that cannot be deleted</li>
* </ol>
*
* <p>This method can be overridden by subclasses to customize cleanup
* behavior. All exceptions are caught and logged to ensure cleanup
* continues even if individual file deletions fail.</p>
*
* @see #cleanUp()
* @see #cleanUpDiskFileItems()
*/
protected void cleanUpTemporaryFiles() {
LOG.debug("Cleaning up {} temporary files created for in-memory uploads", temporaryFiles.size());
for (File tempFile : temporaryFiles) {
try {
if (!Files.deleteIfExists(tempFile.toPath())) {
LOG.warn("There was a problem attempting to delete temporary file: {}", tempFile.getAbsolutePath());
}
} catch (Exception e) {
LOG.warn("Error cleaning up temporary file: {}", tempFile.getAbsolutePath(), e);
}
}
}
/**
* Performs complete cleanup of all resources associated with this request.
*
* <p>This method extends the parent cleanup functionality to ensure proper
* cleanup of Jakarta-specific resources:</p>
* <ol>
* <li>Calls parent cleanup to handle base class resources</li>
* <li>Cleans up all tracked {@link DiskFileItem} instances</li>
* <li>Cleans up all temporary files created for in-memory uploads</li>
* <li>Clears internal tracking collections</li>
* </ol>
*
* <p>This method is designed to be safe to call multiple times and will
* not throw exceptions even if cleanup operations fail. All errors are
* logged for debugging purposes.</p>
*
* <p><strong>Important:</strong> This method should always be called in a
* finally block to ensure resources are properly released, even if
* exceptions occur during request processing.</p>
*
* @see #cleanUpDiskFileItems()
* @see #cleanUpTemporaryFiles()
* @see AbstractMultiPartRequest#cleanUp()
*/
@Override
public void cleanUp() {
super.cleanUp();
try {
cleanUpDiskFileItems();
cleanUpTemporaryFiles();
} finally {
diskFileItems.clear();
temporaryFiles.clear();
}
}
}
@@ -19,16 +19,13 @@
package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.core.DiskFileItemFactory;
import org.apache.commons.fileupload2.core.FileItemInput;
import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
import org.apache.commons.fileupload2.core.FileUploadSizeException;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.apache.struts2.inject.Inject;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
@@ -42,12 +39,11 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
/**
* Multi-part form data request adapter for Jakarta Commons FileUpload package that
* Multipart form data request adapter for Jakarta Commons FileUpload package that
* leverages the streaming API rather than the traditional non-streaming API.
* <p>
* For more details see WW-3025
@@ -58,15 +54,6 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
private static final Logger LOG = LogManager.getLogger(JakartaStreamMultiPartRequest.class);
public JakartaStreamMultiPartRequest() {
super(Boolean.FALSE.toString());
}
@Inject(value = StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, required = false)
public JakartaStreamMultiPartRequest(String dmiValue) {
super(dmiValue);
}
/**
* Processes the upload.
*
@@ -93,56 +80,62 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
});
}
protected JakartaServletDiskFileUpload createJakartaFileUpload(Charset charset, Path location) {
DiskFileItemFactory.Builder builder = DiskFileItemFactory.builder();
LOG.debug("Using file save directory: {}", location);
builder.setPath(location);
LOG.debug("Sets buffer size: {}", bufferSize);
builder.setBufferSize(bufferSize);
LOG.debug("Using charset: {}", charset);
builder.setCharset(charset);
DiskFileItemFactory factory = builder.get();
return new JakartaServletDiskFileUpload(factory);
}
/**
* Reads the entire contents of an input stream into a string.
*
* <p>This method uses a buffered approach to efficiently read the stream
* content without loading the entire stream into memory at once. It uses
* try-with-resources to ensure proper cleanup of resources.</p>
*
* @param inputStream the input stream to read from
* @return the stream contents as a UTF-8 string
* @throws IOException if an error occurs reading the stream
*/
private String readStream(InputStream inputStream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
for (int length; (length = inputStream.read(buffer)) != -1; ) {
result.write(buffer, 0, length);
// Use try-with-resources to ensure ByteArrayOutputStream is properly closed
try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024]; // 1KB buffer for efficient reading
// Read the stream in chunks to avoid loading everything into memory at once
for (int length; (length = inputStream.read(buffer)) != -1; ) {
result.write(buffer, 0, length);
}
// Convert to string using UTF-8 encoding
return result.toString(StandardCharsets.UTF_8);
}
return result.toString(StandardCharsets.UTF_8);
}
/**
* Processes the FileItem as a normal form field.
*
* @param fileItemInput a form field item input
* Processes a normal form field (non-file) from the multipart request using streaming API.
*
* <p>This method handles text form fields by:</p>
* <ol>
* <li>Validating the field name is not null</li>
* <li>Reading the field value from the input stream</li>
* <li>Checking if the field value exceeds maximum string length</li>
* <li>Adding the value to the parameters collection</li>
* </ol>
*
* <p>Fields with null names are skipped with a warning log message.</p>
* <p>The streaming approach is more memory-efficient for large form data.</p>
*
* @param fileItemInput a form field item input from the streaming API
* @throws IOException if an error occurs reading the input stream
* @see #readStream(InputStream)
* @see #exceedsMaxStringLength(String, String)
*/
protected void processFileItemAsFormField(FileItemInput fileItemInput) throws IOException {
String fieldName = fileItemInput.getFieldName();
String fieldValue = readStream(fileItemInput.getInputStream());
if (isExcluded(fieldName)) {
LOG.warn(() -> "Form field [%s] is rejected!".formatted(normalizeSpace(fieldName)));
if (fieldName == null) {
LOG.warn("Form field has null fieldName, skipping");
return;
}
String fieldValue = readStream(fileItemInput.getInputStream());
if (exceedsMaxStringLength(fieldName, fieldValue)) {
return;
}
List<String> values;
if (parameters.containsKey(fieldName)) {
values = parameters.get(fieldName);
} else {
values = new ArrayList<>();
parameters.put(fieldName, values);
}
List<String> values = parameters.computeIfAbsent(fieldName, k -> new ArrayList<>());
values.add(fieldValue);
}
@@ -197,10 +190,28 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
/**
* Processes the FileItem as a file field.
*
* @param fileItemInput file item representing upload file
* @param location location
* Processes a file field from the multipart request using streaming API.
*
* <p>This method handles file uploads by:</p>
* <ol>
* <li>Validating the file name and field name are not null/empty</li>
* <li>Checking if the upload exceeds maximum file count</li>
* <li>Creating a temporary file in the specified location</li>
* <li>Streaming the file content directly to disk</li>
* <li>Checking if the total size exceeds maximum allowed size</li>
* <li>Creating an {@link UploadedFile} abstraction or cleaning up on size exceeded</li>
* </ol>
*
* <p>Files with null names or field names are skipped with appropriate logging.</p>
* <p>The streaming approach is more memory-efficient for large file uploads
* as it writes directly to disk rather than loading into memory first.</p>
*
* @param fileItemInput file item representing upload file from streaming API
* @param location the directory where temporary files will be created
* @throws IOException if an error occurs during file processing
* @see #createTemporaryFile(String, Path)
* @see #streamFileToDisk(FileItemInput, File)
* @see #createUploadedFile(FileItemInput, File)
*/
protected void processFileItemAsFileField(FileItemInput fileItemInput, Path location) throws IOException {
// Skip file uploads that don't have a file name - meaning that no file was selected.
@@ -208,9 +219,10 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(fileItemInput.getFieldName()));
return;
}
if (isExcluded(fileItemInput.getName())) {
LOG.warn(() -> "File field [%s] rejected".formatted(normalizeSpace(fileItemInput.getName())));
// Skip file uploads that don't have a field name
if (fileItemInput.getFieldName() == null) {
LOG.warn("File upload has null fieldName, skipping");
return;
}
@@ -220,6 +232,15 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
File file = createTemporaryFile(fileItemInput.getName(), location);
streamFileToDisk(fileItemInput, file);
// Reject empty files (0 bytes) as they are not considered valid uploads
if (rejectEmptyFile(file.length(), fileItemInput.getName(), fileItemInput.getFieldName())) {
// Clean up the empty temporary file
if (!Files.deleteIfExists(file.toPath())) {
LOG.warn("Failed to delete empty temporary file: {}", file.getAbsolutePath());
}
return;
}
Long currentFilesSize = maxSizeOfFiles != null ? actualSizeOfUploadedFiles() : null;
if (maxSizeOfFiles != null && currentFilesSize + file.length() >= maxSizeOfFiles) {
@@ -229,20 +250,6 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
}
/**
* Creates a temporary file based on the given filename and location.
*
* @param fileName file name
* @param location location
* @return a temporary file based on the given filename and location
*/
protected File createTemporaryFile(String fileName, Path location) {
String uid = UUID.randomUUID().toString().replace("-", "_");
File file = location.resolve("upload_" + uid + ".tmp").toFile();
LOG.debug("Creating temporary file: {} (originally: {})", file.getName(), fileName);
return file;
}
/**
* Streams the file upload stream to the specified file.
*
@@ -253,9 +260,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
InputStream input = fileItemInput.getInputStream();
try (OutputStream output = new BufferedOutputStream(Files.newOutputStream(file.toPath()), bufferSize)) {
byte[] buffer = new byte[bufferSize];
if (LOG.isDebugEnabled()) {
LOG.debug("Streaming file: {} using buffer size: {}", normalizeSpace(fileItemInput.getName()), bufferSize);
}
LOG.debug("Streaming file: {} using buffer size: {}", normalizeSpace(fileItemInput.getName()), bufferSize);
for (int length; ((length = input.read(buffer)) > 0); ) {
output.write(buffer, 0, length);
}
@@ -263,29 +268,40 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
/**
* Create {@link UploadedFile} abstraction over uploaded file
*
* @param fileItemInput file item stream
* @param file the file
* Creates an {@link UploadedFile} abstraction over an uploaded file.
*
* <p>This method creates a wrapper around the uploaded file that provides
* a consistent interface for accessing file information and content.
* The created {@link UploadedFile} instance contains:</p>
* <ul>
* <li>The original filename as provided by the client</li>
* <li>The content type (MIME type) if available</li>
* <li>The form field name that contained the file</li>
* <li>A reference to the temporary file on disk</li>
* </ul>
*
* <p>The file is automatically added to the uploaded files collection,
* grouped by field name to support multiple file uploads per field.</p>
*
* @param fileItemInput file item stream containing file metadata
* @param file the temporary file containing the uploaded content
* @see UploadedFile
* @see StrutsUploadedFile
*/
protected void createUploadedFile(FileItemInput fileItemInput, File file) {
String fileName = fileItemInput.getName();
String fieldName = fileItemInput.getFieldName();
// fieldName null check already done in processFileItemAsFileField
UploadedFile uploadedFile = StrutsUploadedFile.Builder
.create(file)
.withOriginalName(fileName)
.withContentType(fileItemInput.getContentType())
.withInputName(fileItemInput.getFieldName())
.withInputName(fieldName)
.build();
if (uploadedFiles.containsKey(fieldName)) {
uploadedFiles.get(fieldName).add(uploadedFile);
} else {
List<UploadedFile> infos = new ArrayList<>();
infos.add(uploadedFile);
uploadedFiles.put(fieldName, infos);
}
List<UploadedFile> infos = uploadedFiles.computeIfAbsent(fieldName, key -> new ArrayList<>());
infos.add(uploadedFile);
}
}
@@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher.multipart;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletRequestContext;
/**
* Provides a specialized request context for Struts applications,
* extending the Jakarta Servlet request context to add custom handling
* for multipart-related requests.
* <p>
* This class overrides multipart detection logic to ensure that requests
* without a content type are not treated as multipart, improving robustness
* in file upload scenarios.
*/
public class StrutsRequestContext extends JakartaServletRequestContext {
/**
* Constructs a context for this request.
*
* @param request The request to which this context applies.
*/
public StrutsRequestContext(HttpServletRequest request) {
super(request);
}
/**
* Determines if the current request is multipart-related.
* <p>
* This implementation first checks if the request's content type is set.
* If the content type is {@code null}, it returns {@code false} immediately.
* Otherwise, it delegates to the superclass implementation to perform
* further checks.
*
* @return {@code true} if the request is multipart-related; {@code false} otherwise.
*/
@Override
public boolean isMultipartRelated() {
if (this.getRequest().getContentType() == null) {
return false;
}
return super.isMultipartRelated();
}
}
@@ -18,11 +18,11 @@
*/
package org.apache.struts2.factory;
import org.apache.struts2.conversion.TypeConverter;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ObjectFactory;
import org.apache.struts2.conversion.TypeConverter;
import org.apache.struts2.inject.Inject;
import java.util.Map;
@@ -30,19 +30,18 @@ import java.util.Map;
* Default implementation
*/
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;
}
@Override
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);
}
}
@@ -115,7 +115,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);
@@ -20,11 +20,11 @@ package org.apache.struts2.interceptor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.result.ActionChainResult;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.Unchainable;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.result.ActionChainResult;
import org.apache.struts2.result.Result;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.ProxyUtil;
@@ -167,17 +167,18 @@ public class ChainingInterceptor extends AbstractInterceptor {
}
private void copyStack(ActionInvocation invocation, CompoundRoot root) {
List list = prepareList(root);
List<Object> list = prepareList(root);
Map<String, Object> ctxMap = invocation.getInvocationContext().getContextMap();
for (Object object : list) {
if (shouldCopy(object)) {
Object action = invocation.getAction();
Class<?> editable = null;
if(ProxyUtil.isProxy(action)) {
editable = ProxyUtil.ultimateTargetClass(action);
}
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
if (!shouldCopy(object)) {
continue;
}
Object action = invocation.getAction();
Class<?> editable = null;
if (ProxyUtil.isProxy(action)) {
editable = ProxyUtil.ultimateTargetClass(action);
}
reflectionProvider.copy(object, action, ctxMap, prepareExcludes(), includes, editable);
}
}
@@ -204,9 +205,8 @@ public class ChainingInterceptor extends AbstractInterceptor {
return o != null && !(o instanceof Unchainable);
}
@SuppressWarnings("unchecked")
private List prepareList(CompoundRoot root) {
List list = new ArrayList(root);
private List<Object> prepareList(CompoundRoot root) {
var list = new ArrayList<>(root);
list.remove(0);
Collections.reverse(list);
return list;
@@ -215,9 +215,9 @@ public class ExceptionMappingInterceptor extends AbstractInterceptor {
HttpParameters parameters = HttpParameters.create(mappingParams).build();
invocation.getInvocationContext().withParameters(parameters);
result = mappingConfig.getResult();
ExceptionHolder holder = new ExceptionHolder(e);
threadAllowlist.allowClass(holder.getClass());
threadAllowlist.allowClass(e.getClass());
var holder = new ExceptionHolder(e);
threadAllowlist.allowClassHierarchy(ExceptionHolder.class);
threadAllowlist.allowClassHierarchy(e.getClass());
publishException(invocation, holder);
} else {
throw e;
@@ -20,6 +20,8 @@ package org.apache.struts2.interceptor;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ModelDriven;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.ValueStack;
@@ -79,20 +81,27 @@ import org.apache.struts2.util.ValueStack;
public class ModelDrivenInterceptor extends AbstractInterceptor {
protected boolean refreshModelBeforeResult = false;
private ThreadAllowlist threadAllowlist;
public void setRefreshModelBeforeResult(boolean val) {
this.refreshModelBeforeResult = val;
}
@Inject
public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
this.threadAllowlist = threadAllowlist;
}
@Override
public String intercept(ActionInvocation invocation) throws Exception {
Object action = invocation.getAction();
if (action instanceof ModelDriven modelDriven) {
if (action instanceof ModelDriven<?> modelDriven) {
ValueStack stack = invocation.getStack();
Object model = modelDriven.getModel();
if (model != null) {
stack.push(model);
if (model != null) {
stack.push(model);
threadAllowlist.allowClassHierarchy(model.getClass());
}
if (refreshModelBeforeResult) {
invocation.addPreResultListener(new RefreshModelBeforeResult(modelDriven, model));
@@ -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 +
'}';
}
}
}
@@ -16,13 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher.multipart;
public class JakartaMultiPartRequestWithDMITest extends AbstractMultiPartRequestWithDMITest {
@Override
protected AbstractMultiPartRequest createMultipartRequest() {
return new JakartaMultiPartRequest(Boolean.TRUE.toString());
}
package org.apache.struts2.interceptor.csp;
/**
* Source of the nonce value
*/
public enum CspNonceSource {
REQUEST,
SESSION
}
@@ -44,6 +44,11 @@ public interface CspSettings {
String HTTPS = "https:";
String CSP_REPORT_TYPE = "application/csp-report";
/**
* Adds CSP related headers to response based on request state (e.g., if session has been created)
*
* @since Struts 6.0.3
*/
void addCspHeaders(HttpServletRequest request, HttpServletResponse response);
/**
@@ -20,13 +20,15 @@ package org.apache.struts2.interceptor.csp;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.struts2.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 java.security.SecureRandom;
import java.util.Base64;
import java.util.Objects;
import static java.lang.String.format;
@@ -43,63 +45,86 @@ 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;
@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));
}
protected String getNonceString(HttpServletRequest request) {
Object nonce = request.getSession().getAttribute("nonce");
return Objects.toString(nonce);
return builder.toString();
}
private byte[] getRandomBytes() {
@@ -128,10 +153,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,86 @@
/*
* 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 jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
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.inject.Inject;
import org.apache.struts2.util.ValueStack;
/**
* 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());
}
}
@@ -18,8 +18,6 @@
*/
package org.apache.struts2.interceptor.debugging;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ClassUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionContext;
@@ -38,6 +36,7 @@ import org.apache.struts2.util.reflection.ReflectionProvider;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import org.apache.struts2.views.freemarker.FreemarkerResult;
import jakarta.servlet.http.HttpServletResponse;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
@@ -273,9 +272,7 @@ public class DebuggingInterceptor extends AbstractInterceptor {
private void allowListClass(Object o) {
if (o != null) {
threadAllowlist.allowClass(o.getClass());
ClassUtils.getAllSuperclasses(o.getClass()).forEach(threadAllowlist::allowClass);
ClassUtils.getAllInterfaces(o.getClass()).forEach(threadAllowlist::allowClass);
threadAllowlist.allowClassHierarchy(o.getClass());
}
}
@@ -19,7 +19,6 @@
package org.apache.struts2.interceptor.parameter;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionContext;
@@ -33,12 +32,14 @@ import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.interceptor.MethodFilterInterceptor;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.security.AcceptedPatternsChecker;
import org.apache.struts2.security.DefaultAcceptedPatternsChecker;
import org.apache.struts2.security.ExcludedPatternsChecker;
import org.apache.struts2.util.ClearableValueStack;
import org.apache.struts2.util.MemberAccessValueStack;
import org.apache.struts2.util.ProxyUtil;
import org.apache.struts2.util.TextParseUtil;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.ValueStackFactory;
@@ -46,7 +47,6 @@ import org.apache.struts2.util.reflection.ReflectionContextState;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
@@ -93,6 +93,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
protected boolean requireAnnotationsTransitionMode = false;
private ValueStackFactory valueStackFactory;
private OgnlUtil ognlUtil;
protected ThreadAllowlist threadAllowlist;
private ExcludedPatternsChecker excludedPatterns;
private AcceptedPatternsChecker acceptedPatterns;
@@ -104,6 +105,11 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
this.valueStackFactory = valueStackFactory;
}
@Inject
public void setOgnlUtil(OgnlUtil ognlUtil) {
this.ognlUtil = ognlUtil;
}
@Inject
public void setThreadAllowlist(ThreadAllowlist threadAllowlist) {
this.threadAllowlist = threadAllowlist;
@@ -351,9 +357,8 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
long paramDepth = name.codePoints().mapToObj(c -> (char) c).filter(NESTING_CHARS::contains).count();
if (action instanceof ModelDriven<?> && !ActionContext.getContext().getValueStack().peek().equals(action)) {
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement and OGNL allowlisting model type");
// (Exempted by annotation on org.apache.struts2.ModelDriven#getModel)
return hasValidAnnotatedMember("model", action, paramDepth + 1);
LOG.debug("Model driven Action detected, exempting from @StrutsParameter annotation requirement");
return true;
}
if (requireAnnotationsTransitionMode && paramDepth == 0) {
@@ -395,6 +400,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
}
protected boolean hasValidAnnotatedPropertyDescriptor(Object action, PropertyDescriptor propDesc, long paramDepth) {
Class<?> actionClass = ultimateClass(action);
Method relevantMethod = paramDepth == 0 ? propDesc.getWriteMethod() : propDesc.getReadMethod();
if (relevantMethod == null) {
return false;
@@ -412,7 +418,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
return false;
}
LOG.debug("Success: Matching annotated method [{}] found for property [{}] of depth [{}] on Action [{}]",
relevantMethod.getName(), propDesc.getName(), paramDepth, action.getClass().getSimpleName());
relevantMethod.getName(), propDesc.getName(), paramDepth, actionClass.getSimpleName());
if (paramDepth >= 1) {
allowlistClass(propDesc.getPropertyType());
}
@@ -439,36 +445,35 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
}
protected void allowlistParamType(Type paramType) {
if (paramType instanceof Class) {
allowlistClass((Class<?>) paramType);
if (paramType instanceof Class<?> clazz) {
allowlistClass(clazz);
}
}
protected void allowlistClass(Class<?> clazz) {
threadAllowlist.allowClass(clazz);
ClassUtils.getAllSuperclasses(clazz).forEach(threadAllowlist::allowClass);
ClassUtils.getAllInterfaces(clazz).forEach(threadAllowlist::allowClass);
threadAllowlist.allowClassHierarchy(clazz);
}
protected boolean hasValidAnnotatedField(Object action, String fieldName, long paramDepth) {
Class<?> actionClass = ultimateClass(action);
LOG.debug("No matching annotated method found for property [{}] of depth [{}] on Action [{}], now also checking for public field",
fieldName, paramDepth, action.getClass().getSimpleName());
fieldName, paramDepth, actionClass.getSimpleName());
Field field;
try {
field = action.getClass().getDeclaredField(fieldName);
field = actionClass.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
LOG.debug("Matching field for property [{}] not found on Action [{}]", fieldName, action.getClass().getSimpleName());
LOG.debug("Matching field for property [{}] not found on Action [{}]", fieldName, actionClass.getSimpleName());
return false;
}
if (!Modifier.isPublic(field.getModifiers())) {
LOG.debug("Matching field [{}] is not public on Action [{}]", field.getName(), action.getClass().getSimpleName());
LOG.debug("Matching field [{}] is not public on Action [{}]", field.getName(), actionClass.getSimpleName());
return false;
}
if (getPermittedInjectionDepth(field) < paramDepth) {
String logMessage = format(
"Parameter injection for field [%s] on Action [%s] rejected. Ensure it is annotated with @StrutsParameter with an appropriate 'depth'.",
field.getName(),
action.getClass().getName());
actionClass.getName());
if (devMode) {
notifyDeveloperOfError(LOG, action, logMessage);
} else {
@@ -477,7 +482,7 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
return false;
}
LOG.debug("Success: Matching annotated public field [{}] found for property of depth [{}] on Action [{}]",
field.getName(), paramDepth, action.getClass().getSimpleName());
field.getName(), paramDepth, actionClass.getSimpleName());
if (paramDepth >= 1) {
allowlistClass(field.getType());
}
@@ -510,11 +515,19 @@ public class ParametersInterceptor extends MethodFilterInterceptor {
return element.getAnnotation(StrutsParameter.class);
}
protected Class<?> ultimateClass(Object action) {
if (ProxyUtil.isProxy(action)) {
return ProxyUtil.ultimateTargetClass(action);
}
return action.getClass();
}
protected BeanInfo getBeanInfo(Object action) {
Class<?> actionClass = ultimateClass(action);
try {
return Introspector.getBeanInfo(action.getClass());
return ognlUtil.getBeanInfo(actionClass);
} catch (IntrospectionException e) {
LOG.warn("Error introspecting Action {} for parameter injection validation", action.getClass(), e);
LOG.warn("Error introspecting Action {} for parameter injection validation", actionClass, e);
return null;
}
}
@@ -15,21 +15,37 @@
*/
package org.apache.struts2.ognl;
import java.util.function.Function;
import static java.util.Objects.requireNonNull;
/**
* A basic cache interface for use with OGNL processing (such as Expression, BeanInfo).
* All OGNL caches will have an eviction limit, but setting an extremely high value can
* simulate an "effectively unlimited" cache.
* A basic cache interface for use with OGNL processing (such as Expression, BeanInfo). Implementation must be
* thread-safe. All OGNL caches will have an eviction limit, but setting an extremely high value can simulate an
* "effectively unlimited" cache.
*
* @param <Key> The type for the cache key entries
* @param <Value> The type for the cache value entries
* @param <K> The type for the cache key entries
* @param <V> The type for the cache value entries
*/
public interface OgnlCache<Key, Value> {
public interface OgnlCache<K, V> {
Value get(Key key);
V get(K key);
void put(Key key, Value value);
/**
* @since 7.1
*/
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
requireNonNull(mappingFunction);
if (get(key) == null) {
putIfAbsent(key, mappingFunction.apply(key));
}
return get(key);
}
void putIfAbsent(Key key, Value value);
void put(K key, V value);
void putIfAbsent(K key, V value);
int size();
@@ -18,6 +18,8 @@ package org.apache.struts2.ognl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.function.Function;
/**
* <p>This OGNL Cache implementation is backed by {@link Caffeine} which uses the Window TinyLfu algorithm.</p>
*
@@ -46,6 +48,11 @@ public class OgnlCaffeineCache<K, V> implements OgnlCache<K, V> {
return cache.getIfPresent(key);
}
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
return cache.asMap().computeIfAbsent(key, mappingFunction);
}
@Override
public void put(K key, V value) {
cache.put(key, value);
@@ -17,6 +17,7 @@ package org.apache.struts2.ognl;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
* <p>Basic OGNL cache implementation.</p>
@@ -45,16 +46,21 @@ public class OgnlDefaultCache<K, V> implements OgnlCache<K, V> {
return ognlCache.get(key);
}
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
return ognlCache.computeIfAbsent(key, mappingFunction);
}
@Override
public void put(K key, V value) {
ognlCache.put(key, value);
this.clearIfEvictionLimitExceeded();
clearIfEvictionLimitExceeded();
}
@Override
public void putIfAbsent(K key, V value) {
ognlCache.putIfAbsent(key, value);
this.clearIfEvictionLimitExceeded();
clearIfEvictionLimitExceeded();
}
@Override
@@ -69,12 +75,12 @@ public class OgnlDefaultCache<K, V> implements OgnlCache<K, V> {
@Override
public int getEvictionLimit() {
return this.cacheEvictionLimit.get();
return cacheEvictionLimit.get();
}
@Override
public void setEvictionLimit(int cacheEvictionLimit) {
this.cacheEvictionLimit.set(cacheEvictionLimit);
public void setEvictionLimit(int newCacheEvictionLimit) {
cacheEvictionLimit.set(newCacheEvictionLimit);
}
/**
@@ -19,6 +19,7 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
/**
* <p>A basic OGNL LRU cache implementation.</p>
@@ -54,6 +55,11 @@ public class OgnlLRUCache<K, V> implements OgnlCache<K, V> {
return ognlLRUCache.get(key);
}
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
return ognlLRUCache.computeIfAbsent(key, mappingFunction);
}
@Override
public void put(K key, V value) {
ognlLRUCache.put(key, value);
@@ -23,6 +23,10 @@ import ognl.Ognl;
import java.util.Map;
/**
* @deprecated since 6.8.0, to be removed, see {@link ReflectionContextFactory}
*/
@Deprecated(since = "6.8.0", forRemoval = true)
public class OgnlReflectionContextFactory implements ReflectionContextFactory {
@Override
@@ -18,12 +18,6 @@
*/
package org.apache.struts2.ognl;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.reflection.ReflectionException;
import ognl.ClassResolver;
import ognl.Ognl;
import ognl.OgnlContext;
@@ -35,7 +29,12 @@ import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.ognl.OgnlGuard;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.reflection.ReflectionException;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
@@ -676,13 +675,19 @@ public class OgnlUtil {
* @throws IntrospectionException is thrown if an exception occurs during introspection.
*/
public BeanInfo getBeanInfo(Class<?> clazz) throws IntrospectionException {
synchronized (beanInfoCache) {
BeanInfo beanInfo = beanInfoCache.get(clazz);
if (beanInfo == null) {
beanInfo = Introspector.getBeanInfo(clazz, Object.class);
beanInfoCache.putIfAbsent(clazz, beanInfo);
try {
return beanInfoCache.computeIfAbsent(clazz, k -> {
try {
return Introspector.getBeanInfo(k, Object.class);
} catch (IntrospectionException e) {
throw new IllegalArgumentException(e);
}
});
} catch (IllegalArgumentException e) {
if (e.getCause() instanceof IntrospectionException innerEx) {
throw innerEx;
}
return beanInfo;
throw e;
}
}
@@ -18,13 +18,13 @@
*/
package org.apache.struts2.ognl;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.ProxyUtil;
import ognl.MemberAccess;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.util.ProxyUtil;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
@@ -38,6 +38,10 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import static java.text.MessageFormat.format;
import static java.util.Collections.emptySet;
import static org.apache.struts2.StrutsConstants.STRUTS_ALLOWLIST_CLASSES;
import static org.apache.struts2.StrutsConstants.STRUTS_ALLOWLIST_PACKAGE_NAMES;
import static org.apache.struts2.util.ConfigParseUtil.toClassObjectsSet;
import static org.apache.struts2.util.ConfigParseUtil.toClassesSet;
import static org.apache.struts2.util.ConfigParseUtil.toNewClassesSet;
@@ -45,10 +49,6 @@ import static org.apache.struts2.util.ConfigParseUtil.toNewPackageNamesSet;
import static org.apache.struts2.util.ConfigParseUtil.toNewPatternsSet;
import static org.apache.struts2.util.ConfigParseUtil.toPackageNamesSet;
import static org.apache.struts2.util.DebugUtils.logWarningForFirstOccurrence;
import static java.text.MessageFormat.format;
import static java.util.Collections.emptySet;
import static org.apache.struts2.StrutsConstants.STRUTS_ALLOWLIST_CLASSES;
import static org.apache.struts2.StrutsConstants.STRUTS_ALLOWLIST_PACKAGE_NAMES;
/**
* Allows access decisions to be made on the basis of whether a member is static or not.
@@ -141,6 +141,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)) {
@@ -209,16 +212,17 @@ public class SecurityMemberAccess implements MemberAccess {
return true;
}
if (!disallowProxyObjectAccess && target != null && 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
Class<?> targetClass = target != null ? target.getClass() : null;
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities and Spring proxies to their
// underlying classes/members. This allows the allowlist capability to continue working and still offer
// protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate
// entities. This is preferred to having to disable the allowlist capability entirely.
Object newTarget = ProxyUtil.getHibernateProxyTarget(target);
if (newTarget != target) {
logAllowlistHibernateEntity(target, newTarget);
target = newTarget;
member = ProxyUtil.resolveTargetMember(member, newTarget);
// entities and Spring proxies. This is preferred to having to disable the allowlist capability entirely.
Class<?> newTargetClass = ProxyUtil.ultimateTargetClass(target);
if (newTargetClass != targetClass) {
targetClass = newTargetClass;
member = ProxyUtil.resolveTargetMember(member, newTargetClass);
}
}
@@ -228,10 +232,10 @@ public class SecurityMemberAccess implements MemberAccess {
memberClass, member, STRUTS_ALLOWLIST_CLASSES, STRUTS_ALLOWLIST_PACKAGE_NAMES);
return false;
}
if (target == null || target.getClass() == memberClass) {
if (targetClass == null || targetClass == memberClass) {
return true;
}
Class<?> targetClass = target.getClass();
if (!isClassAllowlisted(targetClass)) {
LOG.warn("Target class [{}] of target [{}] is not allowlisted! Add to '{}' or '{}' configuration.",
targetClass, target, STRUTS_ALLOWLIST_CLASSES, STRUTS_ALLOWLIST_PACKAGE_NAMES);
@@ -240,20 +244,6 @@ public class SecurityMemberAccess implements MemberAccess {
return true;
}
private void logAllowlistHibernateEntity(Object original, Object resolved) {
if (!isDevMode && !LOG.isDebugEnabled()) {
return;
}
String msg = "Hibernate entity [{}] resolved to [{}] for purpose of OGNL allowlisting." +
" We don't recommend executing OGNL expressions against Hibernate entities, you may disallow this behaviour using the configuration `{}=true`.";
Object[] args = {original, resolved, StrutsConstants.STRUTS_DISALLOW_PROXY_OBJECT_ACCESS};
if (isDevMode) {
LOG.warn(msg, args);
} else {
LOG.debug(msg, args);
}
}
protected boolean isClassAllowlisted(Class<?> clazz) {
return allowlistClasses.contains(clazz)
|| ALLOWLIST_REQUIRED_CLASSES.contains(clazz)
@@ -18,6 +18,8 @@
*/
package org.apache.struts2.ognl;
import org.apache.commons.lang3.ClassUtils;
import java.util.HashSet;
import java.util.Set;
@@ -34,6 +36,15 @@ public class ThreadAllowlist {
private final ThreadLocal<Set<Class<?>>> allowlist = new ThreadLocal<>();
/**
* @since 7.1.0
*/
public void allowClassHierarchy(Class<?> clazz) {
allowClass(clazz);
ClassUtils.getAllSuperclasses(clazz).forEach(this::allowClass);
ClassUtils.getAllInterfaces(clazz).forEach(this::allowClass);
}
public void allowClass(Class<?> clazz) {
if (allowlist.get() == null) {
allowlist.set(new HashSet<>());
@@ -18,8 +18,6 @@
*/
package org.apache.struts2.result;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.inject.Inject;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -28,9 +26,11 @@ 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.ActionInvocation;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsStatics;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.url.QueryStringParser;
import java.io.Serial;
@@ -158,13 +158,6 @@ public class ServletDispatcherResult extends StrutsResultSupport {
//if we are inside an action tag, we always need to do an include
boolean insideActionTag = (Boolean) ObjectUtils.defaultIfNull(request.getAttribute(StrutsStatics.STRUTS_ACTION_TAG_INVOCATION), Boolean.FALSE);
// this should allow integration with third-party view related frameworks
if (finalLocation.contains("?")) {
request.setAttribute(RequestDispatcher.FORWARD_SERVLET_PATH, finalLocation.substring(0, finalLocation.indexOf('?')));
} else {
request.setAttribute(RequestDispatcher.FORWARD_SERVLET_PATH, finalLocation);
}
// If we're included, then include the view
// Otherwise do forward
// This allow the page to, for example, set content type
@@ -176,6 +169,7 @@ public class ServletDispatcherResult extends StrutsResultSupport {
dispatcher.forward(request, response);
} else {
LOG.debug("Including location: {}", finalLocation);
dispatcher.include(request, response);
}
}
@@ -18,23 +18,27 @@
*/
package org.apache.struts2.util;
import org.apache.struts2.ognl.DefaultOgnlCacheFactory;
import org.apache.struts2.ognl.OgnlCache;
import org.apache.struts2.ognl.OgnlCacheFactory;
import org.apache.commons.lang3.reflect.ConstructorUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.apache.struts2.ognl.DefaultOgnlCacheFactory;
import org.apache.struts2.ognl.OgnlCache;
import org.apache.struts2.ognl.OgnlCacheFactory;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.aop.SpringProxy;
import org.springframework.aop.TargetClassAware;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
/**
* <code>ProxyUtil</code>
@@ -44,17 +48,14 @@ import static java.lang.reflect.Modifier.isPublic;
*
*/
public class ProxyUtil {
private static final String SPRING_ADVISED_CLASS_NAME = "org.springframework.aop.framework.Advised";
private static final String SPRING_SPRINGPROXY_CLASS_NAME = "org.springframework.aop.SpringProxy";
private static final String SPRING_SINGLETONTARGETSOURCE_CLASS_NAME = "org.springframework.aop.target.SingletonTargetSource";
private static final String SPRING_TARGETCLASSAWARE_CLASS_NAME = "org.springframework.aop.TargetClassAware";
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 OgnlCache<Object, Class<?>> targetClassCache = new DefaultOgnlCacheFactory<Object, Class<?>>(
CACHE_MAX_SIZE, OgnlCacheFactory.CacheType.WTLFU, CACHE_INITIAL_CAPACITY).buildOgnlCache();
/**
* Determine the ultimate target class of the given instance, traversing
@@ -65,15 +66,18 @@ public class ProxyUtil {
* object as fallback; never {@code null})
*/
public static Class<?> ultimateTargetClass(Object candidate) {
Class<?> result = null;
if (isSpringAopProxy(candidate))
result = springUltimateTargetClass(candidate);
if (result == null) {
result = candidate.getClass();
}
return result;
return targetClassCache.computeIfAbsent(candidate, k -> {
Class<?> result = null;
if (isSpringAopProxy(k)) {
result = springUltimateTargetClass(k);
} else if (isHibernateProxy(k)) {
result = getHibernateProxyTarget(k).getClass();
}
if (result == null) {
result = k.getClass();
}
return result;
});
}
/**
@@ -81,16 +85,9 @@ public class ProxyUtil {
* @param object the object to check
*/
public static boolean isProxy(Object object) {
Class<?> clazz = object.getClass();
Boolean flag = isProxyCache.get(clazz);
if (flag != null) {
return flag;
}
boolean isProxy = isSpringAopProxy(object) || isHibernateProxy(object);
isProxyCache.put(clazz, isProxy);
return isProxy;
if (object == null) return false;
return isProxyCache.computeIfAbsent(object.getClass(),
k -> isSpringAopProxy(object) || isHibernateProxy(object));
}
/**
@@ -99,19 +96,11 @@ public class ProxyUtil {
* @param object the object to check
*/
public static boolean isProxyMember(Member member, Object object) {
if (!Modifier.isStatic(member.getModifiers()) && !isProxy(object) && !isHibernateProxy(object)) {
if (!isStatic(member.getModifiers()) && !isProxy(object)) {
return false;
}
Boolean flag = isProxyMemberCache.get(member);
if (flag != null) {
return flag;
}
boolean isProxyMember = isSpringProxyMember(member) || isHibernateProxyMember(member);
isProxyMemberCache.put(member, isProxyMember);
return isProxyMember;
return isProxyMemberCache.computeIfAbsent(member,
k -> isSpringProxyMember(member) || isHibernateProxyMember(member));
}
/**
@@ -121,8 +110,8 @@ public class ProxyUtil {
*/
public static boolean isHibernateProxy(Object object) {
try {
return HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (NoClassDefFoundError ignored) {
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (LinkageError ignored) {
return false;
}
}
@@ -134,12 +123,10 @@ public class ProxyUtil {
*/
public static boolean isHibernateProxyMember(Member member) {
try {
Class<?> clazz = ClassLoaderUtil.loadClass(HIBERNATE_HIBERNATEPROXY_CLASS_NAME, ProxyUtil.class);
return hasMember(clazz, member);
} catch (ClassNotFoundException ignored) {
return hasMember(HibernateProxy.class, member);
} catch (LinkageError ignored) {
return false;
}
return false;
}
/**
@@ -151,20 +138,11 @@ public class ProxyUtil {
* object as fallback; never {@code null})
*/
private static Class<?> springUltimateTargetClass(Object candidate) {
Object current = candidate;
Class<?> result = null;
while (null != current && implementsInterface(current.getClass(), SPRING_TARGETCLASSAWARE_CLASS_NAME)) {
try {
result = (Class<?>) MethodUtils.invokeMethod(current, "getTargetClass");
} catch (Throwable ignored) {
}
current = getSingletonTarget(current);
try {
return AopProxyUtils.ultimateTargetClass(candidate);
} catch (LinkageError ignored) {
return candidate.getClass();
}
if (result == null) {
Class<?> clazz = candidate.getClass();
result = (isCglibProxyClass(clazz) ? clazz.getSuperclass() : candidate.getClass());
}
return result;
}
/**
@@ -172,9 +150,11 @@ public class ProxyUtil {
* @param object the object to check
*/
private static boolean isSpringAopProxy(Object object) {
Class<?> clazz = object.getClass();
return (implementsInterface(clazz, SPRING_SPRINGPROXY_CLASS_NAME) && (Proxy.isProxyClass(clazz)
|| isCglibProxyClass(clazz)));
try {
return AopUtils.isAopProxy(object);
} catch (LinkageError ignored) {
return false;
}
}
/**
@@ -183,79 +163,32 @@ public class ProxyUtil {
*/
private static boolean isSpringProxyMember(Member member) {
try {
Class<?> clazz = ClassLoaderUtil.loadClass(SPRING_ADVISED_CLASS_NAME, ProxyUtil.class);
if (hasMember(clazz, member))
if (hasMember(Advised.class, member))
return true;
clazz = ClassLoaderUtil.loadClass(SPRING_TARGETCLASSAWARE_CLASS_NAME, ProxyUtil.class);
if (hasMember(clazz, member))
if (hasMember(TargetClassAware.class, member))
return true;
clazz = ClassLoaderUtil.loadClass(SPRING_SPRINGPROXY_CLASS_NAME, ProxyUtil.class);
if (hasMember(clazz, member))
if (hasMember(SpringProxy.class, member))
return true;
} catch (ClassNotFoundException ignored) {
} catch (LinkageError ignored) {
}
return false;
}
/**
* 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)
*/
private static Object getSingletonTarget(Object candidate) {
try {
if (implementsInterface(candidate.getClass(), SPRING_ADVISED_CLASS_NAME)) {
Object targetSource = MethodUtils.invokeMethod(candidate, "getTargetSource");
if (implementsInterface(targetSource.getClass(), SPRING_SINGLETONTARGETSOURCE_CLASS_NAME)) {
return MethodUtils.invokeMethod(targetSource, "getTarget");
}
}
} catch (Throwable ignored) {
}
return null;
}
/**
* Check whether the specified class is a CGLIB-generated class.
* @param clazz the class to check
*/
private static boolean isCglibProxyClass(Class<?> clazz) {
return (clazz != null && clazz.getName().contains("$$"));
}
/**
* Check whether the given class implements an interface with a given class name.
* @param clazz the class to check
* @param ifaceClassName the interface class name to check
*/
private static boolean implementsInterface(Class<?> clazz, String ifaceClassName) {
try {
Class<?> ifaceClass = ClassLoaderUtil.loadClass(ifaceClassName, ProxyUtil.class);
return ifaceClass.isAssignableFrom(clazz);
} catch (ClassNotFoundException e) {
return false;
}
}
/**
* Check whether the given class has a given member.
* @param clazz the class to check
* @param member the member to check
*/
private static boolean hasMember(Class<?> clazz, Member member) {
if (member instanceof Method) {
return null != MethodUtils.getMatchingMethod(clazz, member.getName(), ((Method) member).getParameterTypes());
if (member instanceof Method method) {
return null != MethodUtils.getMatchingMethod(clazz, member.getName(), method.getParameterTypes());
}
if (member instanceof Field) {
return null != FieldUtils.getField(clazz, member.getName(), true);
}
if (member instanceof Constructor) {
return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, ((Constructor) member).getParameterTypes());
if (member instanceof Constructor<?> constructor) {
return null != ConstructorUtils.getMatchingAccessibleConstructor(clazz, constructor.getParameterTypes());
}
return false;
}
@@ -265,26 +198,34 @@ public class ProxyUtil {
public static Object getHibernateProxyTarget(Object object) {
try {
return Hibernate.unproxy(object);
} catch (NoClassDefFoundError ignored) {
} catch (LinkageError ignored) {
return object;
}
}
/**
* @deprecated since 7.1, use {@link #resolveTargetMember(Member, Class)} instead.
*/
@Deprecated
public static Member resolveTargetMember(Member proxyMember, Object target) {
return resolveTargetMember(proxyMember, target.getClass());
}
/**
* @return matching member on target object if one exists, otherwise the same member
*/
public static Member resolveTargetMember(Member proxyMember, Object target) {
public static Member resolveTargetMember(Member proxyMember, Class<?> targetClass) {
int mod = proxyMember.getModifiers();
if (proxyMember instanceof Method) {
if (isPublic(mod)) {
return MethodUtils.getMatchingAccessibleMethod(target.getClass(), proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
return MethodUtils.getMatchingAccessibleMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
} else {
return MethodUtils.getMatchingMethod(target.getClass(), proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
return MethodUtils.getMatchingMethod(targetClass, proxyMember.getName(), ((Method) proxyMember).getParameterTypes());
}
} else if (proxyMember instanceof Field) {
return FieldUtils.getField(target.getClass(), proxyMember.getName(), isPublic(mod));
return FieldUtils.getField(targetClass, proxyMember.getName(), isPublic(mod));
} else if (proxyMember instanceof Constructor && isPublic(mod)) {
return ConstructorUtils.getMatchingAccessibleConstructor(target.getClass(), ((Constructor<?>) proxyMember).getParameterTypes());
return ConstructorUtils.getMatchingAccessibleConstructor(targetClass, ((Constructor<?>) proxyMember).getParameterTypes());
}
return proxyMember;
}
@@ -20,6 +20,10 @@ package org.apache.struts2.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(since = "6.8.0", forRemoval = true)
public interface ReflectionContextFactory {
/**
* Creates and returns a new standard naming context for evaluating an OGNL
@@ -299,4 +299,7 @@ struts.url.queryStringParser=strutsQueryStringParser
struts.url.encoder=strutsUrlEncoder
struts.url.decoder=strutsUrlDecoder
### Defines source to read nonce value from, possible values are: request, session
struts.csp.nonceSource=session
### END SNIPPET: complete_file
@@ -75,6 +75,11 @@ struts.messages.upload.error.FileUploadContentTypeException=Request has wrong co
# Default error message when handling multi-part request
struts.messages.upload.error.FileUploadException=Error parsing the multi-part request.
# IllegalArgumentException for empty files
# 0 - original filename
# 1 - field name
struts.messages.upload.error.IllegalArgumentException=File {0} for field {1} is empty (0 bytes). Empty file uploads are not allowed.
devmode.notification=Developer Notification (set struts.devMode to false to disable this message):\n{0}
struts.exception.missing-package-action.with-context = There is no Action mapped for namespace [{0}] and action name [{1}] associated with context path [{2}].
+3
View File
@@ -250,4 +250,7 @@
<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"/>
</struts>
@@ -165,14 +165,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">Set the value used to retrieve the option value.</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -117,14 +117,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">String that will be appended to the label</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -117,14 +117,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">String that will be appended to the label</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -21,6 +21,7 @@ package org.apache.struts2.components;
import org.apache.struts2.ActionContext;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.components.template.Template;
import org.apache.struts2.components.template.TemplateEngine;
@@ -32,6 +33,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static org.apache.struts2.security.DefaultNotExcludedAcceptedPatternsCheckerTest.NO_EXCLUSION_ACCEPT_ALL_PATTERNS_CHECKER;
@@ -236,6 +238,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setAccesskey(accesskeyValue);
txtFld.evaluateParams();
@@ -250,6 +254,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.addParameter("value", value);
txtFld.evaluateParams();
@@ -338,6 +344,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setCssClass(cssClass);
txtFld.evaluateParams();
@@ -352,6 +360,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setStyle(cssStyle);
txtFld.evaluateParams();
@@ -372,6 +382,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
actionContext.withSession(new SessionMap(req));
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
@@ -392,11 +404,35 @@ public class UIBeanTest extends StrutsInternalTestCase {
session.invalidate();
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertNull(dblSelect.getAttributes().get("nonce"));
}
public void testNonceOfRequestAttribute() {
Map<String, String> params = new HashMap<String, String>(){{
put(StrutsConstants.STRUTS_CSP_NONCE_SOURCE, "request");
}};
initDispatcher(params);
String nonceVal = "r4nd0m";
ValueStack stack = ActionContext.getContext().getValueStack();
MockHttpServletRequest req = new MockHttpServletRequest();
req.setAttribute("nonce", nonceVal);
MockHttpServletResponse res = new MockHttpServletResponse();
ActionContext actionContext = stack.getActionContext();
actionContext.withServletRequest(req);
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
}
public void testSetNullUiStaticContentPath() {
// given
ValueStack stack = ActionContext.getContext().getValueStack();
@@ -50,6 +50,11 @@ import java.util.*;
import static org.junit.Assert.assertArrayEquals;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.util.Date;
import java.util.Set;
@@ -100,6 +105,36 @@ public class XWorkConverterTest extends XWorkTestCase {
Date dateRfc3339DateOnly = (Date) converter.convertValue(context, null, null, null, "2001-01-10", Date.class);
assertEquals(date, dateRfc3339DateOnly);
// java.time library tests
DateTimeFormatter formatterDate = DateTimeFormatter.ofPattern("MM/dd/yyyy");
LocalDate localDate = LocalDate.parse("01/10/2001", formatterDate);
DateTimeFormatter formatterDateTime = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse("2001-01-10T10:11:12", formatterDateTime);
DateTimeFormatter formatterTime = DateTimeFormatter.ofPattern("HH:mm:ss");
LocalTime localTime = LocalTime.parse("10:11:12", formatterTime);
DateTimeFormatter formatterOffsetDateTime = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
OffsetDateTime offsetDateTime = OffsetDateTime.parse("2001-01-10T10:11:12+05:00", formatterOffsetDateTime);
String localDateStr = (String) converter.convertValue(context, null, null, null, localDate, String.class);
String localDateTimeStr = (String) converter.convertValue(context, null, null, null, localDateTime, String.class);
String localTimeStr = (String) converter.convertValue(context, null, null, null, localTime, String.class);
String offsetDateTimeStr = (String) converter.convertValue(context, null, null, null, offsetDateTime, String.class);
LocalDate localDate2 = (LocalDate) converter.convertValue(context, null, null, null, localDateStr, LocalDate.class);
assertEquals(localDate, localDate2);
LocalDateTime localDateTime2 = (LocalDateTime) converter.convertValue(context, null, null, null, localDateTimeStr, LocalDateTime.class);
assertEquals(localDateTime, localDateTime2);
LocalTime localTime2 = (LocalTime) converter.convertValue(context, null, null, null, localTimeStr, LocalTime.class);
assertEquals(localTime, localTime2);
OffsetDateTime offsetDateTime2 = (OffsetDateTime) converter.convertValue(context, null, null, null, offsetDateTimeStr, OffsetDateTime.class);
assertEquals(offsetDateTime, offsetDateTime2);
}
public void testDateConversionWithDefault() throws ParseException {
@@ -19,13 +19,7 @@
package org.apache.struts2.dispatcher.multipart;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.struts2.config.Configuration;
import org.apache.struts2.config.ConfigurationManager;
import org.apache.struts2.dispatcher.Dispatcher;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.apache.struts2.inject.Container;
import org.apache.struts2.util.StrutsTestCaseHelper;
import org.apache.struts2.views.jsp.StrutsMockServletContext;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.After;
import org.junit.Before;
@@ -36,8 +30,10 @@ import org.springframework.mock.web.MockHttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -54,7 +50,6 @@ abstract class AbstractMultiPartRequestTest {
protected final String endline = "\r\n";
protected AbstractMultiPartRequest multiPart;
protected Container container;
abstract protected AbstractMultiPartRequest createMultipartRequest();
@@ -67,13 +62,7 @@ abstract class AbstractMultiPartRequestTest {
}
@Before
public void before() throws Exception {
StrutsMockServletContext servletContext = new StrutsMockServletContext();
Dispatcher dispatcher = StrutsTestCaseHelper.initDispatcher(servletContext, Collections.emptyMap());
ConfigurationManager configurationManager = dispatcher.getConfigurationManager();
Configuration configuration = configurationManager.getConfiguration();
container = configuration.getContainer();
public void before() {
mockRequest = new MockHttpServletRequest();
mockRequest.setCharacterEncoding(StandardCharsets.UTF_8.name());
mockRequest.setMethod("post");
@@ -431,7 +420,6 @@ abstract class AbstractMultiPartRequestTest {
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
mockRequest.setCharacterEncoding(null);
multiPart.setDefaultEncoding(StandardCharsets.ISO_8859_1.name());
multiPart.parse(mockRequest, tempDir);
@@ -507,44 +495,365 @@ abstract class AbstractMultiPartRequestTest {
}
@Test
public void maliciousFields() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formField("top.param", "expression") +
endline + "--" + boundary + "--";
public void cleanupDoesNotClearErrorsList() throws IOException {
// given - create a scenario that generates errors
String content = formFile("file1", "test1.csv", "1,2,3,4");
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
multiPart.setMaxSize("1"); // Very small to trigger error
multiPart.parse(mockRequest, tempDir);
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getParameterNames().asIterator()).toIterable()
.isEmpty();
// Verify errors exist
assertThat(multiPart.getErrors()).isNotEmpty();
int originalErrorCount = multiPart.getErrors().size();
// when
multiPart.cleanUp();
// then - errors should remain (cleanup doesn't clear errors)
assertThat(multiPart.getErrors()).hasSize(originalErrorCount);
}
@Test
public void maliciousFilename() throws IOException {
String content = formFile("file1", "../test1.csv", "1,2,3,4") +
formField("param", "expression") +
endline + "--" + boundary + "--";
public void largeFileUploadHandling() throws IOException {
// Test that large files are handled properly
StringBuilder largeContent = new StringBuilder();
for (int i = 0; i < 1000; i++) {
largeContent.append("line").append(i).append(",");
}
String content = formFile("largefile", "large.csv", largeContent.toString()) +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
// when
multiPart.parse(mockRequest, tempDir);
// then - should complete without memory issues
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.getFile("largefile")).hasSize(1);
// Cleanup should properly handle large files
multiPart.cleanUp();
assertThat(multiPart.uploadedFiles).isEmpty();
}
@Test
public void multipleFileUploadWithMixedContent() throws IOException {
// Test mixed content with multiple files and parameters
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formField("param1", "value1") +
formFile("file2", "test2.csv", "5,6,7,8") +
formField("param2", "value2") +
formFile("file3", "test3.csv", "9,10,11,12") +
formField("param3", "value3") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - verify all content was processed
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.getFile("file1")).hasSize(1);
assertThat(multiPart.getFile("file2")).hasSize(1);
assertThat(multiPart.getFile("file3")).hasSize(1);
assertThat(multiPart.getParameter("param1")).isEqualTo("value1");
assertThat(multiPart.getParameter("param2")).isEqualTo("value2");
assertThat(multiPart.getParameter("param3")).isEqualTo("value3");
// Store file paths for post-cleanup verification
List<String> filePaths = new ArrayList<>();
for (UploadedFile file : multiPart.getFile("file1")) {
filePaths.add(file.getAbsolutePath());
}
for (UploadedFile file : multiPart.getFile("file2")) {
filePaths.add(file.getAbsolutePath());
}
for (UploadedFile file : multiPart.getFile("file3")) {
filePaths.add(file.getAbsolutePath());
}
// when - cleanup
multiPart.cleanUp();
// then - verify complete cleanup
assertThat(multiPart.uploadedFiles).isEmpty();
assertThat(multiPart.parameters).isEmpty();
// Verify files are deleted
for (String filePath : filePaths) {
assertThat(new File(filePath)).doesNotExist();
}
}
@Test
public void createTemporaryFileGeneratesSecureNames() {
// Create a test instance to access the protected method
AbstractMultiPartRequest testRequest = createMultipartRequest();
Path testLocation = Paths.get(tempDir);
// when - create multiple temporary files
File tempFile1 = testRequest.createTemporaryFile("test1.csv", testLocation);
File tempFile2 = testRequest.createTemporaryFile("test2.csv", testLocation);
File tempFile3 = testRequest.createTemporaryFile("../../../malicious.csv", testLocation);
// then - verify secure naming
assertThat(tempFile1.getName()).startsWith("upload_");
assertThat(tempFile1.getName()).endsWith(".tmp");
assertThat(tempFile2.getName()).startsWith("upload_");
assertThat(tempFile2.getName()).endsWith(".tmp");
assertThat(tempFile3.getName()).startsWith("upload_");
assertThat(tempFile3.getName()).endsWith(".tmp");
// Verify each file has a unique name
assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName());
assertThat(tempFile2.getName()).isNotEqualTo(tempFile3.getName());
assertThat(tempFile1.getName()).isNotEqualTo(tempFile3.getName());
// Verify all files are in the correct location
assertThat(tempFile1.getParent()).isEqualTo(tempDir);
assertThat(tempFile2.getParent()).isEqualTo(tempDir);
assertThat(tempFile3.getParent()).isEqualTo(tempDir);
// Verify malicious filename doesn't affect the location
assertThat(tempFile3.getName()).doesNotContain("..");
assertThat(tempFile3.getName()).doesNotContain("/");
assertThat(tempFile3.getName()).doesNotContain("\\");
// Clean up test files
tempFile1.delete();
tempFile2.delete();
tempFile3.delete();
}
@Test
public void createTemporaryFileInSpecificDirectory() throws IOException {
// Create a subdirectory for testing
Path subDir = Paths.get(tempDir, "subdir");
Files.createDirectories(subDir);
AbstractMultiPartRequest testRequest = createMultipartRequest();
// when
File tempFile = testRequest.createTemporaryFile("test.csv", subDir);
// then - verify file is created in the specified subdirectory
assertThat(tempFile.getParent()).isEqualTo(subDir.toString());
assertThat(tempFile.getName()).startsWith("upload_");
assertThat(tempFile.getName()).endsWith(".tmp");
// Clean up
tempFile.delete();
Files.delete(subDir);
}
@Test
public void createTemporaryFileWithNullFileName() throws IOException {
AbstractMultiPartRequest testRequest = createMultipartRequest();
Path testLocation = Paths.get(tempDir);
// when - create temp file with null filename
File tempFile = testRequest.createTemporaryFile(null, testLocation);
// then - should still create a valid temporary file
assertThat(tempFile.getName()).startsWith("upload_");
assertThat(tempFile.getName()).endsWith(".tmp");
assertThat(tempFile.getParent()).isEqualTo(tempDir);
// Clean up
tempFile.delete();
}
@Test
public void createTemporaryFileWithEmptyFileName() throws IOException {
AbstractMultiPartRequest testRequest = createMultipartRequest();
Path testLocation = Paths.get(tempDir);
// when - create temp file with empty filename
File tempFile = testRequest.createTemporaryFile("", testLocation);
// then - should still create a valid temporary file
assertThat(tempFile.getName()).startsWith("upload_");
assertThat(tempFile.getName()).endsWith(".tmp");
assertThat(tempFile.getParent()).isEqualTo(tempDir);
// Clean up
tempFile.delete();
}
@Test
public void createTemporaryFileWithSpecialCharacters() {
AbstractMultiPartRequest testRequest = createMultipartRequest();
Path testLocation = Paths.get(tempDir);
// when - create temp files with various special characters
File tempFile1 = testRequest.createTemporaryFile("file with spaces.csv", testLocation);
File tempFile2 = testRequest.createTemporaryFile("file@#$%^&*().csv", testLocation);
File tempFile3 = testRequest.createTemporaryFile("файл.csv", testLocation); // Cyrillic
// then - all should create valid secure temporary files
File[] tempFiles = {tempFile1, tempFile2, tempFile3};
for (File tempFile : tempFiles) {
assertThat(tempFile.getName()).startsWith("upload_");
assertThat(tempFile.getName()).endsWith(".tmp");
assertThat(tempFile.getParent()).isEqualTo(tempDir);
// Verify no special characters leak into the actual filename
assertThat(tempFile.getName()).matches("upload_[a-zA-Z0-9_]+\\.tmp");
}
// All should have unique names
assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName());
assertThat(tempFile2.getName()).isNotEqualTo(tempFile3.getName());
assertThat(tempFile1.getName()).isNotEqualTo(tempFile3.getName());
// Clean up
tempFile1.delete();
tempFile2.delete();
tempFile3.delete();
}
@Test
public void createTemporaryFileConsistentNaming() {
AbstractMultiPartRequest testRequest = createMultipartRequest();
Path testLocation = Paths.get(tempDir);
// when - create many temporary files to verify naming consistency
List<File> tempFiles = new ArrayList<>();
for (int i = 0; i < 100; i++) {
tempFiles.add(testRequest.createTemporaryFile("test" + i + ".csv", testLocation));
}
// then - all should follow the same naming pattern
for (File tempFile : tempFiles) {
assertThat(tempFile.getName()).startsWith("upload_");
assertThat(tempFile.getName()).endsWith(".tmp");
assertThat(tempFile.getParent()).isEqualTo(tempDir);
// Verify UUID pattern (without hyphens, replaced with underscores)
assertThat(tempFile.getName()).matches("upload_[a-zA-Z0-9_]+\\.tmp");
}
// Verify all names are unique
List<String> fileNames = tempFiles.stream().map(File::getName).toList();
assertThat(fileNames).doesNotHaveDuplicates();
// Clean up
tempFiles.forEach(File::delete);
}
@Test
public void emptyFileUploadsAreRejected() throws IOException {
// Test that empty files (0 bytes) are rejected with proper error message
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"empty.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
// No content - this creates a 0-byte file
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - should reject empty file and add error
assertThat(multiPart.getErrors())
.isEmpty();
.hasSize(1)
.first()
.satisfies(error -> {
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.IllegalArgumentException");
assertThat(error.getArgs()).containsExactly("empty.txt", "emptyfile");
});
assertThat(multiPart.uploadedFiles).isEmpty();
assertThat(multiPart.getFile("emptyfile")).isEmpty();
}
assertThat(multiPart.getParameterNames().asIterator()).toIterable()
.hasSize(1);
assertThat(multiPart.getParameterNames().asIterator()).toIterable()
.containsOnly("param");
assertThat(multiPart.getFileNames("file1")).isEmpty();
@Test
public void mixedEmptyAndValidFilesProcessedCorrectly() throws IOException {
// Test that valid files are processed while empty files are rejected
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"emptyfile1\"; filename=\"empty1.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
// No content - empty file
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
"some valid content" +
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"emptyfile2\"; filename=\"empty2.txt\"" + endline +
"Content-Type: application/octet-stream" + endline +
endline +
// Another empty file
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - should have 2 errors for empty files, 1 valid file processed
assertThat(multiPart.getErrors()).hasSize(2);
assertThat(multiPart.getErrors().get(0))
.satisfies(error -> {
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.IllegalArgumentException");
assertThat(error.getArgs()).containsExactly("empty1.txt", "emptyfile1");
});
assertThat(multiPart.getErrors().get(1))
.satisfies(error -> {
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.IllegalArgumentException");
assertThat(error.getArgs()).containsExactly("empty2.txt", "emptyfile2");
});
// Only the valid file should be processed
assertThat(multiPart.uploadedFiles).hasSize(1);
assertThat(multiPart.getFile("validfile")).hasSize(1);
assertThat(multiPart.getFile("emptyfile1")).isEmpty();
assertThat(multiPart.getFile("emptyfile2")).isEmpty();
// Verify valid file content
assertThat(multiPart.getFile("validfile")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo("some valid content");
}
@Test
public void emptyFileTemporaryFileCleanup() throws IOException {
// Test that temporary files for empty files are properly cleaned up
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"empty.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
// Empty file
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// Count temp files before processing
File[] tempFilesBefore = new File(tempDir).listFiles((dir, name) -> name.startsWith("upload_") && name.endsWith(".tmp"));
int countBefore = tempFilesBefore != null ? tempFilesBefore.length : 0;
// when
multiPart.parse(mockRequest, tempDir);
// then - should reject empty file and clean up temp file
assertThat(multiPart.getErrors()).hasSize(1);
assertThat(multiPart.uploadedFiles).isEmpty();
// Verify that temporary files are cleaned up (may have implementation differences)
// Some implementations create temp files first, others don't create any for empty uploads
File[] tempFilesAfter = new File(tempDir).listFiles((dir, name) -> name.startsWith("upload_") && name.endsWith(".tmp"));
int countAfter = tempFilesAfter != null ? tempFilesAfter.length : 0;
// Allow for implementation differences - just ensure no new temp files remain
assertThat(countAfter).isLessThanOrEqualTo(countBefore);
}
protected String formFile(String fieldName, String filename, String content) {
@@ -1,69 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher.multipart;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import static org.assertj.core.api.Assertions.assertThat;
abstract class AbstractMultiPartRequestWithDMITest extends AbstractMultiPartRequestTest {
@Test
public void actionField() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formField("action:myUploads", "") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
multiPart.parse(mockRequest, tempDir);
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getParameterNames().asIterator()).toIterable()
.containsOnly("action:myUploads");
}
@Test
public void actionFieldWithBang() throws IOException {
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formField("action:myUploads!upload", "") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(mockRequest)).isTrue();
multiPart.parse(mockRequest, tempDir);
assertThat(multiPart.getErrors())
.isEmpty();
assertThat(multiPart.getParameterNames().asIterator()).toIterable()
.containsOnly("action:myUploads!upload");
}
}
@@ -18,6 +18,22 @@
*/
package org.apache.struts2.dispatcher.multipart;
import org.apache.commons.fileupload2.core.DiskFileItem;
import org.apache.struts2.dispatcher.LocalizedMessage;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
import static org.assertj.core.api.Assertions.assertThat;
public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest {
@Override
@@ -25,4 +41,416 @@ public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest {
return new JakartaMultiPartRequest();
}
@Test
public void temporaryFileCleanupForInMemoryUploads() throws IOException, NoSuchFieldException, IllegalAccessException {
// given - small files that will be in-memory
String content = formFile("file1", "test1.csv", "a,b,c,d") +
formFile("file2", "test2.csv", "1,2,3,4") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// Access private field to verify temporary files are tracked
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
tempFilesField.setAccessible(true);
@SuppressWarnings("unchecked")
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
// Store file paths before cleanup for verification
List<String> tempFilePaths = temporaryFiles.stream()
.map(File::getAbsolutePath)
.toList();
// Verify temporary files exist before cleanup
assertThat(temporaryFiles).isNotEmpty();
for (File tempFile : temporaryFiles) {
assertThat(tempFile).exists();
}
// when - cleanup
multiPart.cleanUp();
// then - verify files are deleted and tracking list is cleared
for (String tempFilePath : tempFilePaths) {
assertThat(new File(tempFilePath)).doesNotExist();
}
assertThat(temporaryFiles).isEmpty();
}
@Test
public void cleanupMethodsCanBeOverridden() {
// Create a custom implementation to test extensibility
class CustomJakartaMultiPartRequest extends JakartaMultiPartRequest {
boolean diskFileItemsCleanedUp = false;
boolean temporaryFilesCleanedUp = false;
@Override
protected void cleanUpDiskFileItems() {
diskFileItemsCleanedUp = true;
super.cleanUpDiskFileItems();
}
@Override
protected void cleanUpTemporaryFiles() {
temporaryFilesCleanedUp = true;
super.cleanUpTemporaryFiles();
}
}
CustomJakartaMultiPartRequest customMultiPart = new CustomJakartaMultiPartRequest();
// when
customMultiPart.cleanUp();
// then
assertThat(customMultiPart.diskFileItemsCleanedUp).isTrue();
assertThat(customMultiPart.temporaryFilesCleanedUp).isTrue();
}
@Test
public void temporaryFileCreationFailureAddsError() throws IOException {
// Create a custom implementation that simulates temp file creation failure
class FaultyJakartaMultiPartRequest extends JakartaMultiPartRequest {
@Override
protected void processFileField(DiskFileItem item, String saveDir) {
// Simulate in-memory upload that fails to create temp file
if (item.isInMemory()) {
try {
// Simulate IOException during temp file creation
throw new IOException("Simulated temp file creation failure");
} catch (IOException e) {
// Add the error to the errors list for proper user feedback
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(),
new Object[]{item.getName()});
if (!errors.contains(errorMessage)) {
errors.add(errorMessage);
}
}
} else {
super.processFileField(item, saveDir);
}
}
}
FaultyJakartaMultiPartRequest faultyMultiPart = new FaultyJakartaMultiPartRequest();
// given - small file that would normally be in-memory
String content = formFile("file1", "test1.csv", "a,b") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
faultyMultiPart.parse(mockRequest, tempDir);
// then - verify error is properly captured
assertThat(faultyMultiPart.getErrors())
.hasSize(1)
.first()
.extracting(LocalizedMessage::getTextKey)
.isEqualTo("struts.messages.upload.error.IOException");
}
@Test
public void temporaryFileCreationErrorsAreNotDuplicated() throws IOException {
// Test that duplicate errors are not added to the errors list
JakartaMultiPartRequest multiPartWithDuplicateErrors = new JakartaMultiPartRequest();
// Simulate adding the same error twice
IOException testException = new IOException("Test exception");
LocalizedMessage errorMessage = multiPartWithDuplicateErrors.buildErrorMessage(
testException.getClass(), testException.getMessage(), new Object[]{"test.csv"});
// when - add same error twice
multiPartWithDuplicateErrors.errors.add(errorMessage);
if (!multiPartWithDuplicateErrors.errors.contains(errorMessage)) {
multiPartWithDuplicateErrors.errors.add(errorMessage);
}
// then - only one error should be present
assertThat(multiPartWithDuplicateErrors.getErrors()).hasSize(1);
}
@Test
public void cleanupIsIdempotent() throws IOException {
// given - process some files
String content = formFile("file1", "test1.csv", "1,2,3,4") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
multiPart.parse(mockRequest, tempDir);
// when - call cleanup multiple times
multiPart.cleanUp();
multiPart.cleanUp();
multiPart.cleanUp();
// then - should not throw exceptions and should be safe
assertThat(multiPart.uploadedFiles).isEmpty();
assertThat(multiPart.parameters).isEmpty();
}
@Test
public void endToEndMultipartProcessingWithCleanup() throws IOException {
// Test complete multipart processing lifecycle
String content = formFile("file1", "test1.csv", "1,2,3,4") +
formField("param1", "value1") +
formFile("file2", "test2.csv", "5,6,7,8") +
formField("param2", "value2") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when - full processing
multiPart.parse(mockRequest, tempDir);
// then - verify everything was processed
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.getFile("file1")).hasSize(1);
assertThat(multiPart.getFile("file2")).hasSize(1);
assertThat(multiPart.getParameter("param1")).isEqualTo("value1");
assertThat(multiPart.getParameter("param2")).isEqualTo("value2");
// when - cleanup
multiPart.cleanUp();
// then - verify complete cleanup
assertThat(multiPart.uploadedFiles).isEmpty();
assertThat(multiPart.parameters).isEmpty();
}
@Test
public void temporaryFilesCreatedInSaveDirectory() throws IOException, NoSuchFieldException, IllegalAccessException {
// Test that temporary files for in-memory uploads are created in the saveDir, not system temp
String content = formFile("file1", "test1.csv", "small,content") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// Access private field to get temporary files
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
tempFilesField.setAccessible(true);
@SuppressWarnings("unchecked")
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
// then - verify temporary files are created in saveDir
assertThat(temporaryFiles).isNotEmpty();
for (File tempFile : temporaryFiles) {
// Verify the temporary file is in the saveDir, not system temp
assertThat(tempFile.getParent()).isEqualTo(tempDir);
assertThat(tempFile.getName()).startsWith("upload_");
assertThat(tempFile.getName()).endsWith(".tmp");
assertThat(tempFile).exists();
}
}
@Test
public void secureTemporaryFileNaming() throws IOException, NoSuchFieldException, IllegalAccessException {
// Test that temporary files use UUID-based naming for security
String content = formFile("file1", "malicious../../../etc/passwd", "content") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// Access private field to get temporary files
Field tempFilesField = JakartaMultiPartRequest.class.getDeclaredField("temporaryFiles");
tempFilesField.setAccessible(true);
@SuppressWarnings("unchecked")
List<File> temporaryFiles = (List<File>) tempFilesField.get(multiPart);
// then - verify secure naming prevents directory traversal
assertThat(temporaryFiles).isNotEmpty();
for (File tempFile : temporaryFiles) {
// Verify the temporary file uses secure UUID naming
assertThat(tempFile.getName()).startsWith("upload_");
assertThat(tempFile.getName()).endsWith(".tmp");
// Verify it doesn't contain malicious path elements
assertThat(tempFile.getName()).doesNotContain("..");
assertThat(tempFile.getName()).doesNotContain("/");
assertThat(tempFile.getName()).doesNotContain("\\");
// Verify it's in the correct directory
assertThat(tempFile.getParent()).isEqualTo(tempDir);
}
}
@Test
public void processNormalFormFieldHandlesNullFieldName() throws IOException {
// Test null field name handling in processNormalFormField
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data" + endline + // No name attribute
endline +
"field value without name" +
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"validfield\"" + endline +
endline +
"valid field value" +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - should only process the valid field
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.getParameter("validfield")).isEqualTo("valid field value");
assertThat(multiPart.getParameterNames().asIterator()).toIterable().hasSize(1);
}
@Test
public void processFileFieldHandlesNullFieldName() throws IOException {
// Test null field name handling in processFileField
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data; filename=\"orphan.txt\"" + endline + // No name attribute
"Content-Type: text/plain" + endline +
endline +
"orphaned file content" +
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
"valid file content" +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - should only process the valid file
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.uploadedFiles).hasSize(1);
assertThat(multiPart.getFile("validfile")).hasSize(1);
assertThat(multiPart.getFile("validfile")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo("valid file content");
}
@Test
public void diskFileItemCleanupCoverage() throws IOException, NoSuchFieldException, IllegalAccessException {
// Test disk file item cleanup paths
String content = formFile("file1", "test1.csv", "1,2,3,4") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when - force files to disk with small buffer
multiPart.setBufferSize("1");
multiPart.parse(mockRequest, tempDir);
// Access private field to verify disk file items are tracked
Field diskFileItemsField = JakartaMultiPartRequest.class.getDeclaredField("diskFileItems");
diskFileItemsField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.List<org.apache.commons.fileupload2.core.DiskFileItem> diskFileItems =
(java.util.List<org.apache.commons.fileupload2.core.DiskFileItem>) diskFileItemsField.get(multiPart);
// then - should have disk file items tracked
assertThat(diskFileItems).isNotEmpty();
// when - cleanup
multiPart.cleanUp();
// then - should clear tracking
assertThat(diskFileItems).isEmpty();
}
@Test
public void inMemoryVsDiskFileHandling() throws IOException {
// Test both in-memory and disk file handling paths
String smallContent = "small"; // Should be in-memory
String largeContent = "x".repeat(20000); // Should go to disk
String content = formFile("smallfile", "small.txt", smallContent) +
formFile("largefile", "large.txt", largeContent) +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when - use default buffer size
multiPart.parse(mockRequest, tempDir);
// then - both files should be processed
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.uploadedFiles).hasSize(2);
assertThat(multiPart.getFile("smallfile")).hasSize(1);
assertThat(multiPart.getFile("largefile")).hasSize(1);
// Verify content
assertThat(multiPart.getFile("smallfile")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo(smallContent);
assertThat(multiPart.getFile("largefile")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo(largeContent);
}
@Test
public void errorDuplicationPrevention() throws IOException {
// Test that duplicate errors are not added
JakartaMultiPartRequest multiPartRequest = new JakartaMultiPartRequest();
// Simulate adding the same error multiple times
IOException testException = new IOException("Test error");
LocalizedMessage errorMessage = multiPartRequest.buildErrorMessage(
testException.getClass(), testException.getMessage(), new Object[]{"test.csv"});
// when - try to add same error multiple times
multiPartRequest.errors.add(errorMessage);
if (!multiPartRequest.errors.contains(errorMessage)) {
multiPartRequest.errors.add(errorMessage); // Should not be added
}
if (!multiPartRequest.errors.contains(errorMessage)) {
multiPartRequest.errors.add(errorMessage); // Should not be added
}
// then - should only have one error
assertThat(multiPartRequest.getErrors()).hasSize(1);
}
@Test
public void processFileFieldHandlesEmptyFileName() throws IOException {
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"\"" + endline +
"Content-Type: text/plain" + endline +
endline +
"some content that should be ignored" +
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"validfile\"; filename=\"test.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
"valid file content" +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - should only process the file with valid filename
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.uploadedFiles).hasSize(1);
assertThat(multiPart.getFile("validfile")).hasSize(1);
assertThat(multiPart.getFile("emptyfile")).isEmpty();
assertThat(multiPart.getFile("validfile")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo("valid file content");
}
}
@@ -23,10 +23,18 @@ import org.apache.struts2.dispatcher.LocalizedMessage;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class JakartaStreamMultiPartRequestTest extends AbstractMultiPartRequestTest {
@@ -71,4 +79,316 @@ public class JakartaStreamMultiPartRequestTest extends AbstractMultiPartRequestT
.containsExactly("struts.messages.upload.error.FileUploadSizeException");
}
@Test
public void readStreamProperlyHandlesResources() throws Exception {
// Create a test input stream with known data
byte[] testData = "test data for stream reading".getBytes(StandardCharsets.UTF_8);
InputStream testStream = new java.io.ByteArrayInputStream(testData);
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
// Use reflection to access private readStream method
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
readStreamMethod.setAccessible(true);
// when
String result = (String) readStreamMethod.invoke(streamMultiPart, testStream);
// then
assertThat(result).isEqualTo("test data for stream reading");
}
@Test
public void readStreamHandlesExceptionsProperly() throws Exception {
// Create a stream that throws an exception
InputStream faultyStream = new InputStream() {
@Override
public int read() throws IOException {
throw new IOException("Simulated stream failure");
}
};
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
// Use reflection to access private readStream method
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
readStreamMethod.setAccessible(true);
// when/then - should propagate the exception
assertThatThrownBy(() -> readStreamMethod.invoke(streamMultiPart, faultyStream))
.isInstanceOf(InvocationTargetException.class)
.cause()
.isInstanceOf(IOException.class)
.hasMessage("Simulated stream failure");
}
@Test
public void readStreamHandlesEmptyStream() throws Exception {
// Create an empty stream
InputStream emptyStream = new java.io.ByteArrayInputStream(new byte[0]);
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
// Use reflection to access private readStream method
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
readStreamMethod.setAccessible(true);
// when
String result = (String) readStreamMethod.invoke(streamMultiPart, emptyStream);
// then
assertThat(result).isEmpty();
}
@Test
public void readStreamHandlesLargeData() throws Exception {
// Create a large data stream to test buffer handling
StringBuilder largeData = new StringBuilder();
for (int i = 0; i < 2000; i++) {
largeData.append("line").append(i).append("\n");
}
byte[] testData = largeData.toString().getBytes(StandardCharsets.UTF_8);
InputStream largeStream = new java.io.ByteArrayInputStream(testData);
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
// Use reflection to access private readStream method
Method readStreamMethod = JakartaStreamMultiPartRequest.class.getDeclaredMethod("readStream", InputStream.class);
readStreamMethod.setAccessible(true);
// when
String result = (String) readStreamMethod.invoke(streamMultiPart, largeStream);
// then
assertThat(result).isEqualTo(largeData.toString());
assertThat(result.length()).isGreaterThan(1024); // Verify it's larger than internal buffer
}
@Test
public void processFileItemAsFormFieldHandlesNullFieldName() throws IOException {
// Test the null field name path in processFileItemAsFormField
String content = formFile("", "test.csv", "data") + // Field name will be empty/null-like
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - should complete without error, but no parameters should be added
assertThat(multiPart.getErrors()).isEmpty();
}
@Test
public void processFileItemAsFileFieldHandlesNullFieldName() throws IOException {
// This test covers the null field name path in processFileItemAsFileField
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
// Create a mock file item with null field name
String content = "--" + boundary + endline +
"Content-Disposition: form-data; filename=\"test.csv\"" + endline +
"Content-Type: text/csv" + endline +
endline +
"test data" +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
streamMultiPart.parse(mockRequest, tempDir);
// then - should complete without error but no files should be uploaded
assertThat(streamMultiPart.getErrors()).isEmpty();
assertThat(streamMultiPart.uploadedFiles).isEmpty();
}
@Test
public void exceedsMaxFilesPath() throws IOException {
// Test the exceedsMaxFiles method path
String content = formFile("file1", "test1.csv", "data1") +
formFile("file2", "test2.csv", "data2") +
formFile("file3", "test3.csv", "data3") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when - set max files to 1
multiPart.setMaxFiles("1");
multiPart.parse(mockRequest, tempDir);
// then - should have only 1 file and errors for others
assertThat(multiPart.uploadedFiles).hasSize(1);
assertThat(multiPart.getErrors())
.isNotEmpty()
.allSatisfy(error ->
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.FileUploadFileCountLimitException")
);
}
@Test
public void actualSizeOfUploadedFilesCalculation() throws IOException {
// Test the actualSizeOfUploadedFiles method
String content = formFile("file1", "test1.csv", "data1234567890") + // 14 bytes + headers
formFile("file2", "test2.csv", "moredata") + // 8 bytes + headers
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then - should have uploaded files and calculate their total size
assertThat(multiPart.uploadedFiles).hasSize(2);
assertThat(multiPart.getFile("file1")).hasSize(1);
assertThat(multiPart.getFile("file2")).hasSize(1);
// Verify files have the expected content
assertThat(multiPart.getFile("file1")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo("data1234567890");
assertThat(multiPart.getFile("file2")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo("moredata");
}
@Test
public void createTemporaryFileMethod() throws Exception {
// Test the createTemporaryFile method directly
JakartaStreamMultiPartRequest streamMultiPart = new JakartaStreamMultiPartRequest();
Path testLocation = Paths.get(tempDir);
// when
File tempFile1 = streamMultiPart.createTemporaryFile("test.csv", testLocation);
File tempFile2 = streamMultiPart.createTemporaryFile("another.txt", testLocation);
// then
assertThat(tempFile1.getName()).startsWith("upload_");
assertThat(tempFile1.getName()).endsWith(".tmp");
assertThat(tempFile1.getParent()).isEqualTo(tempDir);
assertThat(tempFile2.getName()).startsWith("upload_");
assertThat(tempFile2.getName()).endsWith(".tmp");
assertThat(tempFile2.getParent()).isEqualTo(tempDir);
// Should be unique names
assertThat(tempFile1.getName()).isNotEqualTo(tempFile2.getName());
// Clean up
tempFile1.delete();
tempFile2.delete();
}
@Test
public void streamFileToDiskWithDifferentBufferSizes() throws IOException {
// Test streamFileToDisk with different buffer sizes
String largeContent = "x".repeat(5000); // Content larger than default buffer
String content = formFile("largefile", "large.csv", largeContent) +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when - use small buffer size to ensure multiple reads
multiPart.setBufferSize("100");
multiPart.parse(mockRequest, tempDir);
// then
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.getFile("largefile")).hasSize(1);
assertThat(multiPart.getFile("largefile")[0].getContent())
.asInstanceOf(InstanceOfAssertFactories.FILE)
.content()
.isEqualTo(largeContent);
}
@Test
public void exceedsMaxSizeOfFilesWithFileCleanup() throws IOException {
// Test the file deletion path when max size is exceeded
String content = formFile("file1", "test1.csv", "small") +
formFile("file2", "test2.csv", "this is a much larger file content that should exceed the limit") +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when - set very small max size
multiPart.setMaxSizeOfFiles("20");
multiPart.parse(mockRequest, tempDir);
// then - should have first file uploaded but error for second
assertThat(multiPart.uploadedFiles).hasSize(1);
assertThat(multiPart.getFile("file1")).hasSize(1);
assertThat(multiPart.getFile("file2")).isEmpty();
assertThat(multiPart.getErrors())
.isNotEmpty()
.anyMatch(error ->
error.getTextKey().equals("struts.messages.upload.error.FileUploadSizeException")
);
}
@Test
public void createUploadedFileWithVariousContentTypes() throws IOException {
// Test different content types and file names
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"textfile\"; filename=\"document.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
"Plain text content" +
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"jsonfile\"; filename=\"data.json\"" + endline +
"Content-Type: application/json" + endline +
endline +
"{\"key\": \"value\"}" +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.uploadedFiles).hasSize(2);
// Verify text file
assertThat(multiPart.getFile("textfile")).hasSize(1);
assertThat(multiPart.getFile("textfile")[0].getContentType()).isEqualTo("text/plain");
assertThat(multiPart.getFile("textfile")[0].getOriginalName()).isEqualTo("document.txt");
// Verify JSON file
assertThat(multiPart.getFile("jsonfile")).hasSize(1);
assertThat(multiPart.getFile("jsonfile")[0].getContentType()).isEqualTo("application/json");
assertThat(multiPart.getFile("jsonfile")[0].getOriginalName()).isEqualTo("data.json");
}
@Test
public void emptyFileNameFieldsAreSkipped() throws IOException {
// Test files with empty names are skipped
String content =
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"emptyfile\"; filename=\"\"" + endline +
"Content-Type: text/plain" + endline +
endline +
"This should be skipped" +
endline + "--" + boundary + endline +
"Content-Disposition: form-data; name=\"validfile\"; filename=\"valid.txt\"" + endline +
"Content-Type: text/plain" + endline +
endline +
"This should be processed" +
endline + "--" + boundary + "--";
mockRequest.setContent(content.getBytes(StandardCharsets.UTF_8));
// when
multiPart.parse(mockRequest, tempDir);
// then
assertThat(multiPart.getErrors()).isEmpty();
assertThat(multiPart.uploadedFiles).hasSize(1);
assertThat(multiPart.getFile("emptyfile")).isEmpty();
assertThat(multiPart.getFile("validfile")).hasSize(1);
}
}
@@ -18,21 +18,21 @@
*/
package org.apache.struts2.interceptor;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.locale.DefaultLocaleProvider;
import org.apache.struts2.ValidationAwareSupport;
import org.apache.struts2.mock.MockActionInvocation;
import org.apache.struts2.mock.MockActionProxy;
import org.apache.struts2.util.ClassLoaderUtil;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.ValidationAwareSupport;
import org.apache.struts2.action.UploadedFilesAware;
import org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest;
import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
import org.apache.struts2.dispatcher.multipart.StrutsUploadedFile;
import org.apache.struts2.dispatcher.multipart.UploadedFile;
import org.apache.struts2.locale.DefaultLocaleProvider;
import org.apache.struts2.mock.MockActionInvocation;
import org.apache.struts2.mock.MockActionProxy;
import org.apache.struts2.util.ClassLoaderUtil;
import org.assertj.core.util.Files;
import org.springframework.mock.web.MockHttpServletRequest;
@@ -40,7 +40,6 @@ import java.io.File;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
@@ -51,53 +50,6 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
private static final UploadedFile EMPTY_FILE = new UploadedFile() {
@Override
public Long length() {
return 0L;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getAbsolutePath() {
return null;
}
@Override
public File getContent() {
return Files.newTemporaryFile();
}
@Override
public String getOriginalName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public String getInputName() {
return null;
}
};
private MockHttpServletRequest request;
private ActionFileUploadInterceptor interceptor;
private File tempDir;
@@ -105,16 +57,16 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
private final String htmlContent = "<html><head></head><body>html content</body></html>";
private final String plainContent = "plain content";
private final String boundary = "simple boundary";
private final String endline = "\r\n";
private final String endLine = "\r\n";
public void testAcceptFileWithEmptyAllowedTypesAndExtensions() {
// when allowed type is empty
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename", "text/plain", "inputName");
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename", "text/plain", "inputName");
assertTrue(ok);
assertTrue(validation.getFieldErrors().isEmpty());
assertFalse(validation.hasErrors());
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
}
public void testAcceptFileWithoutEmptyTypes() {
@@ -122,39 +74,38 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
// when file is of allowed types
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName");
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
assertTrue(ok);
assertTrue(validation.getFieldErrors().isEmpty());
assertFalse(validation.hasErrors());
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
// when file is not of allowed types
validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName");
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/html", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
}
public void testAcceptFileWithWildcardContent() {
interceptor.setAllowedTypes("text/*");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName");
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
assertTrue(ok);
assertTrue(validation.getFieldErrors().isEmpty());
assertFalse(validation.hasErrors());
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
interceptor.setAllowedTypes("text/h*");
validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/plain", "inputName");
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/plain", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
}
public void testAcceptFileWithoutEmptyExtensions() {
@@ -162,48 +113,62 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
// when file is of allowed extensions
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.txt", "text/plain", "inputName");
boolean ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.txt", "text/plain", "inputName");
assertTrue(ok);
assertTrue(validation.getFieldErrors().isEmpty());
assertFalse(validation.hasErrors());
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
// when file is not of allowed extensions
validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, EMPTY_FILE, "filename.html", "text/html", "inputName");
boolean notOk = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.html", "text/html", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
//test with multiple extensions
interceptor.setAllowedExtensions(".txt,.lol");
validation = new ValidationAwareSupport();
ok = interceptor.acceptFile(validation, EMPTY_FILE, "filename.lol", "text/plain", "inputName");
ok = interceptor.acceptFile(validation, createTestFile(Files.newTemporaryFile()), "filename.lol", "text/plain", "inputName");
assertTrue(ok);
assertTrue(validation.getFieldErrors().isEmpty());
assertFalse(validation.hasErrors());
assertThat(ok).isTrue();
assertThat(validation.getFieldErrors()).isEmpty();
assertThat(validation.hasErrors()).isFalse();
}
public void testAcceptFileWithNoFile() {
ActionFileUploadInterceptor interceptor = new ActionFileUploadInterceptor();
interceptor.setContainer(container);
interceptor.setAllowedTypes("text/plain");
// when file is not of allowed types
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, null, "filename.html", "text/html", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
List<String> errors = validation.getFieldErrors().get("inputName");
assertEquals(1, errors.size());
String msg = errors.get(0);
assertTrue(msg.startsWith("Error uploading:"));
assertTrue(msg.indexOf("inputName") > 0);
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
assertThat(validation.getFieldErrors().get("inputName"))
.hasSize(1)
.first()
.asString()
.startsWith("Error uploading:")
.contains("inputName");
}
public void testAcceptFileWithNoContent() {
interceptor.setAllowedTypes("text/plain");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, createTestFile(null), "filename.html", "text/plain", "inputName");
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
assertThat(validation.getFieldErrors().get("inputName"))
.hasSize(1)
.first()
.asString()
.startsWith("Error uploading:")
.contains("inputName");
}
public void testAcceptFileWithMaxSize() throws Exception {
@@ -214,23 +179,18 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
URL url = ClassLoaderUtil.getResource("log4j2.xml", ActionFileUploadInterceptorTest.class);
File file = new File(new URI(url.toString()));
assertTrue("log4j2.xml should be in src/test folder", file.exists());
assertThat(file).exists();
UploadedFile uploadedFile = StrutsUploadedFile.Builder.create(file).withContentType("text/html").withOriginalName("filename").build();
boolean notOk = interceptor.acceptFile(validation, uploadedFile, "filename", "text/html", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
List<String> errors = validation.getFieldErrors().get("inputName");
assertEquals(1, errors.size());
String msg = errors.get(0);
// the error message should contain at least this test
assertThat(msg).contains(
"The file is too large to be uploaded",
"inputName",
"log4j2.xml",
"allowed mx size is 10"
);
assertThat(notOk).isFalse();
assertThat(validation.getFieldErrors()).isNotEmpty();
assertThat(validation.hasErrors()).isTrue();
assertThat(validation.getFieldErrors().get("inputName"))
.hasSize(1)
.first()
.asString()
.contains("The file is too large to be uploaded", "inputName", "log4j2.xml", "allowed mx size is 10");
}
public void testNoMultipartRequest() throws Exception {
@@ -246,11 +206,11 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
mai.setInvocationContext(ActionContext.getContext());
// if no multipart request it will bypass and execute it
assertEquals("NoMultipart", interceptor.intercept(mai));
assertThat(interceptor.intercept(mai)).isEqualTo("NoMultipart");
}
public void testInvalidContentTypeMultipartRequest() throws Exception {
request.setContentType("multipart/form-data"); // not a multipart contentype
request.setContentType("multipart/form-data"); // not a multipart Content-Type
request.setMethod("post");
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
@@ -263,7 +223,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
interceptor.intercept(mai);
assertTrue(action.hasErrors());
assertThat(action.hasErrors()).isTrue();
}
public void testNoContentMultipartRequest() throws Exception {
@@ -284,7 +244,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
interceptor.intercept(mai);
assertTrue(action.hasErrors());
assertThat(action.hasErrors()).isTrue();
}
public void testSuccessUploadOfATextFileMultipartRequest() throws Exception {
@@ -292,7 +252,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
// inspired by the unit tests for Jakarta Commons FileUpload
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
@@ -313,14 +273,13 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
interceptor.intercept(mai);
assertFalse(action.hasErrors());
assertThat(action.hasErrors()).isFalse();
List<UploadedFile> files = action.getUploadFiles();
assertNotNull(files);
assertEquals(1, files.size());
assertEquals("text/html", files.get(0).getContentType());
assertNotNull("deleteme.txt", files.get(0).getOriginalName());
assertThat(files).isNotNull().hasSize(1);
assertThat(files.get(0).getContentType()).isEqualTo("text/html");
assertThat(files.get(0).getOriginalName()).isEqualTo("deleteme.txt");
}
/**
@@ -334,46 +293,10 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
String content = encodeTextFile("test.html", "text/plain", plainContent) +
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
endline + "--" + boundary + "--";
endLine + "--" + boundary + "--";
request.setContent(content.getBytes());
assertTrue(JakartaServletDiskFileUpload.isMultipartContent(request));
MyFileUploadAction action = new MyFileUploadAction();
container.inject(action);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withServletRequest(createMultipartRequestMaxSize(2000));
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
List<UploadedFile> files = action.getUploadFiles();
assertNotNull(files);
assertEquals("files accepted ", 2, files.size());
assertEquals("text/html", files.get(0).getContentType());
assertNotNull("test1.html", files.get(0).getOriginalName());
}
public void testUnacceptedNumberOfFiles() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("POST");
request.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
String content = encodeTextFile("test.html", "text/plain", plainContent) +
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
encodeTextFile("test3.html", "text/html", htmlContent) +
endline +
"--" +
boundary +
"--" +
endline;
request.setContent(content.getBytes());
assertTrue(JakartaServletFileUpload.isMultipartContent(request));
assertThat(JakartaServletDiskFileUpload.isMultipartContent(request)).isTrue();
MyFileUploadAction action = new MyFileUploadAction();
container.inject(action);
@@ -386,12 +309,48 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
assertNull(action.getUploadFiles());
assertEquals(1, action.getActionErrors().size());
assertEquals(
"Request exceeded allowed number of files! Permitted number of files is: 3!",
action.getActionErrors().iterator().next()
);
List<UploadedFile> files = action.getUploadFiles();
assertThat(files).isNotNull().hasSize(2);
assertThat(files.get(0).getContentType()).isEqualTo("text/html");
assertThat(files.get(0).getOriginalName()).isEqualTo("test1.html");
assertThat(files.get(1).getContentType()).isEqualTo("text/html");
assertThat(files.get(1).getOriginalName()).isEqualTo("test2.html");
}
public void testUnacceptedNumberOfFiles() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("POST");
request.addHeader("Content-type", "multipart/form-data; boundary=" + boundary);
String content = encodeTextFile("test.html", "text/plain", plainContent) +
encodeTextFile("test1.html", "text/html", htmlContent) +
encodeTextFile("test2.html", "text/html", htmlContent) +
encodeTextFile("test3.html", "text/html", htmlContent) +
endLine +
"--" +
boundary +
"--" +
endLine;
request.setContent(content.getBytes());
assertThat(JakartaServletFileUpload.isMultipartContent(request)).isTrue();
MyFileUploadAction action = new MyFileUploadAction();
container.inject(action);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext().withServletRequest(createMultipartRequestMaxFiles());
interceptor.setAllowedTypes("text/html");
interceptor.intercept(mai);
assertThat(action.getUploadFiles()).isNull();
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.isEqualTo("Request exceeded allowed number of files! Permitted number of files is: 3!");
}
public void testMultipartRequestMaxFileSize() throws Exception {
@@ -399,7 +358,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
// inspired by the unit tests for Jakarta Commons FileUpload
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
@@ -421,15 +380,12 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
interceptor.intercept(mai);
assertTrue(action.hasActionErrors());
assertThat(action.hasActionErrors()).isTrue();
Collection<String> errors = action.getActionErrors();
assertEquals(1, errors.size());
String msg = errors.iterator().next();
// FIXME: the expected size is 40 - length of the string
assertEquals(
"File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 11!",
msg);
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.isEqualTo("File deleteme.txt assigned to file exceeded allowed size limit! Max size allowed is: 10 but file was: 11!");
}
public void testMultipartRequestMaxStringLength() throws Exception {
@@ -437,7 +393,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
// inspired by the unit tests for Jakarta Commons FileUpload
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
@@ -467,14 +423,12 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
interceptor.intercept(mai);
assertTrue(action.hasActionErrors());
assertThat(action.hasActionErrors()).isTrue();
Collection<String> errors = action.getActionErrors();
assertEquals(1, errors.size());
String msg = errors.iterator().next();
assertEquals(
"The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!",
msg);
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.isEqualTo("The request parameter \"normalFormField2\" was too long. Max length allowed is 20, but found 27!");
}
public void testMultipartRequestLocalizedError() throws Exception {
@@ -482,7 +436,6 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
String content = ("""
-----1234\r
Content-Disposition: form-data; name="file"; filename="deleteme.txt"\r
@@ -505,84 +458,24 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
interceptor.intercept(mai);
assertTrue(action.hasActionErrors());
assertThat(action.hasActionErrors()).isTrue();
Collection<String> errors = action.getActionErrors();
assertEquals(1, errors.size());
String msg = errors.iterator().next();
// the error message should contain at least this test
assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte Größe"));
}
public void testUnacceptedFieldName() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
String content = ("-----1234\r\n" +
"Content-Disposition: form-data; name=\"top.file\"; filename=\"deleteme.txt\"\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"Unit test of ActionFileUploadInterceptor" +
"\r\n" +
"-----1234--\r\n");
request.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxSize(2000));
interceptor.intercept(mai);
assertFalse(action.hasActionErrors());
assertNull(action.getUploadFiles());
}
public void testUnacceptedFileName() throws Exception {
request.setCharacterEncoding(StandardCharsets.UTF_8.name());
request.setMethod("post");
request.addHeader("Content-type", "multipart/form-data; boundary=---1234");
// inspired by the unit tests for jakarta commons fileupload
String content = ("-----1234\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"../deleteme.txt\"\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"Unit test of ActionFileUploadInterceptor" +
"\r\n" +
"-----1234--\r\n");
request.setContent(content.getBytes(StandardCharsets.US_ASCII));
MyFileUploadAction action = container.inject(MyFileUploadAction.class);
MockActionInvocation mai = new MockActionInvocation();
mai.setAction(action);
mai.setResultCode("success");
mai.setInvocationContext(ActionContext.getContext());
ActionContext.getContext()
.withServletRequest(createMultipartRequestMaxSize(2000));
interceptor.intercept(mai);
assertFalse(action.hasActionErrors());
assertNull(action.getUploadFiles());
assertThat(action.getActionErrors())
.hasSize(1)
.first()
.asString()
.startsWith("Der Request übertraf die maximal erlaubte Größe");
}
private String encodeTextFile(String filename, String contentType, String content) {
return endline +
return endLine +
"--" + boundary +
endline +
"Content-Disposition: form-data; name=\"" + "file" + "\"; filename=\"" + filename + "\"" +
endline +
endLine +
"Content-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"" +
endLine +
"Content-Type: " + contentType +
endline +
endline +
endLine +
endLine +
content;
}
@@ -628,6 +521,55 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
super.tearDown();
}
private UploadedFile createTestFile(File content) {
return new UploadedFile() {
@Override
public Long length() {
return 0L;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getAbsolutePath() {
return null;
}
@Override
public File getContent() {
return content;
}
@Override
public String getOriginalName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public String getInputName() {
return null;
}
};
}
public static class MyFileUploadAction extends ActionSupport implements UploadedFilesAware {
private List<UploadedFile> uploadedFiles;
@@ -312,7 +312,7 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
public static class CustomDefaultCspSettings extends DefaultCspSettings {
@Override
protected String createPolicyFormat(HttpServletRequest request) {
protected String createPolicyFormat(String nonceValue) {
return "foo";
}
}
@@ -20,16 +20,19 @@ package org.apache.struts2.interceptor;
import com.mockobjects.dynamic.ConstraintMatcher;
import com.mockobjects.dynamic.Mock;
import org.apache.struts2.action.Action;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ActionSupport;
import org.apache.struts2.ModelDriven;
import org.apache.struts2.XWorkTestCase;
import org.apache.struts2.action.Action;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.util.ValueStack;
import java.util.Date;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* @author $Author$
@@ -40,6 +43,7 @@ public class ModelDrivenInterceptorTest extends XWorkTestCase {
Action action;
Mock mockActionInvocation;
ModelDrivenInterceptor modelDrivenInterceptor;
ThreadAllowlist threadAllowlist;
Object model;
PreResultListener preResultListener;
ValueStack stack;
@@ -55,6 +59,7 @@ public class ModelDrivenInterceptorTest extends XWorkTestCase {
Object topOfStack = stack.pop();
assertEquals("our model should be on the top of the stack", model, topOfStack);
verify(threadAllowlist).allowClassHierarchy(model.getClass());
}
private void setupRefreshModelBeforeResult() {
@@ -167,6 +172,8 @@ public class ModelDrivenInterceptorTest extends XWorkTestCase {
super.setUp();
mockActionInvocation = new Mock(ActionInvocation.class);
modelDrivenInterceptor = new ModelDrivenInterceptor();
threadAllowlist = mock(ThreadAllowlist.class);
modelDrivenInterceptor.setThreadAllowlist(threadAllowlist);
stack = ActionContext.getContext().getValueStack();
model = new Date(); // any object will do
}
@@ -18,18 +18,26 @@
*/
package org.apache.struts2.interceptor.parameter;
import org.aopalliance.intercept.Joinpoint;
import org.aopalliance.intercept.MethodInterceptor;
import org.apache.commons.lang3.ClassUtils;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ModelDriven;
import org.apache.struts2.StubValueStack;
import org.apache.struts2.security.AcceptedPatternsChecker;
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
import org.apache.commons.lang3.ClassUtils;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
import org.apache.struts2.ognl.DefaultOgnlBeanInfoCacheFactory;
import org.apache.struts2.ognl.DefaultOgnlExpressionCacheFactory;
import org.apache.struts2.ognl.OgnlUtil;
import org.apache.struts2.ognl.StrutsOgnlGuard;
import org.apache.struts2.ognl.ThreadAllowlist;
import org.apache.struts2.security.AcceptedPatternsChecker.IsAccepted;
import org.apache.struts2.security.ExcludedPatternsChecker.IsExcluded;
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;
import java.util.HashMap;
import java.util.HashSet;
@@ -37,6 +45,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.LRU;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
@@ -56,9 +65,15 @@ public class StrutsParameterAnnotationTest {
threadAllowlist = new ThreadAllowlist();
parametersInterceptor.setThreadAllowlist(threadAllowlist);
var ognlUtil = new OgnlUtil(
new DefaultOgnlExpressionCacheFactory<>(String.valueOf(1000), LRU.toString()),
new DefaultOgnlBeanInfoCacheFactory<>(String.valueOf(1000), LRU.toString()),
new StrutsOgnlGuard());
parametersInterceptor.setOgnlUtil(ognlUtil);
NotExcludedAcceptedPatternsChecker checker = mock(NotExcludedAcceptedPatternsChecker.class);
when(checker.isAccepted(anyString())).thenReturn(AcceptedPatternsChecker.IsAccepted.yes(""));
when(checker.isExcluded(anyString())).thenReturn(NotExcludedAcceptedPatternsChecker.IsExcluded.no(new HashSet<>()));
when(checker.isAccepted(anyString())).thenReturn(IsAccepted.yes(""));
when(checker.isExcluded(anyString())).thenReturn(IsExcluded.no(Set.of()));
parametersInterceptor.setAcceptedPatterns(checker);
parametersInterceptor.setExcludedPatterns(checker);
}
@@ -94,174 +109,267 @@ public class StrutsParameterAnnotationTest {
return set;
}
/**
* Private String field cannot be injected even when annotated.
*/
@Test
public void privateStrAnnotated() {
testParameter(new FieldAction(), "privateStr", false);
}
/**
* Public String field can be injected when annotated.
*/
@Test
public void publicStrAnnotated() {
testParameter(new FieldAction(), "publicStr", true);
assertThat(threadAllowlist.getAllowlist()).isEmpty();
}
/**
* Public String field cannot be injected when not annotated.
*/
@Test
public void publicStrNotAnnotated() {
testParameter(new FieldAction(), "publicStrNotAnnotated", false);
}
/**
* Private Pojo field cannot be injected even when annotated with the appropriate depth.
*/
@Test
public void privatePojoAnnotated() {
testParameter(new FieldAction(), "privatePojo.key", false);
}
/**
* Public Pojo field cannot be injected when annotated with depth zero.
*/
@Test
public void publicPojoDepthZero() {
testParameter(new FieldAction(), "publicPojoDepthZero.key", false);
}
/**
* Public Pojo field can be injected when annotated with depth one.
*/
@Test
public void publicPojoDepthOne() {
testParameter(new FieldAction(), "publicPojoDepthOne.key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public Pojo field can be injected when annotated with depth one, using the square bracket syntax.
*/
@Test
public void publicPojoDepthOne_sqrBracket() {
testParameter(new FieldAction(), "publicPojoDepthOne['key']", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public Pojo field can be injected when annotated with depth one, using the bracket syntax.
*/
@Test
public void publicPojoDepthOne_bracket() {
testParameter(new FieldAction(), "publicPojoDepthOne('key')", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
@Test
public void publicNestedPojoDepthOne() {
testParameter(new FieldAction(), "publicPojoDepthOne.key.key", false);
}
/**
* Public Pojo field can be injected when annotated with a depth greater than required.
*/
@Test
public void publicPojoDepthTwo() {
testParameter(new FieldAction(), "publicPojoDepthTwo.key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public Pojo field cannot be injected two levels when only annotated with depth one.
*/
@Test
public void publicNestedPojoDepthOne() {
testParameter(new FieldAction(), "publicPojoDepthOne.key.key", false);
}
/**
* Public Pojo field can be injected two levels when annotated with depth two.
*/
@Test
public void publicNestedPojoDepthTwo() {
testParameter(new FieldAction(), "publicPojoDepthTwo.key.key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public Pojo field can be injected two levels when annotated with depth two, using the square bracket syntax.
*/
@Test
public void publicNestedPojoDepthTwo_sqrBracket() {
testParameter(new FieldAction(), "publicPojoDepthTwo['key']['key']", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public Pojo field can be injected two levels when annotated with depth two, using the bracket syntax.
*/
@Test
public void publicNestedPojoDepthTwo_bracket() {
testParameter(new FieldAction(), "publicPojoDepthTwo('key')('key')", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Private String setting method cannot be injected even when annotated.
*/
@Test
public void privateStrAnnotatedMethod() {
testParameter(new MethodAction(), "privateStr", false);
}
/**
* Public String setting method can be injected when annotated.
*/
@Test
public void publicStrAnnotatedMethod() {
testParameter(new MethodAction(), "publicStr", true);
assertThat(threadAllowlist.getAllowlist()).isEmpty();
}
/**
* Public String setting method cannot be injected when not annotated.
*/
@Test
public void publicStrNotAnnotatedMethod() {
testParameter(new MethodAction(), "publicStrNotAnnotated", false);
}
/**
* Private Pojo returning method cannot be injected even when annotated with the appropriate depth.
*/
@Test
public void privatePojoAnnotatedMethod() {
testParameter(new MethodAction(), "privatePojo.key", false);
}
/**
* Public Pojo returning method cannot be injected when annotated with depth zero.
*/
@Test
public void publicPojoDepthZeroMethod() {
testParameter(new MethodAction(), "publicPojoDepthZero.key", false);
}
/**
* Public Pojo returning method can be injected when annotated with depth one.
*/
@Test
public void publicPojoDepthOneMethod() {
testParameter(new MethodAction(), "publicPojoDepthOne.key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public Pojo returning method cannot be injected two levels when only annotated with depth one.
*/
@Test
public void publicNestedPojoDepthOneMethod() {
testParameter(new MethodAction(), "publicPojoDepthOne.key.key", false);
}
/**
* Public Pojo returning method can be injected when annotated with a depth greater than required.
*/
@Test
public void publicPojoDepthTwoMethod() {
testParameter(new MethodAction(), "publicPojoDepthTwo.key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public Pojo returning method can be injected two levels when annotated with depth two.
*/
@Test
public void publicNestedPojoDepthTwoMethod() {
testParameter(new MethodAction(), "publicPojoDepthTwo.key.key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Pojo.class));
}
/**
* Public list of Pojo field cannot be injected when annotated with depth one.
*/
@Test
public void publicPojoListDepthOne() {
testParameter(new FieldAction(), "publicPojoListDepthOne[0].key", false);
}
/**
* Public list of Pojo field can be injected when annotated with depth two.
*/
@Test
public void publicPojoListDepthTwo() {
testParameter(new FieldAction(), "publicPojoListDepthTwo[0].key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(List.class, Pojo.class));
}
@Test
public void publicPojoMapDepthTwo() {
testParameter(new FieldAction(), "publicPojoMapDepthTwo['a'].key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Map.class, String.class, Pojo.class));
}
/**
* Public list of Pojo returning method cannot be injected when annotated with depth one.
*/
@Test
public void publicPojoListDepthOneMethod() {
testParameter(new MethodAction(), "publicPojoListDepthOne[0].key", false);
}
/**
* Public list of Pojo returning method can be injected when annotated with depth two.
*/
@Test
public void publicPojoListDepthTwoMethod() {
testParameter(new MethodAction(), "publicPojoListDepthTwo[0].key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(List.class, Pojo.class));
}
/**
* Public map of Pojo field can be injected when annotated with depth two.
*/
@Test
public void publicPojoMapDepthTwo() {
testParameter(new FieldAction(), "publicPojoMapDepthTwo['a'].key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Map.class, String.class, Pojo.class));
}
/**
* Public map of Pojo returning method can be injected when annotated with depth two.
*/
@Test
public void publicPojoMapDepthTwoMethod() {
testParameter(new MethodAction(), "publicPojoMapDepthTwo['a'].key", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Map.class, String.class, Pojo.class));
}
/**
* Public String field can be injected even when not annotated, if transition mode is enabled.
*/
@Test
public void publicStrNotAnnotated_transitionMode() {
parametersInterceptor.setRequireAnnotationsTransitionMode(Boolean.TRUE.toString());
testParameter(new FieldAction(), "publicStrNotAnnotated", true);
}
/**
* Public String setting method can be injected even when not annotated, if transition mode is enabled.
*/
@Test
public void publicStrNotAnnotatedMethod_transitionMode() {
parametersInterceptor.setRequireAnnotationsTransitionMode(Boolean.TRUE.toString());
testParameter(new MethodAction(), "publicStrNotAnnotated", true);
}
/**
* Models of ModelDriven actions can be injected without any annotations on the Action.
*/
@Test
public void publicModelPojo() {
var action = new ModelAction();
@@ -273,10 +381,29 @@ public class StrutsParameterAnnotationTest {
testParameter(action, "name", true);
testParameter(action, "name.nested", true);
assertThat(threadAllowlist.getAllowlist()).containsExactlyInAnyOrderElementsOf(getParentClasses(Object.class, Pojo.class));
}
static class FieldAction {
/**
* Models of ModelDriven actions can be injected without any annotations on the Action, even when the Action is
* proxied.
*/
@Test
public void publicModelPojo_proxied() {
var proxyFactory = new ProxyFactory(new ModelAction());
proxyFactory.setProxyTargetClass(true);
proxyFactory.addAdvice((MethodInterceptor) Joinpoint::proceed);
var proxiedAction = (ModelAction) proxyFactory.getProxy();
// Emulate ModelDrivenInterceptor running previously
var valueStack = new StubValueStack();
valueStack.push(proxiedAction.getModel());
ActionContext.of().withValueStack(valueStack).bind();
testParameter(proxiedAction, "name", true);
testParameter(proxiedAction, "name.nested", true);
}
public static class FieldAction {
@StrutsParameter
private String privateStr;
@@ -307,7 +434,7 @@ public class StrutsParameterAnnotationTest {
public Map<String, Pojo> publicPojoMapDepthTwo;
}
static class MethodAction {
public static class MethodAction {
@StrutsParameter
private void setPrivateStr(String str) {
@@ -360,7 +487,7 @@ public class StrutsParameterAnnotationTest {
}
}
static class ModelAction implements ModelDriven<Pojo> {
public static class ModelAction implements ModelDriven<Pojo> {
@Override
public Pojo getModel() {
@@ -368,6 +495,6 @@ public class StrutsParameterAnnotationTest {
}
}
static class Pojo {
public static class Pojo {
}
}
@@ -18,8 +18,18 @@
*/
package org.apache.struts2.ognl;
import ognl.InappropriateExpressionException;
import ognl.MethodFailedException;
import ognl.NoSuchPropertyException;
import ognl.NullHandler;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.SimpleNode;
import org.apache.struts2.ActionContext;
import org.apache.struts2.text.StubTextProvider;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.StubValueStack;
import org.apache.struts2.XWorkTestCase;
import org.apache.struts2.config.ConfigurationException;
@@ -30,6 +40,7 @@ import org.apache.struts2.ognl.accessor.CompoundRootAccessor;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.test.StubConfigurationProvider;
import org.apache.struts2.test.User;
import org.apache.struts2.text.StubTextProvider;
import org.apache.struts2.util.Bar;
import org.apache.struts2.util.CompoundRoot;
import org.apache.struts2.util.Foo;
@@ -37,20 +48,11 @@ import org.apache.struts2.util.Owner;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.util.location.LocatableProperties;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.InappropriateExpressionException;
import ognl.MethodFailedException;
import ognl.NoSuchPropertyException;
import ognl.NullHandler;
import ognl.Ognl;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import ognl.SimpleNode;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.mockito.MockedStatic;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.lang.reflect.Method;
import java.text.DateFormat;
import java.util.ArrayList;
@@ -66,6 +68,7 @@ import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.BASIC;
import static org.apache.struts2.ognl.OgnlCacheFactory.CacheType.LRU;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mockStatic;
public class OgnlUtilTest extends XWorkTestCase {
@@ -432,6 +435,17 @@ public class OgnlUtilTest extends XWorkTestCase {
assertNotSame("BeanInfo dropped from LRU cache is the same as newly added ?", beanInfo1_1, beanInfo1_4);
}
/**
* Ensure any {@link IntrospectionException} thrown by {@link Introspector} are propagated as is.
*/
public void testBeanInfoCacheExceptionHandling() {
try (MockedStatic<Introspector> introspector = mockStatic(Introspector.class)) {
var exception = new IntrospectionException("Test Exception");
introspector.when(() -> Introspector.getBeanInfo(TestBean1.class, Object.class)).thenThrow(exception);
assertSame(exception, assertThrows(IntrospectionException.class, () -> ognlUtil.getBeanInfo(new TestBean1())));
}
}
public void testClearRuntimeCache() {
// Confirm that no exceptions or failures arise when calling the convenience global clear method.
OgnlUtil.clearRuntimeCache();
@@ -18,17 +18,26 @@
*/
package org.apache.struts2.ognl;
import ognl.OgnlException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.struts2.SimpleAction;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.TestBean;
import org.apache.struts2.text.TextProvider;
import org.apache.struts2.XWorkTestCase;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.config.DefaultPropertiesProvider;
import org.apache.struts2.conversion.impl.ConversionData;
import org.apache.struts2.conversion.impl.XWorkConverter;
import org.apache.struts2.inject.ContainerBuilder;
import org.apache.struts2.ognl.accessor.RootAccessor;
import org.apache.struts2.test.StubConfigurationProvider;
import org.apache.struts2.test.TestBean2;
import org.apache.struts2.text.TextProvider;
import org.apache.struts2.util.Bar;
import org.apache.struts2.util.BarJunior;
import org.apache.struts2.util.Cat;
@@ -37,15 +46,6 @@ import org.apache.struts2.util.Foo;
import org.apache.struts2.util.ValueStackFactory;
import org.apache.struts2.util.location.LocatableProperties;
import org.apache.struts2.util.reflection.ReflectionContextState;
import ognl.OgnlException;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsException;
import org.apache.struts2.config.DefaultPropertiesProvider;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -1234,6 +1234,33 @@ public class OgnlValueStackTest extends XWorkTestCase {
assertNull("accessed private field (result not null) ?", accessedValue);
}
public void testFindValueWithConstructorAndProxyChecks() {
loadButSet(Map.of(
StrutsConstants.STRUTS_DISALLOW_PROXY_OBJECT_ACCESS, Boolean.TRUE.toString(),
StrutsConstants.STRUTS_DISALLOW_PROXY_MEMBER_ACCESS, Boolean.TRUE.toString()));
refreshContainerFields();
String value = "test";
String ognlResult = (String) vs.findValue(
"new org.apache.struts2.ognl.OgnlValueStackTest$ValueHolder('" + value + "').value", String.class);
assertEquals(value, ognlResult);
}
@SuppressWarnings({"unused", "ClassCanBeRecord"})
public static class ValueHolder {
// See testFindValueWithConstructorAndProxyChecks
private final String value;
public ValueHolder(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
static class BadJavaBean {
private int count;
private int count2;
@@ -18,12 +18,12 @@
*/
package org.apache.struts2.ognl;
import ognl.MemberAccess;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.struts2.TestBean;
import org.apache.struts2.config.ConfigurationException;
import org.apache.struts2.test.TestBean2;
import org.apache.struts2.util.Foo;
import ognl.MemberAccess;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.junit.Before;
@@ -18,7 +18,6 @@
*/
package org.apache.struts2.result;
import jakarta.servlet.RequestDispatcher;
import org.apache.struts2.ActionContext;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.StrutsStatics;
@@ -39,30 +38,38 @@ public class ServletDispatcherResultTest extends StrutsInternalTestCase implemen
ServletDispatcherResult view = new ServletDispatcherResult();
view.setLocation("foo.jsp");
request.setRequestURI("/app/namespace/my.action");
request.setContextPath("/app");
request.setServletPath("/namespace/my.action");
request.setPathInfo(null);
request.setQueryString("a=1&b=2");
request.setAttribute("struts.actiontag.invocation", null);
request.setAttribute("jakarta.servlet.include.servlet_path", null);
request.setRequestURI("foo.jsp");
response.setCommitted(Boolean.FALSE);
view.execute(invocation);
assertEquals("foo.jsp", response.getForwardedUrl());
assertEquals("foo.jsp", request.getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH));
}
public void testInclude() throws Exception {
ServletDispatcherResult view = new ServletDispatcherResult();
view.setLocation("foo.jsp");
request.setRequestURI("/app/namespace/my.action");
request.setContextPath("/app");
request.setServletPath("/namespace/my.action");
request.setPathInfo(null);
request.setQueryString("a=1&b=2");
request.setAttribute("struts.actiontag.invocation", null);
response.setCommitted(Boolean.TRUE);
request.setRequestURI("foo.jsp");
view.execute(invocation);
assertEquals("foo.jsp", response.getIncludedUrl());
assertEquals("foo.jsp", request.getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH));
}
public void testWithParameter() throws Exception {
@@ -76,7 +83,6 @@ public class ServletDispatcherResultTest extends StrutsInternalTestCase implemen
// See https://issues.apache.org/jira/browse/WW-5486
assertEquals("1", stack.findString("#parameters.bar"));
assertEquals("foo.jsp", request.getAttribute(RequestDispatcher.FORWARD_SERVLET_PATH));
}
@Override
@@ -93,7 +93,7 @@ public class DefaultExcludedPatternsCheckerTest extends XWorkTestCase {
public void testDefaultExcludePatterns() throws Exception {
// given
List<String> prefixes = Arrays.asList("#[0].%s", "[0].%s", "top.%s", "%{[0].%s}", "%{#[0].%s}", "%{top.%s}", "%{#top.%s}", "%{#%s}", "%{%s}", "#%s", "top.param", "%{top.request}", "#top.param");
List<String> prefixes = Arrays.asList("#[0].%s", "[0].%s", "top.%s", "%{[0].%s}", "%{#[0].%s}", "%{top.%s}", "%{#top.%s}", "%{#%s}", "%{%s}", "#%s");
List<String> inners = Arrays.asList("servletRequest", "servletResponse", "servletContext", "application", "session", "struts", "request", "response", "dojo", "parameters");
List<String> suffixes = Arrays.asList("['test']", "[\"test\"]", ".test");
+2 -1
View File
@@ -24,7 +24,8 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
<artifactId>struts2-jakarta</artifactId>
<packaging>pom</packaging>
+2 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jakarta</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-velocity-tools-jsp-jakarta</artifactId>
<packaging>jar</packaging>
@@ -41,7 +41,7 @@
<plugin>
<groupId>org.eclipse.transformer</groupId>
<artifactId>transformer-maven-plugin</artifactId>
<version>0.5.0</version>
<version>1.0.0</version>
<extensions>true</extensions>
<configuration>
<rules>
+2 -2
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-jakarta</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-velocity-tools-view-jakarta</artifactId>
<packaging>jar</packaging>
@@ -41,7 +41,7 @@
<plugin>
<groupId>org.eclipse.transformer</groupId>
<artifactId>transformer-maven-plugin</artifactId>
<version>0.5.0</version>
<version>1.0.0</version>
<extensions>true</extensions>
<configuration>
<rules>
+305
View File
@@ -0,0 +1,305 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/*
* 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.
*/
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-bom</artifactId>
<version>7.1.0</version>
<relativePath>../bom/pom.xml</relativePath>
</parent>
<artifactId>struts2-parent</artifactId>
<packaging>pom</packaging>
<name>Struts Parent POM</name>
<description>Apache Struts</description>
<properties>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts-annotations</artifactId>
<version>2.0</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>${freemarker.version}</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.2.2</version>
</dependency>
<!-- Velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity.tools</groupId>
<artifactId>velocity-tools-generic</artifactId>
<version>${velocity-tools.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-velocity-tools-view-jakarta</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-velocity-tools-jsp-jakarta</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ognl</groupId>
<artifactId>ognl</artifactId>
<version>${ognl.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-bom</artifactId>
<version>${jakarta-ee.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-bom</artifactId>
<version>${jaxb-impl.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>5.0.0-M1</version>
<scope>test</scope>
</dependency>
<!-- Commons -->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.3.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-fileupload2-jakarta-servlet6</artifactId>
<version>2.0.0-M4</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.18.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jci-fam</artifactId>
<version>1.1</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.28.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-digester3</artifactId>
<version>3.2</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
<exclusion>
<groupId>asm</groupId>
<artifactId>asm</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mockobjects</groupId>
<artifactId>mockobjects-core</artifactId>
<version>0.09</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>4.16.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jmock</groupId>
<artifactId>jmock</artifactId>
<version>1.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte-buddy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>${byte-buddy.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-bom</artifactId>
<version>${log4j2.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.21</version>
<exclusions>
<exclusion>
<groupId>io.github.x-stream</groupId>
<artifactId>mxparser</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-async-plugin</artifactId>
+5 -12
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -37,11 +37,9 @@
</properties>
<dependencies>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
@@ -55,29 +53,24 @@
<artifactId>jakarta.el</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
<groupId>jakarta.activation</groupId>
<artifactId>jakarta.activation-api</artifactId>
<scope>test</scope>
</dependency>
+12 -11
View File
@@ -25,7 +25,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-cdi-plugin</artifactId>
@@ -33,7 +33,6 @@
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
@@ -43,29 +42,31 @@
<dependency>
<groupId>org.jboss.weld</groupId>
<artifactId>weld-core-impl</artifactId>
<scope>provided</scope>
<version>${weld.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se-core</artifactId>
<version>${weld.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<!-- org.springframework.mock.jndi.SimpleNamingContextBuilder removed from newer version -->
<version>4.3.0.RELEASE</version>
<groupId>com.github.h-thurow</groupId>
<artifactId>simple-jndi</artifactId>
<version>0.25.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
@@ -21,11 +21,14 @@ package org.apache.struts2.cdi;
import org.jboss.weld.bootstrap.api.helpers.RegistrySingletonProvider;
import org.jboss.weld.environment.se.Weld;
import org.jboss.weld.environment.se.WeldContainer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
import jakarta.enterprise.inject.spi.InjectionTarget;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
@@ -33,14 +36,28 @@ import static org.junit.Assert.assertTrue;
public class CdiObjectFactoryTest {
private static final String SHARED_JNDI = "org.osjava.sj.jndi.shared";
private static InitialContext context;
private static WeldContainer container;
@BeforeClass
public static void setup() throws Exception {
Weld weld = new Weld().containerId(RegistrySingletonProvider.STATIC_INSTANCE);
WeldContainer container = weld.initialize();
container = new Weld().containerId(RegistrySingletonProvider.STATIC_INSTANCE).initialize();
SimpleNamingContextBuilder builder = new SimpleNamingContextBuilder();
builder.activate();
builder.bind(CdiObjectFactory.CDI_JNDIKEY_BEANMANAGER_COMP, container.getBeanManager());
System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.SimpleContextFactory");
System.setProperty(SHARED_JNDI, "true");
context = new InitialContext();
context.bind(CdiObjectFactory.CDI_JNDIKEY_BEANMANAGER_COMP, container.getBeanManager());
}
@AfterClass
public static void tearDown() throws NamingException {
container.shutdown();
context.unbind(CdiObjectFactory.CDI_JNDIKEY_BEANMANAGER_COMP);
context.close();
System.clearProperty(Context.INITIAL_CONTEXT_FACTORY);
System.clearProperty(SHARED_JNDI);
}
@Test
@@ -50,16 +67,16 @@ public class CdiObjectFactoryTest {
@Test
public void testGetBean() throws Exception {
final CdiObjectFactory cdiObjectFactory = new CdiObjectFactory();
FooConsumer fooConsumer = (FooConsumer) cdiObjectFactory.buildBean(FooConsumer.class.getCanonicalName(), null, false);
var cdiObjectFactory = new CdiObjectFactory();
var fooConsumer = (FooConsumer) cdiObjectFactory.buildBean(FooConsumer.class.getCanonicalName(), null, false);
assertNotNull(fooConsumer);
assertNotNull(fooConsumer.fooService);
}
@Test
public void testGetInjectionTarget() {
final CdiObjectFactory cdiObjectFactory = new CdiObjectFactory();
final InjectionTarget<?> injectionTarget = cdiObjectFactory.getInjectionTarget(FooConsumer.class);
var cdiObjectFactory = new CdiObjectFactory();
InjectionTarget<?> injectionTarget = cdiObjectFactory.getInjectionTarget(FooConsumer.class);
assertNotNull(injectionTarget);
assertTrue(cdiObjectFactory.injectionTargetCache.containsKey(FooConsumer.class));
assertSame(cdiObjectFactory.getInjectionTarget(FooConsumer.class), injectionTarget);
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-config-browser-plugin</artifactId>
@@ -25,6 +25,7 @@ import org.apache.struts2.util.reflection.ReflectionProvider;
import org.apache.struts2.validator.Validator;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionContext;
import org.apache.struts2.interceptor.parameter.StrutsParameter;
import java.beans.BeanInfo;
@@ -49,19 +50,13 @@ public class ShowValidatorAction extends ListValidatorsAction {
private Set<PropertyInfo> properties = Collections.emptySet();
private int selected = 0;
ReflectionProvider reflectionProvider;
ReflectionContextFactory reflectionContextFactory;
private ReflectionProvider reflectionProvider;
@Inject
public void setReflectionProvider(ReflectionProvider prov) {
this.reflectionProvider = prov;
}
@Inject
public void setReflectionContextFactory(ReflectionContextFactory fac) {
this.reflectionContextFactory = fac;
}
public int getSelected() {
return selected;
}
@@ -85,7 +80,6 @@ public class ShowValidatorAction extends ListValidatorsAction {
Validator validator = getSelectedValidator();
properties = new TreeSet<>();
try {
Map<String, Object> context = reflectionContextFactory.createDefaultContext(validator);
BeanInfo beanInfoFrom;
try {
beanInfoFrom = Introspector.getBeanInfo(validator.getClass(), Object.class);
@@ -97,6 +91,7 @@ public class ShowValidatorAction extends ListValidatorsAction {
PropertyDescriptor[] pds = beanInfoFrom.getPropertyDescriptors();
Map<String, Object> context = ActionContext.getContext().getContextMap();
for (PropertyDescriptor pd : pds) {
String name = pd.getName();
Object value = null;
@@ -113,9 +108,9 @@ public class ShowValidatorAction extends ListValidatorsAction {
}
} catch (Exception e) {
if (LOG.isWarnEnabled()) {
LOG.warn("Unable to retrieve properties.", e);
LOG.warn("Unable to retrieve properties.", e);
}
addActionError("Unable to retrieve properties: " + e.toString());
addActionError("Unable to retrieve properties: " + e);
}
if (hasErrors()) {
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-convention-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>7.0.2</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-jasperreports-plugin</artifactId>
+6
View File
@@ -0,0 +1,6 @@
# Jasper Reports plugin
This plugin allows to use Jasper reports as a one of the result types.
You will find more details in [documentation](https://struts.apache.org/plugins/jasperreports/).
## Installation
Just drop this plugin JAR into `WEB-INF/lib` folder or add it as a Maven dependency.
+82
View File
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/*
* 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.
*/
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>7.1.0</version>
</parent>
<artifactId>struts2-jasperreports7-plugin</artifactId>
<packaging>jar</packaging>
<name>Struts 2 Jasper Reports 7 Plugin [EXPERIMENTAL]</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jasperreports7.version>7.0.3</jasperreports7.version>
</properties>
<dependencies>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>${jasperreports7.version}</version>
<exclusions>
<!-- not necessary to compile and it force dependency convergence issues -->
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports-pdf</artifactId>
<version>${jasperreports7.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-junit-plugin</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,65 @@
/*
* 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.views.jasperreports7;
import net.sf.jasperreports.engine.JasperReport;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
import java.util.Locale;
public interface JasperReport7Aware {
/**
* Used to perform an action before report is going to be generated
*
* @param invocation current {@link ActionInvocation}
*/
default void beforeReportGeneration(ActionInvocation invocation) throws StrutsException {
}
/**
* Used to perform an action before report is going to be generated
*
* @param invocation current {@link ActionInvocation}
*/
default void afterReportGeneration(ActionInvocation invocation, JasperReport jasperReport) throws StrutsException {
}
/**
* Allows to specify action specific CSV delimiter, if returns null,
* default one specified by {@link JasperReport7Constants#STRUTS_JASPER_REPORT_CSV_DELIMITER} will be used
*
* @return delimiter or null
*/
default String getCsvDelimiter(ActionInvocation invocation) {
return null;
}
/**
* Allows to specify different local than used by the framework or an action
*
* @param invocation current {@link ActionInvocation}
* @return locale or null
*/
default Locale getReportLocale(ActionInvocation invocation) {
return invocation.getInvocationContext().getLocale();
}
}
@@ -0,0 +1,63 @@
/*
* 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.views.jasperreports7;
public interface JasperReport7Constants {
/**
* PDF format constant
*/
String FORMAT_PDF = "pdf";
/**
* XML format constant
*/
String FORMAT_XML = "xml";
/**
* HTML format constant
*/
String FORMAT_HTML = "html";
/**
* XLSX format constant
*/
String FORMAT_XLSX = "xlsx";
/**
* CSV format constant
*/
String FORMAT_CSV = "csv";
/**
* RTF format constant
*/
String FORMAT_RTF = "rtf";
/**
* Allows to define a custom default delimiter when exporting report into CSV file
*/
String STRUTS_JASPER_REPORT_CSV_DELIMITER = "struts.jasperReport7.csv.defaultDelimiter";
/**
* Allows to define a custom url to image servlet used when exporting report into HTML
*/
String STRUTS_JASPER_REPORT_HTML_IMAGE_SERVLET_URL = "struts.jasperReport7.html.imageServletUrl";
}
@@ -0,0 +1,384 @@
/*
* 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.views.jasperreports7;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.util.JRLoader;
import net.sf.jasperreports.export.Exporter;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.result.StrutsResultSupport;
import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker;
import org.apache.struts2.util.ValueStack;
import org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider;
import java.io.File;
import java.sql.Connection;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
/**
* <!-- START SNIPPET: description -->
* <p>
* Generates a JasperReports report using the specified format or PDF if no
* format is specified.
* </p>
* <!-- END SNIPPET: description -->
* <p>
* <b>This result type takes the following parameters:</b>
* </p>
* <!-- START SNIPPET: params -->
*
* <ul>
*
* <li><b>location (default)</b> - the location where the compiled jasper report
* definition is (foo.jasper), relative from current URL.</li>
* <li><b>dataSource (required)</b> - the EL expression used to retrieve the
* datasource from the value stack (usually a List).</li>
* <li><b>parse</b> - true by default. If set to false, all the parameters will
* not be parsed for EL expressions.</li>
* <li><b>format</b> - the format in which the report should be generated. Valid
* values can be found in {@link JasperReport7Constants}. If no format is
* specified, PDF will be used.</li>
* <li><b>contentDisposition</b> - disposition (defaults to "inline", values are
* typically <i>filename="document.pdf"</i>).</li>
* <li><b>documentName</b> - name of the document (will generate the http header
* <code>Content-disposition = X; filename=X.[format]</code>).</li>
* <li>
* <b>reportParameters</b> - an expression used to retrieve a map of
* report parameters from the value stack. The parameters may be accessed
* in the report via the usual JR mechanism and might include data not
* part of the dataSource, such as the user name of the report creator, etc.
* </li>
* <li>
* <b>connection</b> - a JDBC Connection which can be passed to the
* report instead of dataSource
* </li>
* <li><b>wrapField</b> - defines if fields should warp with ValueStackDataSource
* see <a href="https://issues.apache.org/jira/browse/WW-3698">WW-3698</a> for more details
* </li>
* </ul>
* <p>
* This result follows the same rules from {@link StrutsResultSupport}.
* Specifically, all parameters will be parsed if the "parse" parameter
* is not set to false.
* </p>
* <!-- END SNIPPET: params -->
* <p><b>Example:</b></p>
* <pre>
* <!-- START SNIPPET: example1 -->
* &lt;result name="success" type="jasperReport7"&gt;
* &lt;param name="location"&gt;foo.jasper&lt;/param&gt;
* &lt;param name="dataSource"&gt;mySource&lt;/param&gt;
* &lt;param name="format"&gt;CSV&lt;/param&gt;
* &lt;/result&gt;
* <!-- END SNIPPET: example1 -->
* </pre>
* <p>
* or for pdf
*
* <pre>
* <!-- START SNIPPET: example2 -->
* &lt;result name="success" type="jasperReport7"&gt;
* &lt;param name="location"&gt;foo.jasper&lt;/param&gt;
* &lt;param name="dataSource"&gt;mySource&lt;/param&gt;
* &lt;/result&gt;
* <!-- END SNIPPET: example2 -->
* </pre>
*/
public class JasperReport7Result extends StrutsResultSupport implements JasperReport7Constants {
private static final Logger LOG = LogManager.getLogger(JasperReport7Result.class);
private String parsedDataSource;
protected String dataSource;
protected String format;
protected String documentName;
protected String contentDisposition;
protected String timeZone;
protected boolean wrapField = true;
/**
* Connection can be passed to the report instead of dataSource.
*/
protected String connection;
/**
* Names a report parameters map stack value, allowing additional report parameters from the action.
*/
protected String reportParameters;
private String parsedReportParameters;
/**
* Parameters validator, excludes not accepted params
*/
private NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns;
public JasperReport7Result() {
super();
}
@Inject
public void setNotExcludedAcceptedPatterns(NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns) {
this.notExcludedAcceptedPatterns = notExcludedAcceptedPatterns;
}
protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
initializeProperties(invocation);
LOG.debug("Creating JasperReport for dataSource: {} and format: {}", dataSource, format);
// Construct the data source for the report.
ValueStack stack = invocation.getStack();
Connection reportConnection = (Connection) stack.findValue(connection);
ValueStackDataSource reportDataSource = null;
if (reportConnection == null) {
reportDataSource = prepareDataSource(stack);
}
if (invocation.getAction() instanceof JasperReport7Aware action) {
LOG.debug("Passing control to action: {} before generating report", invocation.getInvocationContext().getActionName());
action.beforeReportGeneration(invocation);
}
ServletContext servletContext = invocation.getInvocationContext().getServletContext();
String systemId = servletContext.getRealPath(finalLocation);
Map<String, Object> parameters = new ValueStackShadowMap(stack);
File directory = new File(systemId.substring(0, systemId.lastIndexOf(File.separator)));
parameters.put("reportDirectory", directory);
applyLocale(invocation, parameters);
applyTimeZone(invocation, parameters);
applyCustomParameters(stack, parameters);
JasperPrint jasperPrint;
// Fill the report and produce a print object
try {
JasperReport jasperReport = (JasperReport) JRLoader.loadObject(new File(systemId));
if (reportConnection == null) {
jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, reportDataSource);
} else {
jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, reportConnection);
}
if (invocation.getAction() instanceof JasperReport7Aware action) {
LOG.debug("Passing control to action: {} after generating report: {}",
invocation.getInvocationContext().getActionName(), jasperReport.getName());
action.afterReportGeneration(invocation, jasperReport);
}
} catch (JRException e) {
LOG.error("Error building report for uri: {}", systemId, e);
throw new ServletException(e.getMessage(), e);
}
try {
LOG.debug("Export the print object to the desired output format: {}", format);
JasperReport7ExporterProvider<?> exporterProvider = invocation.getInvocationContext().getContainer().getInstance(JasperReport7ExporterProvider.class, format);
if (exporterProvider == null) {
throw new StrutsException("No exporter found for format: " + format);
}
exportReport(invocation, jasperPrint, exporterProvider);
} catch (StrutsException e) {
LOG.error("Error producing: {} report for uri: {}", format, systemId, e);
throw new ServletException(e.getMessage(), e);
} finally {
try {
if (reportConnection != null) {
reportConnection.close();
}
} catch (Exception e) {
LOG.warn("Could not close db connection properly", e);
}
}
}
protected ValueStackDataSource prepareDataSource(ValueStack stack) throws ServletException {
boolean evaluated = parsedDataSource != null && !parsedDataSource.equals(dataSource);
boolean reevaluate = !evaluated || isAcceptableExpression(parsedDataSource);
if (reevaluate) {
return new ValueStackDataSource(stack, parsedDataSource, wrapField);
} else {
throw new ServletException(String.format("Unaccepted dataSource expression [%s]", parsedDataSource));
}
}
protected void applyLocale(ActionInvocation invocation, Map<String, Object> parameters) {
Locale locale = null;
if (invocation.getAction() instanceof JasperReport7Aware action) {
locale = action.getReportLocale(invocation);
}
if (locale == null) {
locale = invocation.getInvocationContext().getLocale();
}
LOG.debug("Using locale: {} to generate report", locale);
parameters.put(JRParameter.REPORT_LOCALE, locale);
}
protected void applyTimeZone(ActionInvocation invocation, Map<String, Object> parameters) {
if (timeZone != null) {
timeZone = conditionalParse(timeZone, invocation);
LOG.debug("Puts timezone in jasper report parameter: {}", timeZone);
final TimeZone tz = TimeZone.getTimeZone(timeZone);
if (tz != null) {
parameters.put(JRParameter.REPORT_TIME_ZONE, tz);
}
}
}
@SuppressWarnings("unchecked")
protected void applyCustomParameters(ValueStack stack, Map<String, Object> parameters) {
boolean evaluated = parsedReportParameters != null && !parsedReportParameters.equals(reportParameters);
boolean reevaluate = !evaluated || isAcceptableExpression(parsedReportParameters);
Map<String, Object> reportParams = reevaluate ? (Map<String, Object>) stack.findValue(parsedReportParameters) : null;
if (reportParams != null) {
LOG.debug("Found report parameters: {}", reportParams);
parameters.putAll(reportParams);
}
}
protected void exportReport(ActionInvocation invocation, JasperPrint jasperPrint, JasperReport7ExporterProvider<?> exporterProvider) throws StrutsException {
HttpServletResponse response = prepapreHttpServletResponse(invocation);
try {
Exporter<?, ?, ?, ?> exporter = exporterProvider.createExporter(invocation, jasperPrint);
LOG.debug("Exporting report: {} as: {} and flushing response stream", jasperPrint.getName(), format);
exporter.exportReport();
response.getOutputStream().flush();
} catch (Exception e) {
throw new StrutsException(e);
}
}
private HttpServletResponse prepapreHttpServletResponse(ActionInvocation invocation) {
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
if (contentDisposition != null || documentName != null) {
final StringBuilder tmp = new StringBuilder();
tmp.append((contentDisposition == null) ? "inline" : contentDisposition);
if (documentName != null) {
tmp.append("; filename=");
tmp.append(documentName);
tmp.append(".");
tmp.append(format);
}
response.setHeader("Content-disposition", tmp.toString());
}
return response;
}
/**
* Sets up result properties, parsing etc.
*
* @param invocation Current invocation.
*/
private void initializeProperties(ActionInvocation invocation) {
if (dataSource == null && connection == null) {
String message = "No dataSource specified...";
LOG.error(message);
throw new RuntimeException(message);
}
if (dataSource != null) {
parsedDataSource = conditionalParse(dataSource, invocation);
}
format = conditionalParse(format, invocation);
if (StringUtils.isEmpty(format)) {
format = FORMAT_PDF;
}
if (contentDisposition != null) {
contentDisposition = conditionalParse(contentDisposition, invocation);
}
if (documentName != null) {
documentName = conditionalParse(documentName, invocation);
}
parsedReportParameters = conditionalParse(reportParameters, invocation);
}
/**
* Checks if expression doesn't contain vulnerable code
*
* @param expression of result
* @return true|false
* @since 6.0.0
*/
protected boolean isAcceptableExpression(String expression) {
NotExcludedAcceptedPatternsChecker.IsAllowed isAllowed = notExcludedAcceptedPatterns.isAllowed(expression);
if (isAllowed.isAllowed()) {
return true;
}
LOG.warn("Expression [{}] isn't allowed by pattern [{}]! See Accepted / Excluded patterns at\n" +
"https://struts.apache.org/security/", expression, isAllowed.getAllowedPattern());
return false;
}
public void setDataSource(String dataSource) {
this.dataSource = dataSource;
}
public void setFormat(String format) {
this.format = format;
}
public void setDocumentName(String documentName) {
this.documentName = documentName;
}
public void setContentDisposition(String contentDisposition) {
this.contentDisposition = contentDisposition;
}
public void setTimeZone(final String timeZone) {
this.timeZone = timeZone;
}
public void setWrapField(boolean wrapField) {
this.wrapField = wrapField;
}
public void setReportParameters(String reportParameters) {
this.reportParameters = reportParameters;
}
public void setConnection(String connection) {
this.connection = connection;
}
}
@@ -0,0 +1,145 @@
/*
* 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.views.jasperreports7;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.JRRewindableDataSource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.util.MakeIterator;
import org.apache.struts2.util.ValueStack;
import java.util.Iterator;
/**
* Ported to Struts.
*/
public class ValueStackDataSource implements JRRewindableDataSource {
private static final Logger LOG = LogManager.getLogger(ValueStackDataSource.class);
private final ValueStack valueStack;
private final String dataSource;
private final boolean wrapField;
private Iterator<?> iterator;
private boolean firstTimeThrough = true;
/**
* Create a value stack data source on the given iterable property
*
* @param valueStack The value stack to base the data source on
* @param dataSourceParam The property to iterate over for the report
*/
public ValueStackDataSource(ValueStack valueStack, String dataSourceParam, boolean wrapField) {
this.valueStack = valueStack;
this.dataSource = dataSourceParam;
this.wrapField = wrapField;
Object dataSourceValue = valueStack.findValue(dataSource);
if (dataSourceValue != null) {
if (MakeIterator.isIterable(dataSourceValue)) {
iterator = MakeIterator.convert(dataSourceValue);
} else {
Object[] array = new Object[1];
array[0] = dataSourceValue;
iterator = MakeIterator.convert(array);
}
} else {
LOG.warn("Data source value for data source: {} was null", dataSource);
}
}
/**
* Get the value of a given field
*
* @param field The field to get the value for. The expression language to get the value
* of the field is either taken from the description property or from the name of the field
* if the description is <code>null</code>.
* @return an <code>Object</code> containing the field value or a new
* <code>ValueStackDataSource</code> object if the field value evaluates to
* an object that can be iterated over.
*/
public Object getFieldValue(JRField field) {
String expression = field.getName();
Object value = valueStack.findValue(expression);
LOG.debug("Field [{}] = [{}]", field.getName(), value);
if (!wrapField && MakeIterator.isIterable(value) && field.getValueClass().isInstance(value)) {
return value;
} else if (MakeIterator.isIterable(value)) {
// wrap value with ValueStackDataSource if not already wrapped
return new ValueStackDataSource(this.valueStack, expression, wrapField);
} else {
return value;
}
}
/**
* Move to the first item.
*/
public void moveFirst() {
Object dataSourceValue = valueStack.findValue(dataSource);
if (dataSourceValue != null) {
if (MakeIterator.isIterable(dataSourceValue)) {
iterator = MakeIterator.convert(dataSourceValue);
} else {
Object[] array = new Object[1];
array[0] = dataSourceValue;
iterator = MakeIterator.convert(array);
}
} else {
LOG.warn("Data source value for data source [{}] was null", dataSource);
}
}
/**
* Is there any more data
*
* @return <code>true</code> if there are more elements to iterate over and
* <code>false</code> otherwise
* @throws JRException if there is a problem determining whether there
* is more data
*/
public boolean next() throws JRException {
if (firstTimeThrough) {
firstTimeThrough = false;
} else {
valueStack.pop();
}
if ((iterator != null) && (iterator.hasNext())) {
valueStack.push(iterator.next());
if (LOG.isDebugEnabled()) {
LOG.debug("Pushed next value: {}", valueStack.findValue("."));
}
return true;
} else {
LOG.debug("No more values");
return false;
}
}
}
@@ -0,0 +1,82 @@
/*
* 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.views.jasperreports7;
import org.apache.struts2.util.ValueStack;
import java.util.HashMap;
import java.util.Objects;
/**
* Ported to Struts:
*/
public class ValueStackShadowMap extends HashMap<String, Object> {
/**
* valueStack reference
*/
transient ValueStack valueStack;
/**
* Constructs an instance of ValueStackShadowMap.
*
* @param valueStack - the underlying valuestack
*/
public ValueStackShadowMap(ValueStack valueStack) {
this.valueStack = valueStack;
}
/**
* Implementation of containsKey(), overriding HashMap implementation.
*
* @param key - The key to check in HashMap and if not found to check on valueStack.
* @return <tt>true</tt>, if contains key, <tt>false</tt> otherwise.
* @see java.util.HashMap#containsKey
*/
@Override
public boolean containsKey(Object key) {
boolean hasKey = super.containsKey(key);
if (!hasKey && key != null && valueStack.findValue(key.toString()) != null) {
hasKey = true;
}
return hasKey;
}
/**
* Implementation of get(), overriding HashMap implementation.
*
* @param key - The key to get in HashMap and if not found there from the valueStack.
* @return value - The object from HashMap or if null, from the valueStack.
* @see java.util.HashMap#get
*/
@Override
public Object get(Object key) {
Object value = super.get(key);
if (key != null && value == null) {
value = valueStack.findValue(key.toString());
}
return value;
}
}
@@ -0,0 +1,96 @@
/*
* 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.views.jasperreports7.export;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.export.JRCsvExporter;
import net.sf.jasperreports.export.SimpleCsvExporterConfiguration;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleWriterExporterOutput;
import net.sf.jasperreports.export.WriterExporterOutput;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.views.jasperreports7.JasperReport7Aware;
import org.apache.struts2.views.jasperreports7.JasperReport7Constants;
import java.io.IOException;
import java.io.OutputStream;
public class JasperReport7CsvExporterProvider implements JasperReport7ExporterProvider<JRCsvExporter> {
private static final Logger LOG = LogManager.getLogger(JasperReport7CsvExporterProvider.class);
/**
* A delimiter used when generating CSV report. By default, "," is used.
*/
private String defaultDelimiter = ",";
@Inject
public JasperReport7CsvExporterProvider(
@Inject(value = JasperReport7Constants.STRUTS_JASPER_REPORT_CSV_DELIMITER, required = false)
String defaultDelimiter
) {
if (StringUtils.isNoneEmpty(defaultDelimiter)) {
LOG.debug("Using custom default delimiter [{}]", defaultDelimiter);
this.defaultDelimiter = defaultDelimiter;
}
}
@Override
public JRCsvExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
response.setContentType("text/csv");
JRCsvExporter exporter = new JRCsvExporter();
String reportDelimiter = null;
if (invocation.getAction() instanceof JasperReport7Aware action) {
reportDelimiter = action.getCsvDelimiter(invocation);
}
if (StringUtils.isEmpty(reportDelimiter)) {
reportDelimiter = defaultDelimiter;
}
LOG.debug("Using delimiter: [{}]", reportDelimiter);
SimpleCsvExporterConfiguration config = new SimpleCsvExporterConfiguration();
config.setFieldDelimiter(reportDelimiter);
config.setRecordDelimiter(reportDelimiter);
exporter.setConfiguration(config);
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
exporter.setExporterInput(input);
try (OutputStream responseStream = response.getOutputStream()) {
WriterExporterOutput exporterOutput = new SimpleWriterExporterOutput(responseStream);
exporter.setExporterOutput(exporterOutput);
} catch (IOException e) {
LOG.error("Error writing CSV report output using: {}", JasperReport7CsvExporterProvider.class.getName(), e);
throw new StrutsException(e.getMessage(), e);
}
return exporter;
}
}
@@ -0,0 +1,34 @@
/*
* 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.views.jasperreports7.export;
import net.sf.jasperreports.engine.JRAbstractExporter;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.export.JRExporterContext;
import net.sf.jasperreports.export.ExporterConfiguration;
import net.sf.jasperreports.export.ExporterOutput;
import net.sf.jasperreports.export.ReportExportConfiguration;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
public interface JasperReport7ExporterProvider<T extends JRAbstractExporter<? extends ReportExportConfiguration, ? extends ExporterConfiguration, ? extends ExporterOutput, ? extends JRExporterContext>> {
T createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException;
}
@@ -0,0 +1,85 @@
/*
* 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.views.jasperreports7.export;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.export.HtmlExporter;
import net.sf.jasperreports.engine.export.HtmlResourceHandler;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleHtmlExporterOutput;
import net.sf.jasperreports.web.util.WebHtmlResourceHandler;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
import org.apache.struts2.inject.Inject;
import org.apache.struts2.views.jasperreports7.JasperReport7Constants;
import java.io.IOException;
import java.io.OutputStream;
public class JasperReport7HtmlExporterProvider implements JasperReport7ExporterProvider<HtmlExporter> {
private static final Logger LOG = LogManager.getLogger(JasperReport7HtmlExporterProvider.class);
/**
* Name of the url that, when prefixed with the context page, can return report images
*/
private String imageServletUrl = "/images/";
@Inject
public JasperReport7HtmlExporterProvider(
@Inject(value = JasperReport7Constants.STRUTS_JASPER_REPORT_HTML_IMAGE_SERVLET_URL, required = false)
String imageServletUrl
) {
if (StringUtils.isNoneEmpty(imageServletUrl)) {
LOG.debug("Using custom image servlet url: {}", imageServletUrl);
this.imageServletUrl = imageServletUrl;
}
}
@Override
public HtmlExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
LOG.debug("Creating: {} exporter with image servlet url: {}", this.getClass().getSimpleName(), imageServletUrl);
HttpServletRequest request = invocation.getInvocationContext().getServletRequest();
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
response.setContentType("text/html");
HtmlExporter exporter = new HtmlExporter();
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
exporter.setExporterInput(input);
try (OutputStream responseStream = response.getOutputStream()) {
SimpleHtmlExporterOutput exporterOutput = new SimpleHtmlExporterOutput(responseStream);
HtmlResourceHandler imageHandler = new WebHtmlResourceHandler(request.getContextPath() + imageServletUrl + "%s");
exporterOutput.setImageHandler(imageHandler);
exporter.setExporterOutput(exporterOutput);
} catch (IOException e) {
LOG.error("Error writing HTML report output using: {}", JasperReport7HtmlExporterProvider.class.getName(), e);
throw new StrutsException(e.getMessage(), e);
}
return exporter;
}
}
@@ -0,0 +1,61 @@
/*
* 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.views.jasperreports7.export;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.export.OutputStreamExporterOutput;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import net.sf.jasperreports.pdf.JRPdfExporter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
import java.io.IOException;
import java.io.OutputStream;
public class JasperReport7PdfExporterProvider implements JasperReport7ExporterProvider<JRPdfExporter> {
private static final Logger LOG = LogManager.getLogger(JasperReport7PdfExporterProvider.class);
@Override
public JRPdfExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
response.setContentType("application/pdf");
JRPdfExporter exporter = new JRPdfExporter();
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
exporter.setExporterInput(input);
try (OutputStream responseStream = response.getOutputStream()) {
OutputStreamExporterOutput exporterOutput = new SimpleOutputStreamExporterOutput(responseStream);
exporter.setExporterOutput(exporterOutput);
} catch (IOException e) {
LOG.error("Error writing PDF report output using: {}", JasperReport7PdfExporterProvider.class.getName(), e);
throw new StrutsException(e.getMessage(), e);
}
return exporter;
}
}
@@ -0,0 +1,61 @@
/*
* 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.views.jasperreports7.export;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.export.JRRtfExporter;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleWriterExporterOutput;
import net.sf.jasperreports.export.WriterExporterOutput;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
import java.io.IOException;
import java.io.OutputStream;
public class JasperReport7RtfExporterProvider implements JasperReport7ExporterProvider<JRRtfExporter> {
private static final Logger LOG = LogManager.getLogger(JasperReport7RtfExporterProvider.class);
@Override
public JRRtfExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
response.setContentType("application/rtf");
JRRtfExporter exporter = new JRRtfExporter();
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
exporter.setExporterInput(input);
try (OutputStream responseStream = response.getOutputStream()) {
WriterExporterOutput exporterOutput = new SimpleWriterExporterOutput(responseStream);
exporter.setExporterOutput(exporterOutput);
} catch (IOException e) {
LOG.error("Error writing RTF report output using: {}", JasperReport7RtfExporterProvider.class.getName(), e);
throw new StrutsException(e.getMessage(), e);
}
return exporter;
}
}
@@ -0,0 +1,61 @@
/*
* 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.views.jasperreports7.export;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
import net.sf.jasperreports.export.OutputStreamExporterOutput;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.StrutsException;
import java.io.IOException;
import java.io.OutputStream;
public class JasperReport7XlsxExporterProvider implements JasperReport7ExporterProvider<JRXlsxExporter> {
private static final Logger LOG = LogManager.getLogger(JasperReport7XlsxExporterProvider.class);
@Override
public JRXlsxExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
JRXlsxExporter exporter = new JRXlsxExporter();
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
exporter.setExporterInput(input);
try (OutputStream responseStream = response.getOutputStream()) {
OutputStreamExporterOutput exporterOutput = new SimpleOutputStreamExporterOutput(responseStream);
exporter.setExporterOutput(exporterOutput);
} catch (IOException e) {
LOG.error("Error writing XLSX report output using: {}", JasperReport7XlsxExporterProvider.class.getName(), e);
throw new StrutsException(e.getMessage(), e);
}
return exporter;
}
}

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