Compare commits

..

160 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
144 changed files with 5704 additions and 1469 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.9
uses: github/codeql-action/init@v3.30.3
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3.28.9
uses: github/codeql-action/autobuild@v3.30.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.9
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@0a35e8f6866a39b001e5f7ad1d0daf9836786896 # 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.3</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.3</version>
<version>7.1.0</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>7.0.3</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.3</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.3</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.3</version>
<artifactId>struts2-project</artifactId>
<version>7.1.0</version>
</parent>
<artifactId>struts2-bom</artifactId>
<version>7.0.3</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.3</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_3</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.3</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();
@@ -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)) {
@@ -18,13 +18,14 @@
*/
package org.apache.struts2.dispatcher.multipart;
import org.apache.struts2.inject.Inject;
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.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
@@ -32,7 +33,9 @@ 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.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Path;
@@ -42,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
@@ -187,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);
@@ -207,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,
@@ -234,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[]{};
@@ -257,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);
@@ -288,13 +312,6 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
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()
@@ -384,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()
*/
@@ -20,14 +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.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;
@@ -36,11 +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);
/**
* 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);
@@ -48,44 +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", normalizeSpace(item.getName()));
List<String> values;
String fieldName = item.getFieldName();
if (parameters.get(fieldName) != null) {
values = parameters.get(fieldName);
} else {
values = new ArrayList<>();
if (fieldName == null) {
LOG.warn("Form field has null fieldName, skipping");
return;
}
List<String> values = parameters.computeIfAbsent(fieldName, k -> new ArrayList<>());
String fieldValue = item.getString(charset);
if (exceedsMaxStringLength(fieldName, fieldValue)) {
@@ -99,22 +174,81 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
parameters.put(fieldName, values);
}
protected void processFileField(DiskFileItem item) {
/**
* 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())
@@ -125,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,7 +19,6 @@
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;
@@ -40,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
@@ -82,51 +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();
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);
}
@@ -181,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.
@@ -192,6 +219,12 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(fileItemInput.getFieldName()));
return;
}
// Skip file uploads that don't have a field name
if (fileItemInput.getFieldName() == null) {
LOG.warn("File upload has null fieldName, skipping");
return;
}
if (exceedsMaxFiles(fileItemInput)) {
return;
@@ -199,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) {
@@ -208,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.
*
@@ -240,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 +
'}';
}
}
}
@@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.csp;
/**
* Source of the nonce value
*/
public enum CspNonceSource {
REQUEST,
SESSION
}
@@ -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;
}
}
@@ -212,16 +212,17 @@ public class SecurityMemberAccess implements MemberAccess {
return true;
}
Class<?> targetClass = target != null ? target.getClass() : null;
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities to their underlying
// classes/members. This allows the allowlist capability to continue working and offer some level of
// 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);
}
}
@@ -231,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);
@@ -243,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);
}
}
@@ -26,15 +26,19 @@ 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;
});
}
/**
@@ -82,16 +86,8 @@ public class ProxyUtil {
*/
public static boolean isProxy(Object object) {
if (object == null) return false;
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;
return isProxyCache.computeIfAbsent(object.getClass(),
k -> isSpringAopProxy(object) || isHibernateProxy(object));
}
/**
@@ -100,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));
}
/**
@@ -123,7 +111,7 @@ public class ProxyUtil {
public static boolean isHibernateProxy(Object object) {
try {
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (NoClassDefFoundError ignored) {
} catch (LinkageError ignored) {
return false;
}
}
@@ -135,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;
}
/**
@@ -152,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;
}
/**
@@ -173,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;
}
}
/**
@@ -184,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;
}
@@ -266,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 {
@@ -30,6 +30,9 @@ 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.List;
import java.util.Map;
@@ -417,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);
@@ -492,6 +494,368 @@ abstract class AbstractMultiPartRequestTest {
.containsExactly("struts.messages.upload.error.FileUploadException");
}
@Test
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));
multiPart.setMaxSize("1"); // Very small to trigger error
multiPart.parse(mockRequest, tempDir);
// 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 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));
// 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())
.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();
}
@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) {
return endline +
"--" + boundary + endline +
@@ -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,24 +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"));
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;
}
@@ -568,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,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
+2 -1
View File
@@ -24,7 +24,8 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>7.0.3</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.3</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.3</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.3</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.3</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.3</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.3</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.3</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.3</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;
}
}
@@ -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.JRXmlExporter;
import net.sf.jasperreports.export.SimpleExporterInput;
import net.sf.jasperreports.export.SimpleXmlExporterOutput;
import net.sf.jasperreports.export.XmlExporterOutput;
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 JasperReport7XmlExporterProvider implements JasperReport7ExporterProvider<JRXmlExporter> {
private static final Logger LOG = LogManager.getLogger(JasperReport7XmlExporterProvider.class);
@Override
public JRXmlExporter createExporter(ActionInvocation invocation, JasperPrint jasperPrint) throws StrutsException {
LOG.debug("Creating: {} exporter", this.getClass().getSimpleName());
HttpServletResponse response = invocation.getInvocationContext().getServletResponse();
response.setContentType("text/xml");
JRXmlExporter exporter = new JRXmlExporter();
SimpleExporterInput input = new SimpleExporterInput(jasperPrint);
exporter.setExporterInput(input);
try (OutputStream responseOutput = response.getOutputStream()) {
XmlExporterOutput exporterOutput = new SimpleXmlExporterOutput(responseOutput);
exporter.setExporterOutput(exporterOutput);
} catch (IOException e) {
LOG.error("Error writing report XML output using: {}", JRXmlExporter.class.getName(), e);
throw new StrutsException(e.getMessage(), e);
}
return exporter;
}
}
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed 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.
@@ -0,0 +1,5 @@
Apache Struts
Copyright 2000-2024 The Apache Software Foundation
This product includes software developed by
The Apache Software Foundation (http://www.apache.org/).
@@ -0,0 +1,59 @@
<?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.
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
"https://struts.apache.org/dtds/struts-6.0.dtd">
<struts>
<constant name="struts.jasperReport7.csv.defaultDelimiter" value=","/>
<bean name="pdf"
class="org.apache.struts2.views.jasperreports7.export.JasperReport7PdfExporterProvider"
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
<bean name="csv"
class="org.apache.struts2.views.jasperreports7.export.JasperReport7CsvExporterProvider"
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
<bean name="html"
class="org.apache.struts2.views.jasperreports7.export.JasperReport7HtmlExporterProvider"
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
<bean name="xlsx"
class="org.apache.struts2.views.jasperreports7.export.JasperReport7XlsxExporterProvider"
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
<bean name="xml"
class="org.apache.struts2.views.jasperreports7.export.JasperReport7XmlExporterProvider"
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
<bean name="rtf"
class="org.apache.struts2.views.jasperreports7.export.JasperReport7RtfExporterProvider"
type="org.apache.struts2.views.jasperreports7.export.JasperReport7ExporterProvider"/>
<package name="jasperreports7-default" extends="struts-default">
<result-types>
<result-type name="jasperReport7" class="org.apache.struts2.views.jasperreports7.JasperReport7Result"/>
</result-types>
</package>
</struts>

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