Compare commits

...

82 Commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Fixes CodeQL scan by using proper versions of actions

* WW-5501 Uses FilenameUtils instead of a custom code
2025-02-16 12:05:49 +01:00
Kusal Kithul-Godage 58f37babbf Merge pull request #1215 from apache/WW-5528-multipart-illegal-char-errors-67 2025-02-09 23:44:10 +11:00
Kusal Kithul-Godage fe3dd6ba9f WW-5528 Ensure multipart upload illegal characters reported as error 2025-02-07 12:40:42 +11:00
Lukasz Lenart 7a77c7a82c [maven-release-plugin] prepare for next development iteration 2025-02-04 07:39:44 +01:00
Lukasz Lenart 5038a9209f [maven-release-plugin] prepare release STRUTS_6_7_2 2025-02-04 07:39:35 +01:00
Lukasz Lenart 2135ebf651 WW-5501 Ignores DMI related action field (#1211) 2025-02-04 06:47:43 +01:00
Kusal Kithul-Godage c0dd8dd80c Merge pull request #1210 from apache/WW-5516-attrmap-npe-67 2025-02-04 01:19:16 +11:00
Kusal Kithul-Godage c36eafa37a WW-5516 Fix AttributeMap NPE when PageContext has no request 2025-02-04 01:00:36 +11:00
Lukasz Lenart 07603b9591 [maven-release-plugin] prepare for next development iteration 2025-02-02 10:16:30 +01:00
Lukasz Lenart 3227d5929a [maven-release-plugin] prepare release STRUTS_6_7_1 2025-02-02 10:16:20 +01:00
Lukasz Lenart 4a8ddbb274 Merge pull request #1202 from apache/fix/bom-version
Defines proper snapshot version for BOM
2025-02-02 10:10:35 +01:00
Lukasz Lenart 67ba458d03 Defines proper snapshot version for BOM 2025-02-02 10:00:09 +01:00
Lukasz Lenart 959b2182b2 Merge pull request #1195 from apache/fix/proper-version
Defines proper snapshot version
2025-01-24 08:28:32 +01:00
Lukasz Lenart 6de93e77db Defines proper snapshot version 2025-01-24 07:58:39 +01:00
Lukasz Lenart 59dc9301e0 Merge pull request #1194 from apache/fix/jenkinsfile-67x
Simplifies Jenkinsfile to build Struts 6.x
2025-01-22 08:18:56 +01:00
Lukasz Lenart 2fc4a822a4 Simplifies Jenkinsfile to build Struts 6.x 2025-01-22 07:52:34 +01:00
Kusal Kithul-Godage 6a6270f62a Merge pull request #1190 from apache/WW-5516-fix-temp-theme-fallback
WW-5516 Fix TemplateDir and Theme fallback to Request/Session/Application attributes
2025-01-22 13:24:09 +11:00
Lukasz Lenart 9153228b6a Merge pull request #1192 from apache/fix/nightlies-67x
Fixes nightlies builds
2025-01-21 20:31:59 +01:00
Lukasz Lenart 5713a74edc Fixes nightlies builds 2025-01-21 18:06:11 +01:00
Kusal Kithul-Godage 32071c71ad WW-5516 Fix TemplateDir and Theme fallback to Request/Session/Application attributes 2025-01-21 20:05:24 +11:00
Lukasz Lenart 7e4791985b Merge pull request #1175 from apache/feature/WW-5512-optional-inject
WW-5512 Extends the container to support injecting optional parameters into constructor
2025-01-21 07:25:58 +01:00
Lukasz Lenart 3c1e44ca2d WW-5512 Supports mixing required and optional parameters in any order 2025-01-19 09:53:01 +01:00
Lukasz Lenart 2ba8d97ad0 WW-5512 Supports mixing required and optional parameters 2025-01-19 09:36:27 +01:00
Lukasz Lenart cba0db0b07 Merge pull request #1181 from apache/fix/WW-5501-exclude
WW-5501 Only exclude malicious file names
2025-01-16 07:28:46 +01:00
Lukasz Lenart 688162cae2 WW-5501 Only exclude malicious file names 2025-01-14 07:47:39 +01:00
Lukasz Lenart a33c162f53 WW-5512 Extends the container to support injecting optional parameters into constructor 2025-01-05 17:45:11 +01:00
Lukasz Lenart c7a6daf5ce Merge pull request #1169 from apache/fix/WW-5500
WW-5500 Extends pattern to validate multipart uploads
2025-01-05 09:20:33 +01:00
Lukasz Lenart 370c7ce17a Merge pull request #1168 from apache/fix/WW-5498
WW-5498 Adds devMode errors as action messages to avoid breaking validation logic
2025-01-05 09:16:44 +01:00
Lukasz Lenart 55989befce Merge pull request #1167 from apache/fix/excluded-packages
Excludes new & old packages
2025-01-04 14:37:47 +01:00
Lukasz Lenart 2e4a4b5e36 Excludes new & old packages 2025-01-04 13:56:31 +01:00
Lukasz Lenart 9d07c3b4c3 WW-5498 Adds devMode errors as action messages to avoid breaking validation logic 2025-01-03 17:14:12 +01:00
Lukasz Lenart 575e4e8c9b WW-5500 Extends pattern to validate multipart uploads 2025-01-03 17:10:59 +01:00
Lukasz Lenart 74388dfba7 Merge pull request #1158 from apache/fix/updates-asf-yaml
Updates .asf.yaml to match main
2024-12-23 16:11:55 +01:00
Lukasz Lenart 4ee8b02262 Updates .asf.yaml to match main 2024-12-23 15:24:10 +01:00
Lukasz Lenart c8febc2050 Merge pull request #1156 from apache/feature/WW-5501-exclude
WW-5501 Exclude malicious names
2024-12-23 15:08:58 +01:00
Lukasz Lenart 3ea126388c WW-5501 Uses StringUtils.normalizeSpace instead of sanitizeNewlines 2024-12-23 14:13:26 +01:00
Lukasz Lenart 09993e3476 WW-5501 Exclude malicious names 2024-12-23 12:05:15 +01:00
Lukasz Lenart f5688e680b Merge pull request #1150 from apache/feature/prepare-development
Sets version to 6.7.1-SNAPSHOT to prepare for another development cycle
2024-12-18 15:42:21 +01:00
Lukasz Lenart 60a3db0a63 Sets version to 6.7.1-SNAPSHOT to prepare for another development cycle 2024-12-18 14:43:29 +01:00
Lukasz Lenart 4714d27678 Merge pull request #1141 from apache/fix/WW-5493-parameters
WW-5493 Restores parameters and uses getAttributes()
2024-12-18 07:42:47 +01:00
Lukasz Lenart 4feceee5f1 WW-5493 Restores parameters and uses getAttributes()
Instead of using parameters field directly, this PR uses getAttributes() instead to avoid coupling with internal Component state
2024-12-04 08:19:34 +01:00
120 changed files with 1613 additions and 386 deletions
+15 -3
View File
@@ -11,12 +11,24 @@ notifications:
jira_options: link label worklog
github:
description: "Apache Struts is a free, open-source, MVC framework for creating elegant, modern Java web applications"
homepage: https://struts.apache.org/
del_branch_on_merge: true
protected_branches:
master:
main:
# contexts are the names of checks that must pass.
contexts:
- build
required_status_checks:
contexts:
- "Build and Test (JDK 17)"
required_pull_request_reviews:
# it does not work because our github teams are private/secret, see INFRA-25666
require_code_owner_reviews: false
required_approving_review_count: 0
release/*:
# contexts are the names of checks that must pass.
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
+20 -13
View File
@@ -17,7 +17,8 @@ name: "CodeQL"
on:
push:
branches: [ "master" ]
branches:
- 'release/*'
pull_request:
permissions:
@@ -41,15 +42,21 @@ jobs:
matrix:
language: [ 'java' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.27.1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3.27.1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.27.1
with:
category: "/language:${{matrix.language}}"
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Java JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
cache: 'maven'
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.8
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3.28.8
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.8
with:
category: "/language:${{matrix.language}}"
+7 -4
View File
@@ -1,5 +1,5 @@
# IDEA
.idea
.idea/
*.iml
*.ipr
*.iws
@@ -38,11 +38,14 @@ buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
plugins/testng/test-output
test-output
plugins/testng/test-output/
test-output/
# Sonar
/.sonar/
.sonar/
# Tidelift CLI scanner
.tidelift
# Claude Code specific local settings
.claude/
+160
View File
@@ -0,0 +1,160 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Apache Struts 2 Framework (Version 6.7.x)
This is the Apache Struts 2 web framework, a free open-source solution for creating Java web applications. The codebase is a multi-module Maven project with comprehensive plugin architecture.
## Build System & Common Commands
### Maven Commands
```bash
# Build entire project
./mvnw clean install
# Build without running tests
./mvnw clean install -DskipTests
# Build without assembly
./mvnw clean install -DskipAssembly
# Run all tests
./mvnw clean test
# Run tests with coverage
./mvnw clean verify -Pcoverage -DskipAssembly
# Run integration tests
./mvnw clean verify -DskipAssembly
# Test specific module
./mvnw -pl core clean test
./mvnw -pl plugins/spring clean test
# Check for security vulnerabilities
./mvnw clean verify -Pdependency-check
# Run Apache RAT license check
./mvnw clean prepare-package
```
### Test Framework
- **Primary**: JUnit 4.13.2 with Maven Surefire Plugin 3.5.1
- **Pattern**: `**/*Test.java` (excludes `**/TestBean.java`)
- **Coverage**: JaCoCo 0.8.12
- **Additional**: Mockito, EasyMock, AssertJ, Spring Test
- **Integration**: Maven Failsafe Plugin with Jetty on port 8090
## Project Architecture
### Core Structure
```
struts6/
├── core/ # Core Struts 2 framework (main dependency)
├── plugins/ # Plugin modules
│ ├── spring/ # Spring integration
│ ├── json/ # JSON support
│ ├── tiles/ # Apache Tiles integration
│ ├── velocity/ # Velocity template engine
│ └── [20+ other plugins]
├── apps/ # Sample applications
│ ├── showcase/ # Feature demonstration app
│ └── rest-showcase/ # REST API examples
├── bundles/ # OSGi bundles
├── bom/ # Bill of Materials
└── assembly/ # Distribution packaging
```
### Key Technologies
- **Java**: Minimum JDK 8, supports up to JDK 21
- **Servlet API**: 3.1+ required
- **Expression Language**: OGNL 3.3.5
- **Dependency Injection**: Custom container (`com.opensymphony.xwork2.inject`)
- **Templating**: FreeMarker 2.3.33 (default), Velocity, JSP
- **Logging**: SLF4J 2.0.16 with Log4j2 2.24.1
- **Build**: Maven with wrapper (3.9.6)
### Core Components Architecture
#### Action Framework (MVC Pattern)
- **Actions**: Located in `core/src/main/java/org/apache/struts2/action/`
- **Action Support**: `ActionSupport` base class with validation and i18n
- **Action Context**: `ActionContext` provides access to servlet objects
- **Action Invocation**: `DefaultActionInvocation` handles action execution
#### Configuration System
- **XML-based**: Primary configuration via `struts.xml` files
- **Annotation-based**: Convention plugin for zero-config approach
- **Java-based**: `StrutsJavaConfiguration` for programmatic setup
- **Property files**: `struts.properties` for framework settings
#### Interceptor Chain
- **Framework Core**: All requests processed through interceptor chain
- **Built-in Interceptors**: 20+ interceptors in `org.apache.struts2.interceptor`
- **Validation**: `ValidationInterceptor` with annotation support
- **File Upload**: `FileUploadInterceptor` with security controls
- **Parameters**: `ParametersInterceptor` with OGNL expression handling
#### Result Framework
- **Result Types**: JSP, FreeMarker, Redirect, Stream, JSON, etc.
- **Chaining**: `ActionChainResult` for action-to-action calls
- **Templates**: Pluggable result renderers
#### Value Stack (OGNL Integration)
- **Expression Language**: OGNL for property access and method calls
- **Security**: `SecurityMemberAccess` prevents dangerous operations
- **Performance**: Caffeine-based expression caching
- **Context**: CompoundRoot provides hierarchical value resolution
### Plugin Architecture
Each plugin is a separate Maven module with:
- **Plugin Descriptor**: `struts-plugin.xml` defines beans and configuration
- **Dependency Isolation**: Separate classloaders for plugin resources
- **Extension Points**: Configurable via dependency injection
- **Popular Plugins**: Spring (DI), JSON (REST), Tiles (Layout), Bean Validation (JSR-303)
### Security Architecture
- **OGNL Security**: Restricted method access and class loading
- **CSRF Protection**: Token-based with `TokenInterceptor`
- **File Upload Security**: Type and size restrictions
- **Content Security Policy**: Built-in CSP support
- **Input Validation**: Server-side validation framework
- **Pattern Matching**: Configurable allowed/excluded patterns
## Development Guidelines
### Code Organization
- **Package Structure**: Follow existing `org.apache.struts2.*` hierarchy
- **Naming Conventions**: Use Struts conventions (Actions end with `Action`)
- **Configuration**: Prefer XML configuration in `struts.xml` for complex setups
- **Testing**: Each module has comprehensive unit and integration tests
### Plugin Development
```java
// Plugin descriptor example (struts-plugin.xml)
<bean type="com.opensymphony.xwork2.ObjectFactory"
name="myObjectFactory"
class="com.example.MyObjectFactory" />
```
### Common Patterns
- **Action Implementation**: Extend `ActionSupport` or implement `Action`
- **Result Mapping**: Use result configuration in `struts.xml`
- **Interceptor Development**: Extend `AbstractInterceptor`
- **Type Conversion**: Implement `TypeConverter` for custom types
- **Validation**: Use validation XML or annotations
### Important Notes
- **Version**: Currently 6.7.5-SNAPSHOT (release branch: `release/struts-6-7-x`)
- **Java Compatibility**: Compiled for Java 8, tested through Java 21
- **Security**: Always validate inputs and follow OWASP guidelines
- **Performance**: Leverage built-in caching (OGNL expressions, templates)
- **Deprecation**: Some legacy XWork components marked for removal
### Build Profiles
- **coverage**: Enables JaCoCo coverage reporting
- **dependency-check**: OWASP dependency vulnerability scanning
- **jdk17**: Special configuration for Java 17+ module system
This is a mature, enterprise-grade framework with extensive documentation at https://struts.apache.org/ and active community support through Apache mailing lists and JIRA (project WW).
Vendored
+11 -54
View File
@@ -69,7 +69,7 @@ pipeline {
stages {
stage('Test & Coverage') {
steps {
sh './mvnw -B verify -Pcoverage -DskipAssembly --no-transfer-progress'
sh './mvnw -B verify -Pcoverage -DskipAssembly'
}
post {
always {
@@ -81,58 +81,15 @@ pipeline {
stage('Code Quality') {
when {
anyOf {
branch 'master'; branch 'release/struts-7-0-x'
branch 'release/struts-6-7-x'
}
}
steps {
withCredentials([string(credentialsId: 'asf-struts-sonarcloud', variable: 'SONARCLOUD_TOKEN')]) {
sh './mvnw -B -Pcoverage -DskipAssembly -Dsonar.login=${SONARCLOUD_TOKEN} verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar --no-transfer-progress'
sh './mvnw -B -Pcoverage -DskipAssembly -Dsonar.login=${SONARCLOUD_TOKEN} verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar'
}
}
}
stage('Build Source & JavaDoc') {
when {
branch 'release/struts-7-0-x'
}
steps {
dir("local-snapshots-dir/") {
deleteDir()
}
sh './mvnw -B source:jar javadoc:jar -DskipTests -DskipAssembly --no-transfer-progress'
}
}
stage('Deploy Snapshot') {
when {
branch 'release/struts-7-0-x'
}
steps {
withCredentials([file(credentialsId: 'lukaszlenart-repository-access-token', variable: 'CUSTOM_SETTINGS')]) {
sh './mvnw -s \${CUSTOM_SETTINGS} deploy -DskipTests -DskipAssembly --no-transfer-progress'
}
}
}
stage('Upload nightlies') {
when {
branch 'release/struts-7-0-x'
}
steps {
sh './mvnw -B package -DskipTests --no-transfer-progress'
sshPublisher(publishers: [
sshPublisherDesc(
configName: 'Nightlies',
transfers: [
sshTransfer(
remoteDirectory: '/struts/snapshot',
removePrefix: 'assembly/target/assembly/out',
sourceFiles: 'assembly/target/assembly/out/struts-*.zip',
cleanRemote: true
)
],
verbose: true
)
])
}
}
}
post {
always {
@@ -154,12 +111,12 @@ pipeline {
stages {
stage('Build') {
steps {
sh './mvnw -B clean install -DskipTests -DskipAssembly --no-transfer-progress'
sh './mvnw -B clean install -DskipTests -DskipAssembly'
}
}
stage('Test') {
steps {
sh './mvnw -B verify --no-transfer-progress'
sh './mvnw -B verify -DskipAssembly'
}
post {
always {
@@ -170,31 +127,31 @@ pipeline {
}
stage('Build Source & JavaDoc') {
when {
branch 'master'
branch 'release/struts-6-7-x'
}
steps {
dir("local-snapshots-dir/") {
deleteDir()
}
sh './mvnw -B source:jar javadoc:jar -DskipTests -DskipAssembly --no-transfer-progress'
sh './mvnw -B source:jar javadoc:jar -DskipTests -DskipAssembly'
}
}
stage('Deploy Snapshot') {
when {
branch 'master'
branch 'release/struts-6-7-x'
}
steps {
withCredentials([file(credentialsId: 'lukaszlenart-repository-access-token', variable: 'CUSTOM_SETTINGS')]) {
sh './mvnw -s \${CUSTOM_SETTINGS} deploy -DskipTests -DskipAssembly --no-transfer-progress'
sh './mvnw -s \${CUSTOM_SETTINGS} deploy -DskipTests -DskipAssembly'
}
}
}
stage('Upload nightlies') {
when {
branch 'master'
branch 'release/struts-6-7-x'
}
steps {
sh './mvnw -B package -DskipTests --no-transfer-progress'
sh './mvnw -B package -DskipTests'
sshPublisher(publishers: [
sshPublisherDesc(
configName: 'Nightlies',
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-apps</artifactId>
<packaging>pom</packaging>
+2 -2
View File
@@ -24,12 +24,12 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-rest-showcase</artifactId>
<packaging>war</packaging>
<version>6.7.0</version>
<version>6.8.0</version>
<name>Struts 2 Rest Showcase Webapp</name>
<description>Struts 2 Rest Showcase Example</description>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-apps</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-showcase</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-assembly</artifactId>
+4 -4
View File
@@ -25,11 +25,11 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-bom</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
<packaging>pom</packaging>
<name>Struts 2 Bill of Materials</name>
@@ -44,7 +44,7 @@
</licenses>
<properties>
<struts-version.version>6.7.0</struts-version.version>
<struts-version.version>6.8.0</struts-version.version>
<maven.site.skip>true</maven.site.skip>
<maven.site.deploy.skip>true</maven.site.deploy.skip>
</properties>
@@ -190,7 +190,7 @@
</dependencyManagement>
<scm>
<tag>STRUTS_6_7_0</tag>
<tag>STRUTS_6_8_0</tag>
<connection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</connection>
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/struts.git</developerConnection>
<url>https://github.com/apache/struts/</url>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-bundles</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-osgi-admin-bundle</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-bundles</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-osgi-demo-bundle</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-osgi-bundles</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-parent</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-core</artifactId>
<packaging>jar</packaging>
@@ -68,6 +68,8 @@ import com.opensymphony.xwork2.validator.ValidatorFileParser;
import ognl.PropertyAccessor;
import org.apache.struts2.dispatcher.HttpParameters;
import org.apache.struts2.dispatcher.Parameter;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.interceptor.exec.StrutsExecutorProvider;
import org.apache.struts2.url.QueryStringBuilder;
@@ -159,7 +161,9 @@ public class StrutsDefaultConfigurationProvider implements ConfigurationProvider
.factory(UrlEncoder.class, StrutsUrlEncoder.class, Scope.SINGLETON)
.factory(UrlDecoder.class, StrutsUrlDecoder.class, Scope.SINGLETON)
.factory(ExecutorProvider.class, StrutsExecutorProvider.class, Scope.SINGLETON);
.factory(ExecutorProvider.class, StrutsExecutorProvider.class, Scope.SINGLETON)
.factory(CspNonceReader.class, StrutsCspNonceReader.class, Scope.SINGLETON);
for (Map.Entry<String, Object> entry : DefaultConfiguration.BOOTSTRAP_CONSTANTS.entrySet()) {
props.setProperty(entry.getKey(), String.valueOf(entry.getValue()));
@@ -18,8 +18,8 @@
*/
package com.opensymphony.xwork2.factory;
import com.opensymphony.xwork2.ObjectFactory;
import com.opensymphony.xwork2.conversion.TypeConverter;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
@@ -33,16 +33,15 @@ public class StrutsConverterFactory implements ConverterFactory {
private static final Logger LOG = LogManager.getLogger(StrutsConverterFactory.class);
private Container container;
private ObjectFactory objectFactory;
@Inject
public void setContainer(Container container) {
this.container = container;
public void setObjectFactory(ObjectFactory objectFactory) {
this.objectFactory = objectFactory;
}
public TypeConverter buildConverter(Class<? extends TypeConverter> converterClass, Map<String, Object> extraContext) throws Exception {
LOG.debug("Creating converter of type [{}]", converterClass.getCanonicalName());
return container.inject(converterClass);
return (TypeConverter)objectFactory.buildBean(converterClass, extraContext);
}
}
@@ -242,6 +242,41 @@ class ContainerImpl implements Container {
return toArray(parameterInjectors);
}
/**
* Gets parameter injectors with nulls for optional dependencies.
*
* @param member to which the parameters belong
* @param annotations on the parameters
* @param parameterTypes parameter types
* @return injections
*/
<M extends AccessibleObject & Member> ParameterInjector<?>[] getParametersInjectorsWithNulls(
M member,
Annotation[][] annotations,
Class<?>[] parameterTypes,
String defaultName
) throws MissingDependencyException {
final List<ParameterInjector<?>> parameterInjectors = new ArrayList<>();
final Iterator<Annotation[]> annotationsIterator = Arrays.asList(annotations).iterator();
for (Class<?> parameterType : parameterTypes) {
Inject annotation = findInject(annotationsIterator.next());
String name = annotation == null ? defaultName : annotation.value();
Key<?> key = Key.newInstance(parameterType, name);
try {
parameterInjectors.add(createParameterInjector(key, member));
} catch (MissingDependencyException e) {
if (annotation != null && annotation.required()) {
throw e;
} else {
parameterInjectors.add(createNullParameterInjector(key, member));
}
}
}
return toArray(parameterInjectors);
}
<T> ParameterInjector<T> createParameterInjector(Key<T> key, Member member) throws MissingDependencyException {
final InternalFactory<? extends T> factory = getFactory(key);
if (factory == null) {
@@ -252,6 +287,23 @@ class ContainerImpl implements Container {
return new ParameterInjector<>(externalContext, factory);
}
<T> ParameterInjector<T> createNullParameterInjector(Key<T> key, Member member) throws MissingDependencyException {
final InternalFactory<? extends T> factory = new InternalFactory<T>() {
@Override
public T create(InternalContext context) {
return null;
}
@Override
public Class<? extends T> type() {
return key.getType();
}
};
final ExternalContext<T> externalContext = ExternalContext.newInstance(member, key, this);
return new ParameterInjector<>(externalContext, factory);
}
private ParameterInjector<?>[] toArray(List<ParameterInjector<?>> parameterInjections) {
return parameterInjections.toArray(new ParameterInjector[0]);
}
@@ -361,7 +413,7 @@ class ContainerImpl implements Container {
Inject inject, ContainerImpl container, Constructor<T> constructor) throws MissingDependencyException {
return constructor.getParameterTypes().length == 0
? null // default constructor.
: container.getParametersInjectors(
: container.getParametersInjectorsWithNulls(
constructor,
constructor.getParameterAnnotations(),
constructor.getParameterTypes(),
@@ -23,6 +23,10 @@ import ognl.Ognl;
import java.util.Map;
/**
* @deprecated since 6.8.0, to be removed, see {@link ReflectionContextFactory}
*/
@Deprecated
public class OgnlReflectionContextFactory implements ReflectionContextFactory {
public Map createDefaultContext(Object root) {
@@ -160,6 +160,9 @@ public class SecurityMemberAccess implements MemberAccess {
public boolean isAccessible(Map context, Object target, Member member, String propertyName) {
LOG.debug("Checking access for [target: {}, member: {}, property: {}]", target, member, propertyName);
if (member == null) {
throw new IllegalArgumentException("Member cannot be null!");
}
if (target != null) {
// Special case: Target is a Class object but not Class.class
if (Class.class.equals(target.getClass()) && !Class.class.equals(target)) {
@@ -228,7 +231,7 @@ public class SecurityMemberAccess implements MemberAccess {
return true;
}
if (!disallowProxyObjectAccess && target != null && ProxyUtil.isProxy(target)) {
if (!disallowProxyObjectAccess && ProxyUtil.isProxy(target)) {
// If `disallowProxyObjectAccess` is not set, allow resolving Hibernate entities to their underlying
// classes/members. This allows the allowlist capability to continue working and offer some level of
// protection in applications where the developer has accepted the risk of allowing OGNL access to Hibernate
@@ -36,6 +36,7 @@ public class DefaultExcludedPatternsChecker implements ExcludedPatternsChecker {
private static final Logger LOG = LogManager.getLogger(DefaultExcludedPatternsChecker.class);
public static final String[] EXCLUDED_PATTERNS = {
"(^|\\%\\{)(#?top\\.)[^\\s]*",
"(^|\\%\\{)((#?)(top(\\.|\\['|\\[\")|\\[\\d\\]\\.)?)(dojo|struts|session|request|response|application|servlet(Request|Response|Context)|parameters|context|_memberAccess)(\\.|\\[).*",
".*(^|\\.|\\[|\\'|\"|get)class(\\(\\.|\\[|\\'|\").*",
"actionErrors|actionMessages|fieldErrors"
@@ -35,7 +35,7 @@ public final class DebugUtils {
log.error(message);
if (action instanceof ValidationAware) {
ValidationAware validationAware = (ValidationAware) action;
validationAware.addActionError(message);
validationAware.addActionMessage(message);
}
}
@@ -81,6 +81,7 @@ public class ProxyUtil {
* @param object the object to check
*/
public static boolean isProxy(Object object) {
if (object == null) return false;
Class<?> clazz = object.getClass();
Boolean flag = isProxyCache.get(clazz);
if (flag != null) {
@@ -121,7 +122,7 @@ public class ProxyUtil {
*/
public static boolean isHibernateProxy(Object object) {
try {
return HibernateProxy.class.isAssignableFrom(object.getClass());
return object != null && HibernateProxy.class.isAssignableFrom(object.getClass());
} catch (NoClassDefFoundError ignored) {
return false;
}
@@ -20,6 +20,10 @@ package com.opensymphony.xwork2.util.reflection;
import java.util.Map;
/**
* @deprecated since 6.8.0, avoid using this interface and any of its implementation, it's going to be removed soon
*/
@Deprecated
public interface ReflectionContextFactory {
/**
* Creates and returns a new standard naming context for evaluating an OGNL
@@ -509,4 +509,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";
}
@@ -215,8 +215,8 @@ public class ActionComponent extends ContextBean {
HttpParameters.Builder builder = HttpParameters.create().withParent(parentParams);
if (attributes != null) {
builder = builder.withExtraParams(attributes);
if (getAttributes() != null) {
builder = builder.withExtraParams(getAttributes());
}
return builder.build();
}
@@ -71,7 +71,11 @@ public class Component {
protected boolean devMode = false;
protected boolean escapeHtmlBody = false;
protected ValueStack stack;
protected Map<String, Object> attributes;
/**
* @deprecated use {@link #getAttributes} instead of directly depending on this field
*/
@Deprecated
protected Map<String, Object> parameters;
protected ActionMapper actionMapper;
protected boolean throwExceptionOnELFailure;
protected boolean performClearTagStateForTagPoolingServers = false;
@@ -86,7 +90,7 @@ public class Component {
*/
public Component(ValueStack stack) {
this.stack = stack;
this.attributes = new LinkedHashMap<>();
this.parameters = new LinkedHashMap<>();
getComponentStack().push(this);
}
@@ -279,7 +283,7 @@ public class Component {
*/
protected StrutsException fieldError(String field, String errorMsg, Exception e) {
String msg = "tag '" + getComponentName() + "', field '" + field +
(attributes != null && attributes.containsKey("name") ? "', name '" + attributes.get("name") : "") +
(getAttributes() != null && getAttributes().containsKey("name") ? "', name '" + getAttributes().get("name") : "") +
"': " + errorMsg;
throw new StrutsException(msg, e);
}
@@ -457,7 +461,7 @@ public class Component {
* @param params the parameters to copy.
*/
public void copyParams(Map<String, Object> params) {
stack.push(attributes);
stack.push(getAttributes());
stack.push(this);
try {
for (Map.Entry<String, Object> entry : params.entrySet()) {
@@ -467,7 +471,7 @@ public class Component {
// UI component attributes may contain hypens (e.g. data-ajax), but ognl
// can't handle that, and there can't be a component property with a hypen
// so into the parameters map it goes. See WW-4493
attributes.put(key, entry.getValue());
getAttributes().put(key, entry.getValue());
} else {
stack.setValue(key, entry.getValue());
}
@@ -500,7 +504,7 @@ public class Component {
*/
@Deprecated
public Map<String, Object> getParameters() {
return attributes;
return parameters;
}
/**
@@ -509,7 +513,7 @@ public class Component {
* @return the parameters. Is never <tt>null</tt>.
*/
public Map<String, Object> getAttributes() {
return attributes;
return parameters;
}
/**
@@ -518,7 +522,7 @@ public class Component {
* @param params the parameters to add.
*/
public void addAllParameters(Map<String, Object> params) {
attributes.putAll(params);
getAttributes().putAll(params);
}
/**
@@ -204,7 +204,7 @@ public class Form extends ClosingUIBean {
// keep a collection of the tag names for anything special the templates might want to do (such as pure client
// side validation)
if (!attributes.containsKey("tagNames")) {
if (!getAttributes().containsKey("tagNames")) {
// we have this if check so we don't do this twice (on open and close of the template)
addParameter("tagNames", new ArrayList());
}
@@ -137,14 +137,13 @@ public class Include extends Component {
urlBuf.append(page);
// Add request parameters
if (attributes.size() > 0) {
if (!getAttributes().isEmpty()) {
urlBuf.append('?');
String concat = "";
// Set parameters
for (Object next : attributes.entrySet()) {
Map.Entry entry = (Map.Entry) next;
for (Map.Entry<String, Object> entry : getAttributes().entrySet()) {
Object name = entry.getKey();
List values = (List) entry.getValue();
@@ -234,11 +233,11 @@ public class Include extends Component {
// instead, include tag requires that each parameter be a list of objects,
// just like the HTTP servlet interfaces are (String[])
if (value != null) {
List currentValues = (List) attributes.get(key);
List currentValues = (List) getAttributes().get(key);
if (currentValues == null) {
currentValues = new ArrayList();
attributes.put(key, currentValues);
getAttributes().put(key, currentValues);
}
currentValues.add(value);
@@ -81,8 +81,8 @@ public class Label extends UIBean {
if (value != null) {
addParameter("nameValue", findString(value));
} else if (key != null) {
Object nameValue = attributes.get("nameValue");
if (nameValue == null || nameValue.toString().length() == 0) {
Object nameValue = getAttributes().get("nameValue");
if (nameValue == null || nameValue.toString().isEmpty()) {
// get the label from a TextProvider (default value is the key)
String providedLabel = TextProviderHelper.getText(key, key, stack);
addParameter("nameValue", providedLabel);
@@ -67,7 +67,7 @@ public abstract class ListUIBean extends UIBean {
Object value = null;
if (list == null) {
list = attributes.get("list");
list = getAttributes().get("list");
}
if (list instanceof String) {
@@ -174,7 +174,7 @@ public class ServletUrlRenderer implements UrlRenderer {
namespace, actionName);
if (actionConfig != null) {
ActionMapping mapping = new ActionMapping(actionName, namespace, actionMethod, formComponent.attributes);
ActionMapping mapping = new ActionMapping(actionName, namespace, actionMethod, formComponent.getAttributes());
String result = urlHelper.buildUrl(formComponent.actionMapper.getUriFromActionMapping(mapping),
formComponent.request, formComponent.response, queryStringResult.getQueryParams(), scheme, formComponent.includeContext, true, false, false);
formComponent.addParameter("action", result);
@@ -50,17 +50,16 @@ import javax.servlet.http.HttpServletResponse;
* </pre>
*/
@StrutsTag(
name="textfield",
tldTagClass="org.apache.struts2.views.jsp.ui.TextFieldTag",
description="Render an HTML input field of type text",
allowDynamicAttributes=true)
name = "textfield",
tldTagClass = "org.apache.struts2.views.jsp.ui.TextFieldTag",
description = "Render an HTML input field of type text",
allowDynamicAttributes = true)
public class TextField extends UIBean {
/**
* The name of the default template for the TextFieldTag
*/
final public static String TEMPLATE = "text";
protected String maxlength;
protected String readonly;
protected String size;
@@ -95,27 +94,22 @@ public class TextField extends UIBean {
}
@StrutsTagAttribute(description="HTML maxlength attribute", type="Integer")
@StrutsTagAttribute(description = "HTML maxlength attribute", type = "Integer")
public void setMaxlength(String maxlength) {
this.maxlength = maxlength;
}
@StrutsTagAttribute(description="Deprecated. Use maxlength instead.", type="Integer")
public void setMaxLength(String maxlength) {
this.maxlength = maxlength;
}
@StrutsTagAttribute(description="Whether the input is readonly", type="Boolean", defaultValue="false")
@StrutsTagAttribute(description = "Whether the input is readonly", type = "Boolean", defaultValue = "false")
public void setReadonly(String readonly) {
this.readonly = readonly;
}
@StrutsTagAttribute(description="HTML size attribute", type="Integer")
@StrutsTagAttribute(description = "HTML size attribute", type = "Integer")
public void setSize(String size) {
this.size = size;
}
@StrutsTagAttribute(description="Specifies the html5 type element to display. e.g. text, email, url", defaultValue="text")
@StrutsTagAttribute(description = "Specifies the html5 type element to display. e.g. text, email, url", defaultValue = "text")
public void setType(String type) {
this.type = type;
}
@@ -23,7 +23,6 @@ import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.ValueStack;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
@@ -32,7 +31,9 @@ import org.apache.struts2.components.template.Template;
import org.apache.struts2.components.template.TemplateEngine;
import org.apache.struts2.components.template.TemplateEngineManager;
import org.apache.struts2.components.template.TemplateRenderingContext;
import org.apache.struts2.dispatcher.AttributeMap;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.util.ComponentUtils;
import org.apache.struts2.util.TextProviderHelper;
import org.apache.struts2.views.annotations.StrutsTagAttribute;
@@ -40,7 +41,6 @@ import org.apache.struts2.views.util.ContextUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.Writer;
import java.util.HashMap;
import java.util.LinkedHashMap;
@@ -48,6 +48,10 @@ import java.util.List;
import java.util.Map;
import java.util.function.Function;
import static java.util.Collections.emptyMap;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.struts2.dispatcher.DispatcherConstants.ATTRIBUTES;
/**
* <p>
* UIBean is the standard superclass of all Struts UI components.
@@ -440,6 +444,9 @@ public abstract class UIBean extends Component {
private static final Logger LOG = LogManager.getLogger(UIBean.class);
static final String TEMPLATE_DIR = "templateDir";
static final String THEME = "theme";
protected static final String ATTR_FIELD_VALUE = "fieldValue";
protected static final String ATTR_NAME_VALUE = "nameValue";
protected static final String ATTR_VALUE = "value";
@@ -521,6 +528,8 @@ public abstract class UIBean extends Component {
protected TemplateEngineManager templateEngineManager;
protected CspNonceReader cspNonceReader;
@Inject(StrutsConstants.STRUTS_UI_TEMPLATEDIR)
public void setDefaultTemplateDir(String dir) {
this.defaultTemplateDir = dir;
@@ -546,6 +555,11 @@ public abstract class UIBean extends Component {
this.templateEngineManager = mgr;
}
@Inject
public void setCspNonceReader(CspNonceReader cspNonceReader) {
this.cspNonceReader = cspNonceReader;
}
@Override
public boolean end(Writer writer, String body) {
evaluateParams();
@@ -602,13 +616,18 @@ public abstract class UIBean extends Component {
result = findString(this.templateDir);
}
// Check Request, Session, Application scopes
if (isBlank(result)) {
result = (String) getAttrMap().get(TEMPLATE_DIR);
}
// Default template set
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
result = defaultTemplateDir;
}
// Defaults to 'template'
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
result = "template";
}
@@ -622,26 +641,36 @@ public abstract class UIBean extends Component {
result = findString(this.theme);
}
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
Form form = (Form) findAncestor(Form.class);
if (form != null) {
result = form.getTheme();
}
}
// Check Request, Session, Application scopes
if (isBlank(result)) {
result = (String) getAttrMap().get(THEME);
}
// Default theme set
if (StringUtils.isBlank(result)) {
if (isBlank(result)) {
result = defaultUITheme;
}
return result;
}
private Map<String, Object> getAttrMap() {
AttributeMap attrMap = (AttributeMap) getStack().getContext().get(ATTRIBUTES);
return attrMap != null ? attrMap : emptyMap();
}
public void evaluateParams() {
String gotTheme = getTheme();
addParameter("templateDir", getTemplateDir());
addParameter("theme", gotTheme);
addParameter(TEMPLATE_DIR, getTemplateDir());
addParameter(THEME, gotTheme);
addParameter("template", template != null ? findString(template) : getDefaultTemplate());
addParameter("dynamicAttributes", dynamicAttributes);
addParameter("themeExpansionToken", uiThemeExpansionToken);
@@ -864,13 +893,12 @@ public abstract class UIBean extends Component {
}
// to be used with the CSP interceptor - adds the nonce value as a parameter to be accessed from ftl files
HttpSession session = stack.getActionContext().getServletRequest().getSession(false);
Object nonceValue = session != null ? session.getAttribute("nonce") : null;
CspNonceReader.NonceValue nonceValue = cspNonceReader.readNonceValue(stack);
if (nonceValue != null) {
addParameter("nonce", nonceValue.toString());
if (nonceValue.isNonceValueSet()) {
addParameter("nonce", nonceValue.getNonceValue());
} else {
LOG.debug("Session is not active, cannot obtain nonce value");
LOG.debug("Nonce not defined in: {}", nonceValue.getSource());
}
evaluateExtraParams();
@@ -882,8 +910,8 @@ public abstract class UIBean extends Component {
*/
protected void applyValueParameter(String translatedName) {
// see if the value has been specified as a parameter already
if (attributes.containsKey(ATTR_VALUE)) {
attributes.put(ATTR_NAME_VALUE, attributes.get(ATTR_VALUE));
if (getAttributes().containsKey(ATTR_VALUE)) {
getAttributes().put(ATTR_NAME_VALUE, getAttributes().get(ATTR_VALUE));
} else {
if (evaluateNameValue()) {
final Class<?> valueClazz = getValueClassType();
@@ -111,7 +111,7 @@ public class URL extends ContextBean {
public URL(ValueStack stack, HttpServletRequest req, HttpServletResponse res) {
super(stack);
urlProvider = new ComponentUrlProvider(this, this.attributes);
urlProvider = new ComponentUrlProvider(this, this.getAttributes());
urlProvider.setHttpServletRequest(req);
urlProvider.setHttpServletResponse(res);
}
@@ -69,6 +69,7 @@ import org.apache.struts2.dispatcher.DispatcherErrorHandler;
import org.apache.struts2.dispatcher.StaticContentLoader;
import org.apache.struts2.dispatcher.mapper.ActionMapper;
import org.apache.struts2.dispatcher.multipart.MultiPartRequest;
import org.apache.struts2.interceptor.csp.CspNonceReader;
import org.apache.struts2.interceptor.exec.ExecutorProvider;
import org.apache.struts2.ognl.OgnlGuard;
import org.apache.struts2.url.QueryStringBuilder;
@@ -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);
}
@@ -86,7 +86,7 @@ public class AttributeMap extends AbstractMap<String, Object> {
PageContext pc = getPageContext();
if (pc == null) {
if (pc == null || pc.getRequest() == null) {
RequestMap request = (RequestMap) context.get(DispatcherConstants.REQUEST);
SessionMap session = (SessionMap) context.get(DispatcherConstants.SESSION);
ApplicationMap application = (ApplicationMap) context.get(DispatcherConstants.APPLICATION);
@@ -112,7 +112,7 @@ public class Dispatcher {
*/
public static final String REQUEST_POST_METHOD = "POST";
public static final String MULTIPART_FORM_DATA_REGEX = "^multipart/form-data(?:\\s*;\\s*boundary=[0-9a-zA-Z'()+_,\\-./:=?]{1,70})?(?:\\s*;\\s*charset=[a-zA-Z\\-0-9]{3,14})?";
public static final String MULTIPART_FORM_DATA_REGEX = "^multipart/form-data(?:\\s*;\\s*boundary=[0-9a-zA-Z'\"()+_,\\-./:=?]{1,70})?(?:\\s*;\\s*charset=[a-zA-Z\\-0-9]{3,14})?";
private static final String CONFIG_SPLIT_REGEX = "\\s*,\\s*";
@@ -20,6 +20,7 @@ package org.apache.struts2.dispatcher.multipart;
import com.opensymphony.xwork2.LocaleProviderFactory;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
@@ -157,16 +158,7 @@ public abstract class AbstractMultiPartRequest implements MultiPartRequest {
* @return the canonical name based on the supplied filename
*/
protected String getCanonicalName(final String originalFileName) {
String fileName = originalFileName;
int forwardSlash = fileName.lastIndexOf('/');
int backwardSlash = fileName.lastIndexOf('\\');
if (forwardSlash != -1 && forwardSlash > backwardSlash) {
fileName = fileName.substring(forwardSlash + 1);
} else {
fileName = fileName.substring(backwardSlash + 1);
}
return fileName;
return FilenameUtils.getName(originalFileName);
}
}
@@ -42,7 +42,8 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
/**
* Multipart form data request adapter for Jakarta Commons Fileupload package.
@@ -57,6 +58,9 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
// maps parameter name -> List of param values
protected Map<String, List<String>> params = new HashMap<>();
// List to track all FileItem instances for comprehensive cleanup
protected List<FileItem> allFileItems = new ArrayList<>();
/**
* Creates a new request wrapper to handle multi-part data using methods adapted from Jason Pell's
* multipart classes (see class description).
@@ -100,7 +104,11 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
protected void processUpload(HttpServletRequest request, String saveDir) throws FileUploadException, UnsupportedEncodingException {
if (ServletFileUpload.isMultipartContent(request)) {
for (FileItem item : parseRequest(request, saveDir)) {
LOG.debug("Found file item: [{}]", sanitizeNewlines(item.getFieldName()));
LOG.debug("Found file item: [{}]", normalizeSpace(item.getFieldName()));
// Track all FileItem instances for comprehensive cleanup
allFileItems.add(item);
if (item.isFormField()) {
processNormalFormField(item, request.getCharacterEncoding());
} else {
@@ -115,7 +123,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (item.getName() == null || item.getName().trim().isEmpty()) {
LOG.debug("No file has been uploaded for the field: {}", sanitizeNewlines(item.getFieldName()));
LOG.debug("No file has been uploaded for the field: {}", normalizeSpace(item.getFieldName()));
return;
}
@@ -143,7 +151,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
long size = item.getSize();
if (maxStringLength != null && size > maxStringLength) {
LOG.debug("Form field {} of size {} bytes exceeds limit of {}.", sanitizeNewlines(item.getFieldName()), size, maxStringLength);
LOG.debug("Form field [{}] of size [{}] bytes exceeds limit of [{}].", normalizeSpace(item.getFieldName()), size, maxStringLength);
String errorKey = "struts.messages.upload.error.parameter.too.long";
LocalizedMessage localizedMessage = new LocalizedMessage(this.getClass(), errorKey, null,
new Object[]{item.getFieldName(), maxStringLength, size});
@@ -238,7 +246,11 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
// Ensure file exists even if it is empty.
if (diskFileItem.getSize() == 0 && storeLocation != null && !storeLocation.exists()) {
try {
storeLocation.createNewFile();
if (storeLocation.createNewFile()) {
LOG.debug("File {} has been created", storeLocation.getAbsolutePath());
} else {
LOG.warn("File {} already exists", storeLocation.getAbsolutePath());
}
} catch (IOException e) {
LOG.error("Cannot write uploaded empty file to disk: {}", storeLocation.getAbsolutePath(), e);
}
@@ -355,19 +367,32 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest {
* @see org.apache.struts2.dispatcher.multipart.MultiPartRequest#cleanUp()
*/
public void cleanUp() {
Set<String> names = files.keySet();
for (String name : names) {
List<FileItem> items = files.get(name);
for (FileItem item : items) {
LOG.debug("Removing file {} {}", name, item);
if (!item.isInMemory()) {
item.delete();
try {
LOG.debug("Performing comprehensive cleanup for {} file items.", allFileItems.size());
for (FileItem item : allFileItems) {
try {
if (item instanceof DiskFileItem) {
DiskFileItem diskItem = (DiskFileItem) item;
File storeLocation = diskItem.getStoreLocation();
if (storeLocation != null && storeLocation.exists()) {
LOG.debug("Deleting temporary file: [{}]", storeLocation.getName());
if (!storeLocation.delete()) {
LOG.warn("Unable to delete temporary file: [{}]", storeLocation.getName());
}
}
}
// Also call the item's delete method as backup
if (!item.isInMemory()) {
item.delete();
}
} catch (Exception e) {
LOG.warn("Error during cleanup of file item: [{}]", normalizeSpace(item.getFieldName()), e);
}
}
} finally {
// Clear only the tracking collection, preserve parsed data
allFileItems.clear();
}
}
private String sanitizeNewlines(String before) {
return before.replaceAll("[\n\r]", "_");
}
}
@@ -45,6 +45,8 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
/**
* Multi-part form data request adapter for Jakarta Commons FileUpload package that
* leverages the streaming API rather than the traditional non-streaming API.
@@ -77,7 +79,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
File file = fileInfo.getFile();
LOG.debug("Deleting file '{}'.", file.getName());
if (!file.delete()) {
LOG.warn("There was a problem attempting to delete file '{}'.", file.getName());
LOG.warn("There was a problem attempting to delete file [{}].", file.getName());
}
}
}
@@ -252,7 +254,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
// prevent processing file field item if request size not allowed.
if (!requestSizePermitted) {
addFileSkippedError(itemStream.getName(), request);
LOG.debug("Skipped stream '{}', request maximum size ({}) exceeded.", itemStream.getName(), maxSize);
LOG.debug("Skipped stream [{}], request maximum size ({}) exceeded.", normalizeSpace(itemStream.getName()), maxSize);
continue;
}
@@ -296,7 +298,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
* @param request the servlet request
*/
protected void addFileSkippedError(String fileName, HttpServletRequest request) {
String exceptionMessage = "Skipped file " + fileName + "; request size limit exceeded.";
String exceptionMessage = "Skipped file " + normalizeSpace(fileName) + "; request size limit exceeded.";
long allowedMaxSize = maxSize != null ? maxSize : -1;
FileSizeLimitExceededException exception = new FileUploadBase.FileSizeLimitExceededException(exceptionMessage, getRequestSize(request), allowedMaxSize);
LocalizedMessage message = buildErrorMessage(exception, new Object[]{fileName, getRequestSize(request), allowedMaxSize});
@@ -323,7 +325,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
values.add(fieldValue);
} catch (IOException e) {
LOG.warn("Failed to handle form field '{}'.", fieldName, e);
LOG.warn("Failed to handle form field [{}]", normalizeSpace(fieldName), e);
}
}
@@ -336,7 +338,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
protected void processFileItemStreamAsFileField(FileItemStream itemStream, String location) {
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (itemStream.getName() == null || itemStream.getName().trim().isEmpty()) {
LOG.debug("No file has been uploaded for the field: {}", itemStream.getFieldName());
LOG.debug("No file has been uploaded for the field: {}", normalizeSpace(itemStream.getFieldName()));
return;
}
@@ -353,7 +355,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
try {
file.delete();
} catch (SecurityException se) {
LOG.warn("Failed to delete '{}' due to security exception above.", file.getName(), se);
LOG.warn("Failed to delete [{}] due to security exception above.", normalizeSpace(file.getName()), se);
}
}
}
@@ -385,7 +387,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest {
}
File file = File.createTempFile(prefix + "_", suffix, new File(location));
LOG.debug("Creating temporary file '{}' (originally '{}').", file.getName(), fileName);
LOG.debug("Creating temporary file [{}] (originally [{}]).", file.getName(), normalizeSpace(fileName));
return file;
}
@@ -116,7 +116,7 @@ public abstract class AbstractFileUploadInterceptor extends AbstractInterceptor
}
// If it's null the upload failed
if (file == null) {
if (file == null || file.getContent() == null) {
String errMsg = getTextMessage(action, STRUTS_MESSAGES_ERROR_UPLOADING_KEY, new String[]{inputName});
if (validation != null) {
validation.addFieldError(inputName, errMsg);
@@ -0,0 +1,84 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.csp;
import org.apache.struts2.util.ValueStack;
/**
* Reads the nonce value using the ValueStack, {@link StrutsCspNonceReader} is the default implementation
* @since 6.8.0
*/
public interface CspNonceReader {
NonceValue readNonceValue(ValueStack stack);
class NonceValue {
private final String nonceValue;
private final CspNonceSource source;
private NonceValue(String nonceValue, CspNonceSource source) {
this.nonceValue = nonceValue;
this.source = source;
}
public static NonceValue ofSession(String nonceValue) {
return new NonceValue(nonceValue, CspNonceSource.SESSION);
}
public static NonceValue ofRequest(String nonceValue) {
return new NonceValue(nonceValue, CspNonceSource.REQUEST);
}
public static NonceValue ofNullSession() {
return new NonceValue(null, CspNonceSource.SESSION);
}
public static NonceValue ofNullRequest() {
return new NonceValue(null, CspNonceSource.REQUEST);
}
public boolean isNonceValueSet() {
return nonceValue != null;
}
public String getNonceValue() {
return nonceValue;
}
public CspNonceSource getSource() {
return source;
}
@Override
public String toString() {
String displayNonce;
if (nonceValue != null && nonceValue.length() >= 4) {
displayNonce = String.format("nonceValue='%s**********'", nonceValue.substring(0, 4));
} else if (nonceValue != null) {
displayNonce = String.format("nonceValue='%s**********'", nonceValue);
} else {
displayNonce = "nonceValue='<null>'";
}
return "NonceValue{" +
displayNonce +
", source=" + source +
'}';
}
}
}
@@ -0,0 +1,27 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.csp;
/**
* Source of the nonce value
*/
public enum CspNonceSource {
REQUEST,
SESSION
}
@@ -45,11 +45,10 @@ public interface CspSettings {
String CSP_REPORT_TYPE = "application/csp-report";
/**
* @deprecated use {@link #addCspHeaders(HttpServletRequest, HttpServletResponse)} instead
* Adds CSP related headers to response based on request state (e.g., if session has been created)
*
* @since Struts 6.0.3
*/
@Deprecated
void addCspHeaders(HttpServletResponse response);
void addCspHeaders(HttpServletRequest request, HttpServletResponse response);
/**
@@ -18,15 +18,17 @@
*/
package org.apache.struts2.interceptor.csp;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.action.CspSettingsAware;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Objects;
import static java.lang.String.format;
@@ -43,68 +45,102 @@ import static java.lang.String.format;
*/
public class DefaultCspSettings implements CspSettings {
private final static Logger LOG = LogManager.getLogger(DefaultCspSettings.class);
private static final Logger LOG = LogManager.getLogger(DefaultCspSettings.class);
private static final String NONCE_KEY = "nonce";
private final SecureRandom sRand = new SecureRandom();
private CspNonceSource nonceSource = CspNonceSource.SESSION;
protected String reportUri;
protected String reportTo;
// default to reporting mode
protected String cspHeader = CSP_REPORT_HEADER;
@Override
public void addCspHeaders(HttpServletResponse response) {
throw new UnsupportedOperationException("Unsupported implementation, use #addCspHeaders(HttpServletRequest request, HttpServletResponse response)");
@Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false)
public void setNonceSource(String nonceSource) {
if (StringUtils.isBlank(nonceSource)) {
this.nonceSource = CspNonceSource.SESSION;
} else {
this.nonceSource = CspNonceSource.valueOf(nonceSource.toUpperCase());
}
}
@Override
public void addCspHeaders(HttpServletRequest request, HttpServletResponse response) {
if (this.nonceSource == CspNonceSource.SESSION) {
addCspHeadersWithSession(request, response);
} else if (this.nonceSource == CspNonceSource.REQUEST) {
addCspHeadersWithRequest(request, response);
} else {
LOG.warn("Unknown nonce source: {}, ignoring CSP settings", nonceSource);
}
}
private void addCspHeadersWithSession(HttpServletRequest request, HttpServletResponse response) {
if (isSessionActive(request)) {
LOG.trace("Session is active, applying CSP settings");
associateNonceWithSession(request);
response.setHeader(cspHeader, createPolicyFormat(request));
String nonceValue = generateNonceValue();
request.getSession().setAttribute(NONCE_KEY, nonceValue);
response.setHeader(cspHeader, createPolicyFormat(nonceValue));
} else {
LOG.trace("Session is not active, ignoring CSP settings");
LOG.debug("Session is not active, ignoring CSP settings");
}
}
private void addCspHeadersWithRequest(HttpServletRequest request, HttpServletResponse response) {
String nonceValue = generateNonceValue();
request.setAttribute(NONCE_KEY, nonceValue);
response.setHeader(cspHeader, createPolicyFormat(nonceValue));
}
private boolean isSessionActive(HttpServletRequest request) {
return request.getSession(false) != null;
}
private void associateNonceWithSession(HttpServletRequest request) {
String nonceValue = Base64.getUrlEncoder().encodeToString(getRandomBytes());
request.getSession().setAttribute("nonce", nonceValue);
private String generateNonceValue() {
return Base64.getUrlEncoder().encodeToString(getRandomBytes());
}
protected String createPolicyFormat(HttpServletRequest request) {
StringBuilder policyFormatBuilder = new StringBuilder()
.append(OBJECT_SRC)
.append(format(" '%s'; ", NONE))
.append(SCRIPT_SRC)
.append(" 'nonce-%s' ") // nonce placeholder
.append(format("'%s' ", STRICT_DYNAMIC))
.append(format("%s %s; ", HTTP, HTTPS))
.append(BASE_URI)
.append(format(" '%s'; ", NONE));
protected String createPolicyFormat(String nonceValue) {
StringBuilder builder = new StringBuilder()
.append(OBJECT_SRC)
.append(format(" '%s'; ", NONE))
.append(SCRIPT_SRC)
.append(format(" 'nonce-%s' ", nonceValue))
.append(format("'%s' ", STRICT_DYNAMIC))
.append(format("%s %s; ", HTTP, HTTPS))
.append(BASE_URI)
.append(format(" '%s'; ", NONE));
if (reportUri != null) {
policyFormatBuilder
.append(REPORT_URI)
.append(format(" %s; ", reportUri));
if(reportTo != null) {
policyFormatBuilder
builder
.append(REPORT_URI)
.append(format(" %s; ", reportUri));
if (reportTo != null) {
builder
.append(REPORT_TO)
.append(format(" %s; ", reportTo));
}
}
return format(policyFormatBuilder.toString(), getNonceString(request));
return builder.toString();
}
/**
* @deprecated since 6.8.0, for removal
*/
@Deprecated
protected String createPolicyFormat(HttpServletRequest request) {
throw new UnsupportedOperationException("Unsupported implementation, use createPolicyFormat(String) instead!");
}
/**
* @deprecated since 6.8.0, for removal
*/
@Deprecated
protected String getNonceString(HttpServletRequest request) {
Object nonce = request.getSession().getAttribute("nonce");
return Objects.toString(nonce);
throw new UnsupportedOperationException("Unsupported implementation, don't use!");
}
private byte[] getRandomBytes() {
@@ -133,10 +169,10 @@ public class DefaultCspSettings implements CspSettings {
@Override
public String toString() {
return "DefaultCspSettings{" +
"reportUri='" + reportUri + '\'' +
", reportTo='" + reportTo + '\'' +
", cspHeader='" + cspHeader + '\'' +
'}';
"reportUri='" + reportUri + '\'' +
", reportTo='" + reportTo + '\'' +
", cspHeader='" + cspHeader + '\'' +
'}';
}
}
@@ -0,0 +1,87 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.interceptor.csp;
import com.opensymphony.xwork2.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.util.ValueStack;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* Reads nonce value from session or request attribute.
* @since 6.8.0
*/
public class StrutsCspNonceReader implements CspNonceReader {
private static final Logger LOG = LogManager.getLogger(StrutsCspNonceReader.class);
private final CspNonceSource nonceSource;
@Inject(value = StrutsConstants.STRUTS_CSP_NONCE_SOURCE, required = false)
public StrutsCspNonceReader(String source) {
if (StringUtils.isBlank(source)) {
this.nonceSource = CspNonceSource.SESSION;
} else {
this.nonceSource = CspNonceSource.valueOf(source.toUpperCase());
}
}
@Override
public NonceValue readNonceValue(ValueStack stack) {
HttpServletRequest request = stack.getActionContext().getServletRequest();
NonceValue nonceValue;
if (nonceSource == CspNonceSource.SESSION) {
LOG.debug("Reading nonce value from session");
nonceValue = readNonceFromSession(request);
} else if (nonceSource == CspNonceSource.REQUEST) {
LOG.debug("Reading nonce value from request attribute");
nonceValue = readNonceFromRequest(request);
} else {
LOG.warn("Unknown nonce source: {}, reading nonce value from session", nonceSource);
nonceValue = readNonceFromSession(request);
}
return nonceValue;
}
private NonceValue readNonceFromSession(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Object nonceValue = session != null ? session.getAttribute("nonce") : null;
if (nonceValue == null) {
LOG.debug("Session is not active, cannot obtain nonce value");
return NonceValue.ofNullSession();
}
return NonceValue.ofSession(nonceValue.toString());
}
private NonceValue readNonceFromRequest(HttpServletRequest request) {
Object nonceValue = request.getAttribute("nonce");
if (nonceValue == null) {
LOG.warn("Request attribute 'nonce' is not set, cannot obtain nonce value");
return NonceValue.ofNullRequest();
}
return NonceValue.ofRequest(nonceValue.toString());
}
}
@@ -29,6 +29,8 @@ import org.apache.struts2.dispatcher.AttributeMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.PageContext;
import static org.apache.struts2.dispatcher.DispatcherConstants.ATTRIBUTES;
public class TagUtils {
private static final Logger LOG = LogManager.getLogger(TagUtils.class);
@@ -46,8 +48,8 @@ public class TagUtils {
} else {
LOG.trace("Adds the current PageContext to ActionContext");
stack.getActionContext()
.withPageContext(pageContext)
.with("attr", new AttributeMap(stack.getContext()));
.withPageContext(pageContext)
.with(ATTRIBUTES, new AttributeMap(stack.getContext()));
}
return stack;
@@ -280,4 +280,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
+3
View File
@@ -265,4 +265,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>
@@ -29,6 +29,7 @@
<constant name="struts.excludedClasses"
value="
com.opensymphony.xwork2.ActionContext,
org.apache.struts2.ActionContext,
java.lang.Class,
java.lang.ClassLoader,
java.lang.InheritableThreadLocal,
@@ -75,10 +76,15 @@
value="
antlr.build,
com.opensymphony.xwork2.config,
org.apache.struts2.config,
com.opensymphony.xwork2.inject,
org.apache.struts2.inject,
com.opensymphony.xwork2.ognl,
org.apache.struts2.ognl,
com.opensymphony.xwork2.security,
org.apache.struts2.security,
com.opensymphony.xwork2.util,
org.apache.struts2.util,
freemarker.core,
freemarker.ext.jsp,
freemarker.ext.rhino,
@@ -108,10 +114,15 @@
value="
antlr.build,
com.opensymphony.xwork2.config,
org.apache.struts2.config,
com.opensymphony.xwork2.inject,
org.apache.struts2.inject,
com.opensymphony.xwork2.ognl,
org.apache.struts2.ognl,
com.opensymphony.xwork2.security,
org.apache.struts2.security,
com.opensymphony.xwork2.util,
org.apache.struts2.util,
freemarker.core,
freemarker.ext.jsp,
freemarker.ext.rhino,
@@ -165,14 +165,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">Set the value used to retrieve the option value.</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -117,14 +117,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">String that will be appended to the label</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -117,14 +117,6 @@
<td class="tag-attribute">String</td>
<td class="tag-attribute">String that will be appended to the label</td>
</tr>
<tr>
<td class="tag-attribute">maxLength</td>
<td class="tag-attribute">false</td>
<td class="tag-attribute"></td>
<td class="tag-attribute">false</td>
<td class="tag-attribute">Integer</td>
<td class="tag-attribute">Deprecated. Use maxlength instead.</td>
</tr>
<tr>
<td class="tag-attribute">maxlength</td>
<td class="tag-attribute">false</td>
@@ -28,7 +28,9 @@ import java.util.concurrent.Callable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
@@ -49,6 +51,7 @@ public class ContainerImplTest {
ContainerBuilder cb = new ContainerBuilder();
cb.constant("methodCheck.name", "Lukasz");
cb.constant("fieldCheck.name", "Lukasz");
cb.constant("constructorCheck.name", "Lukasz");
cb.factory(EarlyInitializable.class, EarlyInitializableBean.class, Scope.SINGLETON);
cb.factory(Initializable.class, InitializableBean.class, Scope.SINGLETON);
cb.factory(EarlyInitializable.class, "prototypeEarlyInitializable", EarlyInitializableBean.class, Scope.PROTOTYPE);
@@ -65,15 +68,43 @@ public class ContainerImplTest {
}
@Test
public void fieldInjector() throws Exception {
public void fieldInjector() {
FieldCheck fieldCheck = new FieldCheck();
c.inject(fieldCheck);
assertEquals(fieldCheck.getName(), "Lukasz");
assertEquals("Lukasz", fieldCheck.getName());
}
@Test
public void methodInjector() throws Exception {
c.inject(new MethodCheck());
public void methodInjector() {
MethodCheck methodCheck = new MethodCheck();
c.inject(methodCheck);
assertEquals("Lukasz", methodCheck.getName());
}
@Test
public void constructorInjector() {
ConstructorCheck constructorCheck = c.inject(ConstructorCheck.class);
assertEquals("Lukasz", constructorCheck.getName());
}
@Test
public void optionalConstructorInjector() {
OptionalConstructorCheck constructorCheck = c.inject(OptionalConstructorCheck.class);
assertNull(constructorCheck.getName());
}
@Test
public void requiredOptionalConstructorInjector() {
RequiredOptionalConstructorCheck constructorCheck = c.inject(RequiredOptionalConstructorCheck.class);
assertNotNull(constructorCheck.getExistingName());
assertNull(constructorCheck.getNonExitingName());
}
@Test
public void optionalRequiredConstructorInjector() {
OptionalRequiredConstructorCheck constructorCheck = c.inject(OptionalRequiredConstructorCheck.class);
assertNull(constructorCheck.getNonExitingName());
assertNotNull(constructorCheck.getExistingName());
}
/**
@@ -92,7 +123,7 @@ public class ContainerImplTest {
* Inject values into method under SecurityManager
*/
@Test
public void testMethodInjectorWithSecurityEnabled() throws Exception {
public void testMethodInjectorWithSecurityEnabled() {
assumeTrue(SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_20));
System.setSecurityManager(new TestSecurityManager());
assertThrows(DependencyException.class, () -> c.inject(new MethodCheck()));
@@ -101,7 +132,7 @@ public class ContainerImplTest {
}
@Test
public void testEarlyInitializable() throws Exception {
public void testEarlyInitializable() {
assertTrue("should being initialized already", EarlyInitializableBean.initializedEarly);
EarlyInitializableCheck earlyInitializableCheck = new EarlyInitializableCheck();
@@ -148,22 +179,19 @@ public class ContainerImplTest {
final InitializableCheck initializableCheck3 = new InitializableCheck();
final TestScopeStrategy testScopeStrategy = new TestScopeStrategy();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ContainerBuilder cb2 = new ContainerBuilder();
cb2.factory(EarlyInitializable.class, EarlyInitializableBean.class, Scope.SINGLETON);
cb2.factory(Initializable.class, InitializableBean.class, Scope.SINGLETON);
cb2.factory(EarlyInitializable.class, "prototypeEarlyInitializable", EarlyInitializableBean.class, Scope.PROTOTYPE);
cb2.factory(Initializable.class, "prototypeInitializable", InitializableBean.class, Scope.PROTOTYPE);
cb2.factory(Initializable.class, "requestInitializable", InitializableBean.class, Scope.REQUEST);
cb2.factory(Initializable.class, "sessionInitializable", InitializableBean.class, Scope.SESSION);
cb2.factory(Initializable.class, "threadInitializable", InitializableBean.class, Scope.THREAD);
cb2.factory(Initializable.class, "wizardInitializable", InitializableBean.class, Scope.WIZARD);
Container c2 = cb2.create(false);
c2.setScopeStrategy(testScopeStrategy);
c2.inject(initializableCheck3);
}
Thread thread = new Thread(() -> {
ContainerBuilder cb2 = new ContainerBuilder();
cb2.factory(EarlyInitializable.class, EarlyInitializableBean.class, Scope.SINGLETON);
cb2.factory(Initializable.class, InitializableBean.class, Scope.SINGLETON);
cb2.factory(EarlyInitializable.class, "prototypeEarlyInitializable", EarlyInitializableBean.class, Scope.PROTOTYPE);
cb2.factory(Initializable.class, "prototypeInitializable", InitializableBean.class, Scope.PROTOTYPE);
cb2.factory(Initializable.class, "requestInitializable", InitializableBean.class, Scope.REQUEST);
cb2.factory(Initializable.class, "sessionInitializable", InitializableBean.class, Scope.SESSION);
cb2.factory(Initializable.class, "threadInitializable", InitializableBean.class, Scope.THREAD);
cb2.factory(Initializable.class, "wizardInitializable", InitializableBean.class, Scope.WIZARD);
Container c2 = cb2.create(false);
c2.setScopeStrategy(testScopeStrategy);
c2.inject(initializableCheck3);
});
thread.run();
thread.join();
@@ -205,6 +233,76 @@ public class ContainerImplTest {
}
public static class ConstructorCheck {
private String name;
@Inject("constructorCheck.name")
public ConstructorCheck(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static class OptionalConstructorCheck {
private String name;
@Inject(value = "nonExistingConstant", required = false)
public OptionalConstructorCheck(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public static class RequiredOptionalConstructorCheck {
private final String existingName;
private final String nonExitingName;
@Inject(required = false)
public RequiredOptionalConstructorCheck(
@Inject("constructorCheck.name") String existingName,
@Inject(value = "nonExistingConstant", required = false) String nonExitingName
) {
this.existingName = existingName;
this.nonExitingName = nonExitingName;
}
public String getExistingName() {
return existingName;
}
public String getNonExitingName() {
return nonExitingName;
}
}
public static class OptionalRequiredConstructorCheck {
private final String existingName;
private final String nonExitingName;
@Inject(required = false)
public OptionalRequiredConstructorCheck(
@Inject(value = "nonExistingConstant", required = false) String nonExitingName,
@Inject("constructorCheck.name") String existingName
) {
this.existingName = existingName;
this.nonExitingName = nonExitingName;
}
public String getExistingName() {
return existingName;
}
public String getNonExitingName() {
return nonExitingName;
}
}
class InitializableCheck {
private Initializable initializable;
@@ -1233,6 +1233,34 @@ public class OgnlValueStackTest extends XWorkTestCase {
assertNull("accessed private field (result not null) ?", accessedValue);
}
public void testFindValueWithConstructorAndProxyChecks() {
Map<String, String> properties = new HashMap<>();
properties.put(StrutsConstants.STRUTS_DISALLOW_PROXY_OBJECT_ACCESS, Boolean.TRUE.toString());
properties.put(StrutsConstants.STRUTS_DISALLOW_PROXY_MEMBER_ACCESS, Boolean.TRUE.toString());
loadButSet(properties);
refreshContainerFields();
String value = "test";
String ognlResult = (String) vs.findValue(
"new com.opensymphony.xwork2.ognl.OgnlValueStackTest$ValueHolder('" + value + "').value", String.class);
assertEquals(value, ognlResult);
}
@SuppressWarnings({"unused"})
public static class ValueHolder {
// See testFindValueWithConstructorAndProxyChecks
private final String value;
public ValueHolder(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
static class BadJavaBean {
private int count;
private int count2;
@@ -93,7 +93,7 @@ public class DefaultExcludedPatternsCheckerTest extends XWorkTestCase {
public void testDefaultExcludePatterns() throws Exception {
// given
List<String> prefixes = Arrays.asList("#[0].%s", "[0].%s", "top.%s", "%{[0].%s}", "%{#[0].%s}", "%{top.%s}", "%{#top.%s}", "%{#%s}", "%{%s}", "#%s");
List<String> prefixes = Arrays.asList("#[0].%s", "[0].%s", "top.%s", "%{[0].%s}", "%{#[0].%s}", "%{top.%s}", "%{#top.%s}", "%{#%s}", "%{%s}", "#%s", "top.param", "top.request");
List<String> inners = Arrays.asList("servletRequest", "servletResponse", "servletContext", "application", "session", "struts", "request", "response", "dojo", "parameters");
List<String> suffixes = Arrays.asList("['test']", "[\"test\"]", ".test");
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.opensymphony.xwork2.test;
public class StrutsUtilBean {
public String getMyString() {
return "myString";
}
public boolean getMyBoolean(boolean bool) {
return bool;
}
public String getFoo() {
return "bar";
}
public String getBarExpression() {
return "%{bar}";
}
public String getBar() {
return "bar";
}
}
@@ -0,0 +1,88 @@
/*
* 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.components;
import org.apache.struts2.ActionContext;
import org.apache.struts2.dispatcher.DispatcherConstants;
import org.apache.struts2.views.jsp.AbstractTagTest;
import java.util.Map;
import static org.apache.struts2.components.UIBean.TEMPLATE_DIR;
import static org.apache.struts2.components.UIBean.THEME;
public class UIBeanTagTest extends AbstractTagTest {
private UIBean bean;
@Override
public void setUp() throws Exception {
super.setUp();
bean = new UIBean(stack, request, response) {
@Override
protected String getDefaultTemplate() {
return null;
}
};
}
public void testTemplateDir_ognlExpression() {
bean.setTemplateDir("%{testDir}");
stack.push(new Object() {
public String getTestDir() {
return "testValue";
}
});
assertEquals("testValue", bean.getTemplateDir());
}
public void testTemplateDir_attrMapFallback() {
ActionContext.of(context).getApplication().put(TEMPLATE_DIR, "applicationValue");
assertEquals("applicationValue", bean.getTemplateDir());
ActionContext.of(context).getSession().put(TEMPLATE_DIR, "sessionValue");
assertEquals("sessionValue", bean.getTemplateDir());
((Map<String, Object>) context.get(DispatcherConstants.REQUEST)).put(TEMPLATE_DIR, "requestValue");
assertEquals("requestValue", bean.getTemplateDir());
}
public void testTheme_ognlExpression() {
bean.setTheme("%{testTheme}");
stack.push(new Object() {
public String getTestTheme() {
return "testValue";
}
});
assertEquals("testValue", bean.getTheme());
}
public void testTheme_attrMapFallback() {
ActionContext.of(context).getApplication().put(THEME, "applicationValue");
assertEquals("applicationValue", bean.getTheme());
ActionContext.of(context).getSession().put(THEME, "sessionValue");
assertEquals("sessionValue", bean.getTheme());
((Map<String, Object>) context.get(DispatcherConstants.REQUEST)).put(THEME, "requestValue");
assertEquals("requestValue", bean.getTheme());
}
}
@@ -21,6 +21,7 @@ package org.apache.struts2.components;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.util.ValueStack;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.components.template.Template;
import org.apache.struts2.components.template.TemplateEngine;
@@ -32,6 +33,7 @@ import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import static com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsCheckerTest.NO_EXCLUSION_ACCEPT_ALL_PATTERNS_CHECKER;
@@ -236,6 +238,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setAccesskey(accesskeyValue);
txtFld.evaluateParams();
@@ -250,6 +254,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.addParameter("value", value);
txtFld.evaluateParams();
@@ -338,6 +344,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setCssClass(cssClass);
txtFld.evaluateParams();
@@ -352,6 +360,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
ActionContext.getContext().withServletRequest(req);
TextField txtFld = new TextField(stack, req, res);
container.inject(txtFld);
txtFld.setStyle(cssStyle);
txtFld.evaluateParams();
@@ -372,6 +382,8 @@ public class UIBeanTest extends StrutsInternalTestCase {
actionContext.withSession(new SessionMap(req));
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
@@ -392,11 +404,35 @@ public class UIBeanTest extends StrutsInternalTestCase {
session.invalidate();
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertNull(dblSelect.getAttributes().get("nonce"));
}
public void testNonceOfRequestAttribute() {
Map<String, String> params = new HashMap<String, String>(){{
put(StrutsConstants.STRUTS_CSP_NONCE_SOURCE, "request");
}};
initDispatcher(params);
String nonceVal = "r4nd0m";
ValueStack stack = ActionContext.getContext().getValueStack();
MockHttpServletRequest req = new MockHttpServletRequest();
req.setAttribute("nonce", nonceVal);
MockHttpServletResponse res = new MockHttpServletResponse();
ActionContext actionContext = stack.getActionContext();
actionContext.withServletRequest(req);
DoubleSelect dblSelect = new DoubleSelect(stack, req, res);
container.inject(dblSelect);
dblSelect.evaluateParams();
assertEquals(nonceVal, dblSelect.getAttributes().get("nonce"));
}
public void testSetNullUiStaticContentPath() {
// given
ValueStack stack = ActionContext.getContext().getValueStack();
@@ -40,6 +40,11 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class AttributeMapTest {
@@ -360,4 +365,21 @@ public class AttributeMapTest {
assertEquals("value", value);
}
}
@Test
public void get_whenPageContextHasNoRequest() {
PageContext pageContext = mock(PageContext.class);
when(pageContext.getRequest()).thenReturn(null);
HttpServletRequest req = new MockHttpServletRequest();
req.setAttribute("attr", "reqValue");
AttributeMap attributeMap = new AttributeMap(new HashMap<String, Object>() {{
put(StrutsStatics.PAGE_CONTEXT, pageContext);
put(DispatcherConstants.REQUEST, new RequestMap(req));
}});
assertEquals("reqValue", attributeMap.get("attr"));
verify(pageContext, never()).findAttribute(anyString());
}
}
@@ -343,6 +343,9 @@ public class DispatcherTest extends StrutsJUnit4InternalTestCase {
req.setContentType("Multipart/Form-Data ; boundary=---------------------------207103069210263;charset=UTF-16LE");
assertTrue(dispatcher.isMultipartRequest(req));
req.setContentType("multipart/form-data; boundary=\"----=_Part_38_1092302434.1734807780737\"");
assertTrue(dispatcher.isMultipartRequest(req));
}
@Test
@@ -0,0 +1,183 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.struts2.dispatcher.multipart;
import org.apache.commons.fileupload.FileItem;
import org.apache.struts2.StrutsInternalTestCase;
import org.springframework.mock.web.MockHttpServletRequest;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* Test cases for {@link JakartaMultiPartRequest} that verify security-related functionality,
* specifically comprehensive cleanup of temporary files.
*/
public class JakartaMultiPartRequestTest extends StrutsInternalTestCase {
private File tempDir;
private JakartaMultiPartRequest multiPartRequest;
@Override
protected void setUp() throws Exception {
super.setUp();
// Create a temporary directory for test files
Path tempPath = Files.createTempDirectory("struts-multipart-test");
tempDir = tempPath.toFile();
multiPartRequest = new TestableJakartaMultiPartRequest();
multiPartRequest.setMaxSize("2048");
multiPartRequest.setMaxFiles("10");
multiPartRequest.setMaxFileSize("1024");
}
@Override
protected void tearDown() throws Exception {
if (tempDir != null && tempDir.exists()) {
// Clean up test directory
File[] files = tempDir.listFiles();
if (files != null) {
for (File file : files) {
file.delete();
}
}
tempDir.delete();
}
super.tearDown();
}
/**
* Test that comprehensive cleanup removes all temporary files created during multipart processing.
* This addresses the security vulnerability where temporary files could be leaked.
*/
public void testComprehensiveCleanupRemovesAllTempFiles() throws Exception {
// Create a mock multipart request with both file upload and form field
MockHttpServletRequest request = new MockHttpServletRequest();
request.setContentType("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW");
request.setMethod("POST");
String multipartContent = createMultipartContent();
request.setContent(multipartContent.getBytes(StandardCharsets.UTF_8));
// Count files before processing
int filesBefore = countTempFiles();
// Process the multipart request
multiPartRequest.parse(request, tempDir.getAbsolutePath());
// Count files after processing (should be more due to temp files)
int filesAfterProcessing = countTempFiles();
// Verify that temp files were created during processing
assertTrue("Temporary files should be created during multipart processing",
filesAfterProcessing > filesBefore);
// Perform cleanup
multiPartRequest.cleanUp();
// Count files after cleanup
int filesAfterCleanup = countTempFiles();
// Verify comprehensive cleanup removed all temporary files
assertEquals("All temporary files should be cleaned up", filesBefore, filesAfterCleanup);
}
/**
* Test that all FileItem instances are tracked for cleanup, including both file uploads and form fields.
*/
public void testAllFileItemsAreTrackedForCleanup() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setContentType("multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW");
request.setMethod("POST");
String multipartContent = createMultipartContentWithMultipleFields();
request.setContent(multipartContent.getBytes(StandardCharsets.UTF_8));
// Process the multipart request
multiPartRequest.parse(request, tempDir.getAbsolutePath());
// Access the tracked items through our testable implementation
TestableJakartaMultiPartRequest testable = (TestableJakartaMultiPartRequest) multiPartRequest;
List<FileItem> trackedItems = testable.getAllFileItems();
// Verify that all items (both files and form fields) are tracked
assertTrue("Should track multiple FileItem instances", trackedItems.size() >= 3);
// Verify tracking includes both form fields and file uploads
boolean hasFormField = false;
boolean hasFileUpload = false;
for (FileItem item : trackedItems) {
if (item.isFormField()) {
hasFormField = true;
} else {
hasFileUpload = true;
}
}
assertTrue("Should track form field items", hasFormField);
assertTrue("Should track file upload items", hasFileUpload);
}
private String createMultipartContent() {
return "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"textField\"\r\n\r\n" +
"test value\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"fileField\"; filename=\"test.txt\"\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"file content\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n";
}
private String createMultipartContentWithMultipleFields() {
return "------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"textField1\"\r\n\r\n" +
"value1\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"textField2\"\r\n\r\n" +
"value2\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW\r\n" +
"Content-Disposition: form-data; name=\"fileField\"; filename=\"test.txt\"\r\n" +
"Content-Type: text/plain\r\n\r\n" +
"file content\r\n" +
"------WebKitFormBoundary7MA4YWxkTrZu0gW--\r\n";
}
private int countTempFiles() {
if (tempDir == null || !tempDir.exists()) {
return 0;
}
File[] files = tempDir.listFiles();
return files != null ? files.length : 0;
}
/**
* Testable subclass that exposes internal state for verification
*/
private static class TestableJakartaMultiPartRequest extends JakartaMultiPartRequest {
public List<FileItem> getAllFileItems() {
return allFileItems;
}
}
}
@@ -98,6 +98,53 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
}
};
public static final UploadedFile NULL_CONTENT = new UploadedFile() {
@Override
public Long length() {
return 0L;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getAbsolutePath() {
return null;
}
@Override
public File getContent() {
return null;
}
@Override
public String getOriginalName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public String getInputName() {
return null;
}
};
private ActionFileUploadInterceptor interceptor;
private File tempDir;
@@ -205,6 +252,22 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithNoContent() {
interceptor.setAllowedTypes("text/plain");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, NULL_CONTENT, "filename.html", "text/plain", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
List<String> errors = validation.getFieldErrors().get("inputName");
assertEquals(1, errors.size());
String msg = errors.get(0);
assertTrue(msg.startsWith("Error uploading:"));
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithMaxSize() throws Exception {
interceptor.setMaximumSize(10L);
@@ -672,7 +735,7 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
"file" +
"\"; filename=\"" +
filename +
"\r\n" +
"\"\r\n" +
"Content-Type: " +
contentType +
"\r\n" +
@@ -697,7 +760,6 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
}
private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxfilesize, int maxfiles, int maxStringLength) {
JakartaMultiPartRequest jak = new JakartaMultiPartRequest();
jak.setMaxSize(String.valueOf(maxsize));
jak.setMaxFileSize(String.valueOf(maxfilesize));
@@ -707,7 +769,6 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase {
}
private MultiPartRequestWrapper createMultipartRequestNoMaxParamsSet(HttpServletRequest req) {
JakartaMultiPartRequest jak = new JakartaMultiPartRequest();
return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider());
}
@@ -312,7 +312,7 @@ public class CspInterceptorTest extends StrutsInternalTestCase {
*/
public static class CustomDefaultCspSettings extends DefaultCspSettings {
protected String createPolicyFormat(HttpServletRequest request) {
protected String createPolicyFormat(String nonceValue) {
return "foo";
}
}
@@ -100,6 +100,53 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
}
};
public static final UploadedFile NULL_CONTENT = new UploadedFile() {
@Override
public Long length() {
return 0L;
}
@Override
public String getName() {
return "";
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean delete() {
return false;
}
@Override
public String getAbsolutePath() {
return null;
}
@Override
public File getContent() {
return null;
}
@Override
public String getOriginalName() {
return null;
}
@Override
public String getContentType() {
return null;
}
@Override
public String getInputName() {
return null;
}
};
private FileUploadInterceptor interceptor;
private File tempDir;
@@ -202,6 +249,22 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithNoContent() {
interceptor.setAllowedTypes("text/plain");
ValidationAwareSupport validation = new ValidationAwareSupport();
boolean notOk = interceptor.acceptFile(validation, NULL_CONTENT, "filename.html", "text/plain", "inputName");
assertFalse(notOk);
assertFalse(validation.getFieldErrors().isEmpty());
assertTrue(validation.hasErrors());
List<String> errors = validation.getFieldErrors().get("inputName");
assertEquals(1, errors.size());
String msg = errors.get(0);
assertTrue(msg.startsWith("Error uploading:"));
assertTrue(msg.indexOf("inputName") > 0);
}
public void testAcceptFileWithMaxSize() throws Exception {
interceptor.setMaximumSize(10L);
@@ -736,7 +799,7 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
"file" +
"\"; filename=\"" +
filename +
"\r\n" +
"\"\r\n" +
"Content-Type: " +
contentType +
"\r\n" +
@@ -761,18 +824,18 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase {
}
private MultiPartRequestWrapper createMultipartRequest(HttpServletRequest req, int maxsize, int maxfilesize, int maxfiles, int maxStringLength) {
JakartaMultiPartRequest jak = new JakartaMultiPartRequest();
jak.setMaxSize(String.valueOf(maxsize));
jak.setMaxFileSize(String.valueOf(maxfilesize));
jak.setMaxFiles(String.valueOf(maxfiles));
jak.setMaxStringLength(String.valueOf(maxStringLength));
return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider());
}
private MultiPartRequestWrapper createMultipartRequestNoMaxParamsSet(HttpServletRequest req) {
JakartaMultiPartRequest jak = new JakartaMultiPartRequest();
return new MultiPartRequestWrapper(jak, req, tempDir.getAbsolutePath(), new DefaultLocaleProvider());
}
@@ -116,9 +116,9 @@ public class ParametersInterceptorTest extends XWorkTestCase {
pi.setParameters(action, vs, HttpParameters.create(params).build());
// then
assertEquals(3, action.getActionErrors().size());
assertEquals(3, action.getActionMessages().size());
List<String> actionErrors = new ArrayList<>(action.getActionErrors());
List<String> actionErrors = new ArrayList<>(action.getActionMessages());
String msg1 = actionErrors.get(0);
String msg2 = actionErrors.get(1);
@@ -203,9 +203,9 @@ public class ParametersInterceptorTest extends XWorkTestCase {
pi.setParameters(action, vs, HttpParameters.create(params).build());
// then
assertEquals(3, action.getActionErrors().size());
assertEquals(3, action.getActionMessages().size());
List<String> actionErrors = new ArrayList<>(action.getActionErrors());
List<String> actionErrors = new ArrayList<>(action.getActionMessages());
String msg1 = actionErrors.get(0);
String msg2 = actionErrors.get(1);
String msg3 = actionErrors.get(2);
@@ -585,7 +585,7 @@ public class ParametersInterceptorTest extends XWorkTestCase {
container.inject(config.getInterceptors().get(0).getInterceptor());
ActionProxy proxy = actionProxyFactory.createActionProxy("", MockConfigurationProvider.PARAM_INTERCEPTOR_ACTION_NAME, null, extraContext.getContextMap());
proxy.execute();
final String actionError = "" + ((SimpleAction) proxy.getAction()).getActionErrors().toArray()[0];
final String actionError = "" + ((SimpleAction) proxy.getAction()).getActionMessages().toArray()[0];
assertTrue(actionError.contains("Error setting expression 'not_a_property' with value 'There is no action property named like this'"));
}
@@ -19,6 +19,7 @@
package org.apache.struts2.util;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.test.StrutsUtilBean;
import com.opensymphony.xwork2.util.ValueStack;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.TestAction;
@@ -37,7 +38,6 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* Test case for StrutsUtil.
*
*/
public class StrutsUtilTest extends StrutsInternalTestCase {
@@ -53,14 +53,7 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
}
public void testIsTrueMethod() {
stack.push(new Object() {
public String getMyString() {
return "myString";
}
public boolean getMyBoolean(boolean bool) {
return bool;
}
});
stack.push(new StrutsUtilBean());
assertTrue(strutsUtil.isTrue("myString == 'myString'"));
assertFalse(strutsUtil.isTrue("myString == 'myOtherString'"));
assertTrue(strutsUtil.isTrue("getMyBoolean(true)"));
@@ -68,14 +61,7 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
}
public void testFindStringMethod() {
stack.push(new Object() {
public String getMyString() {
return "myString";
}
public boolean getMyBoolean(boolean bool) {
return bool;
}
});
stack.push(new StrutsUtilBean());
assertEquals(strutsUtil.findString("myString"), "myString");
assertNull(strutsUtil.findString("myOtherString"));
@@ -105,14 +91,7 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
public void testFindValueMethod() throws Exception {
stack.push(new Object() {
public String getMyString() {
return "myString";
}
public boolean getMyBoolean(boolean bool) {
return bool;
}
});
stack.push(new StrutsUtilBean());
Object obj1 = strutsUtil.findValue("myString", "java.lang.String");
Object obj2 = strutsUtil.findValue("getMyBoolean(true)", "java.lang.Boolean");
@@ -125,7 +104,6 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
}
public void testGetTextMethod() {
// this should be in xwork-messages.properties (included by default
// by LocalizedTextUtil
@@ -233,11 +211,7 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
}
public void testTranslateVariables() {
stack.push(new Object() {
public String getFoo() {
return "bar";
}
});
stack.push(new StrutsUtilBean());
String obj1 = strutsUtil.translateVariables("try: %{foo}");
assertNotNull(obj1);
@@ -245,15 +219,8 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
}
public void testTranslateVariablesRecursion() {
stack.push(new Object() {
public String getFoo() {
return "%{bar}";
}
public String getBar() {
return "bar";
}
});
String obj1 = strutsUtil.translateVariables("try: %{foo}");
stack.push(new StrutsUtilBean());
String obj1 = strutsUtil.translateVariables("try: %{barExpression}");
assertNotNull(obj1);
assertEquals("try: %{bar}", obj1);
@@ -277,12 +244,10 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
super.tearDown();
}
// === internal class to assist in testing
protected static class InternalMockHttpServletRequest extends MockHttpServletRequest {
InternalMockRequestDispatcher dispatcher = null;
public RequestDispatcher getRequestDispatcher(String path) {
dispatcher = new InternalMockRequestDispatcher(path);
return dispatcher;
@@ -296,10 +261,12 @@ public class StrutsUtilTest extends StrutsInternalTestCase {
protected static class InternalMockRequestDispatcher extends MockRequestDispatcher {
private final String url;
boolean included = false;
public InternalMockRequestDispatcher(String url) {
super(url);
this.url = url;
}
public void include(ServletRequest servletRequest, ServletResponse servletResponse) {
if (servletResponse instanceof MockHttpServletResponse) {
((MockHttpServletResponse) servletResponse).setIncludedUrl(this.url);
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-async-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
+1 -1
View File
@@ -25,7 +25,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-cdi-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-config-browser-plugin</artifactId>
@@ -19,12 +19,12 @@
package org.apache.struts2.config_browser;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.reflection.ReflectionContextFactory;
import com.opensymphony.xwork2.util.reflection.ReflectionException;
import com.opensymphony.xwork2.util.reflection.ReflectionProvider;
import com.opensymphony.xwork2.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 +49,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 +79,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 +90,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 +107,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>6.7.0</version>
<version>6.8.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>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-dwr-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-embeddedjsp-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-gxp-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-jasperreports-plugin</artifactId>
+1 -1
View File
@@ -25,7 +25,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-javatemplates-plugin</artifactId>
@@ -23,6 +23,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.ActionError;
import org.apache.struts2.components.Anchor;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import java.util.ArrayList;
import java.util.List;
@@ -72,6 +74,7 @@ public class ActionErrorTest extends AbstractTest {
//errors are needed to setup stack
super.setUp();
this.tag = new ActionError(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -23,6 +23,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.ActionError;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import java.util.ArrayList;
import java.util.List;
@@ -72,6 +74,7 @@ public class ActionMessageTest extends AbstractTest {
//errors are needed to setup stack
super.setUp();
this.tag = new ActionError(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -24,6 +24,8 @@ import com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsChecke
import org.apache.struts2.components.Anchor;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.components.ServletUrlRenderer;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class AnchorTest extends AbstractTest {
private Anchor tag;
@@ -111,6 +113,7 @@ public class AnchorTest extends AbstractTest {
this.tag = new Anchor(stack, request, response);
this.tag.setUrlRenderer(new ServletUrlRenderer());
this.tag.setNotExcludedAcceptedPatterns(new DefaultNotExcludedAcceptedPatternsChecker());
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -23,6 +23,8 @@ package org.apache.struts2.views.java.simple;
import com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsChecker;
import org.apache.struts2.components.Checkbox;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class CheckboxTest extends AbstractCommonAttributesTest {
private Checkbox tag;
@@ -88,6 +90,7 @@ public class CheckboxTest extends AbstractCommonAttributesTest {
super.setUp();
tag = new Checkbox(stack, request, response);
tag.setNotExcludedAcceptedPatterns(new DefaultNotExcludedAcceptedPatternsChecker());
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.DateTextField;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class DateTextFieldTest extends AbstractCommonAttributesTest {
@@ -56,6 +58,7 @@ public class DateTextFieldTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new DateTextField(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.FieldError;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import java.util.*;
@@ -100,6 +102,7 @@ public class FieldErrorTest extends AbstractTest {
//errors are needed to setup stack
super.setUp();
this.tag = new FieldError(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.File;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class FileTest extends AbstractCommonAttributesTest {
private File tag;
@@ -50,6 +52,7 @@ public class FileTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new File(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -23,6 +23,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Form;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.components.UrlRenderer;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import org.easymock.EasyMock;
public class FormTest extends AbstractCommonAttributesTest {
@@ -80,5 +82,6 @@ public class FormTest extends AbstractCommonAttributesTest {
UrlRenderer renderer = EasyMock.createNiceMock(UrlRenderer.class);
EasyMock.replay(renderer);
tag.setUrlRenderer(renderer);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
}
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Head;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class HeadTest extends AbstractTest {
private Head tag;
@@ -39,6 +41,7 @@ public class HeadTest extends AbstractTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Head(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Hidden;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class HiddenTest extends AbstractTest {
private Hidden tag;
@@ -49,6 +51,7 @@ public class HiddenTest extends AbstractTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Hidden(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Label;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class LabelTest extends AbstractCommonAttributesTest {
private Label tag;
@@ -48,6 +50,7 @@ public class LabelTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Label(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -20,6 +20,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Link;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class LinkTest extends AbstractTest {
@@ -103,5 +105,6 @@ public class LinkTest extends AbstractTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Link(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
}
@@ -22,15 +22,14 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Password;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class PasswordTest extends AbstractCommonAttributesTest {
private Password tag;
public void testRenderPassword() throws Exception {
super.setUp();
this.tag = new Password(stack, request, response);
tag.setName("name");
tag.setValue("val1");
tag.setSize("10");
@@ -51,9 +50,6 @@ public class PasswordTest extends AbstractCommonAttributesTest {
}
public void testRenderPasswordShowIt() throws Exception {
super.setUp();
this.tag = new Password(stack, request, response);
tag.setName("name");
tag.setValue("val1");
tag.setSize("10");
@@ -75,7 +71,9 @@ public class PasswordTest extends AbstractCommonAttributesTest {
@Override
protected void setUp() throws Exception {
//dont call base setup
super.setUp();
this.tag = new Password(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -85,8 +83,6 @@ public class PasswordTest extends AbstractCommonAttributesTest {
@Override
protected UIBean getUIBean() throws Exception {
super.setUp();
this.tag = new Password(stack, request, response);
return tag;
}
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Reset;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class ResetTest extends AbstractCommonAttributesTest {
private Reset tag;
@@ -69,6 +71,7 @@ public class ResetTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Reset(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -21,6 +21,8 @@ package org.apache.struts2.views.java.simple;
import com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsChecker;
import org.apache.struts2.components.Script;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class ScriptTest extends AbstractTest {
@@ -72,5 +74,6 @@ public class ScriptTest extends AbstractTest {
this.tag = new Script(stack, request, response);
tag.setNotExcludedAcceptedPatterns(new DefaultNotExcludedAcceptedPatternsChecker());
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
}
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Select;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import java.util.Arrays;
import java.util.HashMap;
@@ -114,6 +116,7 @@ public class SelectTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Select(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.Submit;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class SubmitTest extends AbstractCommonAttributesTest {
private Submit tag;
@@ -154,6 +156,7 @@ public class SubmitTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Submit(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.TextArea;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class TextAreaTest extends AbstractCommonAttributesTest {
private TextArea tag;
@@ -71,6 +73,7 @@ public class TextAreaTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new TextArea(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -22,6 +22,8 @@ package org.apache.struts2.views.java.simple;
import org.apache.struts2.components.TextField;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
public class TextFieldTest extends AbstractCommonAttributesTest {
private TextField tag;
@@ -52,6 +54,7 @@ public class TextFieldTest extends AbstractCommonAttributesTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new TextField(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
}
@Override
@@ -23,6 +23,8 @@ package org.apache.struts2.views.java.simple;
import com.opensymphony.xwork2.ActionContext;
import org.apache.struts2.components.Token;
import org.apache.struts2.components.UIBean;
import org.apache.struts2.interceptor.csp.CspNonceSource;
import org.apache.struts2.interceptor.csp.StrutsCspNonceReader;
import java.util.HashMap;
import java.util.regex.Pattern;
@@ -49,6 +51,7 @@ public class TokenTest extends AbstractTest {
protected void setUp() throws Exception {
super.setUp();
this.tag = new Token(stack, request, response);
this.tag.setCspNonceReader(new StrutsCspNonceReader(CspNonceSource.SESSION.name()));
ActionContext.of()
.withSession(new HashMap<>())
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-jfreechart-plugin</artifactId>
+1 -1
View File
@@ -24,7 +24,7 @@
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>6.7.0</version>
<version>6.8.0</version>
</parent>
<artifactId>struts2-json-plugin</artifactId>

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