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