mirror of
https://github.com/apache/struts.git
synced 2026-08-31 19:35:40 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8135547c7a | |||
| 76b2800d18 | |||
| 8373599f01 | |||
| 70341ac8b0 | |||
| d304ea3ffd | |||
| 97d6872beb | |||
| 042e6c0422 | |||
| 157cc6583c | |||
| 13e976f502 | |||
| 411ffc127a | |||
| 2c9a23eaef | |||
| 4a6c4ad085 | |||
| 53d2f7ea4a | |||
| d176c997bf | |||
| d543af34cc | |||
| 845f524255 | |||
| 6b9175c510 | |||
| 0a70a1e477 | |||
| 29b1593301 | |||
| 77d7527d51 | |||
| 4253375672 | |||
| 344eaf3e33 | |||
| 9a278838c2 | |||
| d0f1acad94 | |||
| 44c2413ad2 | |||
| f796b25f33 | |||
| 862a6ed36e | |||
| 0a1d4d7728 | |||
| 1b3f6102f6 | |||
| 2ab4926795 | |||
| b734e6e2e4 | |||
| 2f101588c9 | |||
| 30359e5f62 | |||
| 3a14ddcaba | |||
| cae4e442ed | |||
| b9d365a04c | |||
| 34993c7473 | |||
| 6a46ca7fcd | |||
| 13b136c559 | |||
| 909f724288 | |||
| 60b108e05e | |||
| 60f1622f3d | |||
| d62437ee75 | |||
| 2543a00e62 | |||
| e83468aee5 | |||
| 20f7cdc513 | |||
| 652cd4a899 | |||
| 9b8f0a99c5 | |||
| 8df1a49e44 | |||
| 1e769cc3b3 | |||
| 299579aef2 | |||
| 85c9e4c171 | |||
| 6d3cceca00 | |||
| 2154ad8513 | |||
| fa754949ab | |||
| 9d71021b14 | |||
| b6be295074 | |||
| 13e6ae21d4 | |||
| 1e73567cec | |||
| 8fa480d66b | |||
| 8910d90721 | |||
| db2bf7b4e3 | |||
| 77cb215263 | |||
| 51d985b3b7 | |||
| 3563fe21e7 | |||
| 8f589c03db | |||
| bb8d0bf28a | |||
| 09f11dac5c | |||
| fbe7fe226f | |||
| 4e78cfce78 | |||
| 004ab6c47c | |||
| ff7144ca42 | |||
| ae053fb669 | |||
| f621683dc1 | |||
| 209965eff0 | |||
| 0a64f616e8 | |||
| 6f97753eec | |||
| 46dd4d4ca6 | |||
| 98d4313537 | |||
| 231e27fd5e | |||
| 08f5faf1af | |||
| 1be016ae02 | |||
| 0a6c72e0c7 | |||
| dceee365c0 | |||
| eca621ef35 | |||
| fdcfcb8c15 | |||
| 422c84f511 | |||
| 16939ed8b1 | |||
| b39e1f24c2 | |||
| a1e9ba6aa4 | |||
| 7da5f114b4 | |||
| 4fd5d2ab15 | |||
| fe3d2c2228 | |||
| e75a5ae946 | |||
| a4e91a9581 | |||
| 5c1593c02e | |||
| 28a98b5757 | |||
| df14a287d0 | |||
| 60391db64f | |||
| 6692879894 | |||
| 9452a5c591 | |||
| a181296a17 | |||
| 94d7466015 | |||
| c6c51c6ba4 | |||
| f13243a2bf | |||
| b2496a566c | |||
| 756c2f58d5 | |||
| cac778c170 | |||
| 66479bc36e | |||
| e023f033af | |||
| 8199b1a3b7 | |||
| d078084107 | |||
| c990786a59 | |||
| 3f7dd2ef82 | |||
| 7d8dfff8a1 | |||
| 129e4559ce | |||
| ff9bfe3bd4 | |||
| e163098224 | |||
| dd200c173c | |||
| d7189a6648 | |||
| 2cab82fafc | |||
| 1472a49a0f | |||
| 5e3464d467 | |||
| d60e674046 | |||
| 6e81a692b7 | |||
| fbedbdedbe | |||
| 1a2bd58e65 | |||
| 421ff8d068 | |||
| 0e7dbe0b62 | |||
| ae4e489d9e | |||
| dcc79d485d | |||
| 75b379d5d4 | |||
| cb580fe614 |
@@ -1,52 +0,0 @@
|
||||
# Documentation https://s.apache.org/asfyaml
|
||||
notifications:
|
||||
commits: commits@struts.apache.org
|
||||
# Send all issue emails (new, closed, comments) to issues@
|
||||
issues: issues@struts.apache.org
|
||||
# Send new/closed PR notifications to commits@
|
||||
pullrequests_status: notifications@struts.apache.org
|
||||
# Send individual PR comments/reviews to issues@
|
||||
pullrequests_comment: notifications@struts.apache.org
|
||||
# Link opened PRs with JIRA
|
||||
jira_options: link label worklog
|
||||
|
||||
github:
|
||||
description: "Apache Struts is a free, open-source, MVC framework for creating elegant, modern Java web applications"
|
||||
homepage: https://struts.apache.org/
|
||||
protected_branches:
|
||||
main:
|
||||
# contexts are the names of checks that must pass.
|
||||
required_status_checks:
|
||||
contexts:
|
||||
- "Build and Test (JDK 17)"
|
||||
required_pull_request_reviews:
|
||||
# it does not work because our github teams are private/secret, see INFRA-25666
|
||||
require_code_owner_reviews: false
|
||||
required_approving_review_count: 0
|
||||
support/struts-6-x-x:
|
||||
# contexts are the names of checks that must pass.
|
||||
required_status_checks:
|
||||
contexts:
|
||||
- "Build and Test (8)"
|
||||
required_pull_request_reviews:
|
||||
# it does not work because our github teams are private/secret, see INFRA-25666
|
||||
require_code_owner_reviews: false
|
||||
required_approving_review_count: 0
|
||||
support/release-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
|
||||
required_approving_review_count: 0
|
||||
pull_requests:
|
||||
# allow auto-merge
|
||||
allow_auto_merge: true
|
||||
# auto-delete head branches after being merged
|
||||
del_branch_on_merge: true
|
||||
autolink_jira:
|
||||
- WW
|
||||
dependabot_alerts: true
|
||||
dependabot_updates: true
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebSearch",
|
||||
"WebFetch(domain:struts.apache.org)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)",
|
||||
"WebFetch(domain:issues.apache.org)",
|
||||
"WebFetch(domain:freemarker.apache.org)",
|
||||
"Bash(mvn:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git checkout:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(gh pr view:*)",
|
||||
"Bash(gh pr diff:*)",
|
||||
"Bash(gh pr create:*)",
|
||||
"mcp__jetbrains"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
---
|
||||
name: creating-release-vote-mail
|
||||
description: Use when opening the formal release vote for a Struts release candidate on any maintenance line (6.x, 7.x) - composing and drafting the [VOTE] Apache Struts X.Y.Z mail to dev@ once the Version Notes page, GitHub release and staged artifacts are published.
|
||||
---
|
||||
|
||||
# Creating a Release Vote Mail
|
||||
|
||||
## Overview
|
||||
|
||||
The `[VOTE]` mail opens the formal release vote. It is four links wrapped in frozen ASF
|
||||
boilerplate, around a plain-text rendering of the release's Version Notes page.
|
||||
|
||||
**Core principle:** the mail is a *rendering* of the page, not a second account of the release.
|
||||
|
||||
**This is the step after `creating-version-notes`.** That skill produces the page, the GitHub
|
||||
release and the `[TEST]` announcement; this one consumes all three. If they do not exist yet,
|
||||
you are in the wrong skill. `releasing-struts` holds the surrounding phases and what happens
|
||||
once the vote passes.
|
||||
|
||||
[`vote-mail-template.md`](vote-mail-template.md) is the source of truth for the artifact.
|
||||
|
||||
## The mail is exactly these parts, in this order
|
||||
|
||||
1. The two-sentence opener
|
||||
2. The page's `Breaking changes`, `Deprecations` and `Rejected requests`, where present
|
||||
3. The page's issue-type sections, in page order
|
||||
4. The four link lines
|
||||
5. The vote boilerplate
|
||||
6. The sign-off
|
||||
|
||||
**A part not on this list is not in the mail, and one mail is produced, not two.** Every
|
||||
section is the page's content; the opener is the only prose you write.
|
||||
|
||||
The pull here is toward helpfulness — an upgrade-notes section derived from the fix commits, a
|
||||
summary of what changed for integrators, a companion note to a subset of recipients. All of it
|
||||
is real work that belongs somewhere else. A vote is a judgement on the staged artifacts, and
|
||||
the page is what describes them.
|
||||
|
||||
## The vote carries no security information
|
||||
|
||||
No severity, no CVE, no S2-XXX, no bulletin link, no attack description, no reporter or
|
||||
coordination detail. **That disclosure happens after the vote passes and the version is
|
||||
released.**
|
||||
|
||||
**The restriction is on the vote, not on the audience.** Routing advisory detail through
|
||||
`private@`, a Cc, an attachment or a companion mail is the same violation as putting it in the
|
||||
body — a second mail sent to open the vote is part of the vote. "The recipients already hold
|
||||
this information" is not an exemption; the vote is simply not the vehicle.
|
||||
|
||||
Neutral ticket summaries carried from the page are not security information. Keep them exactly
|
||||
as the page has them, including where the page truncates one at a clause boundary.
|
||||
|
||||
**REQUIRED BACKGROUND:** `creating-security-bulletins` governs what may be said, and when.
|
||||
|
||||
## Recipients
|
||||
|
||||
```
|
||||
To: dev@struts.apache.org
|
||||
Bcc: private@struts.apache.org
|
||||
```
|
||||
|
||||
**`user@struts.apache.org` must not appear in any header — not To, not Cc, not Bcc.** The
|
||||
`[TEST]` mail one step earlier goes to `dev@` and `user@`, which is right for it: it asks
|
||||
people to test. This mail asks people to *vote*, and a vote invitation on the user list
|
||||
solicits votes that are not binding and scatters the tally across two lists.
|
||||
|
||||
Cc is not a compromise. If a release manager asks you to include the user list, the answer is
|
||||
that the `[TEST]` mail already did.
|
||||
|
||||
**`private@` is on the mail for reach, not for confidentiality.** Not every PMC member follows
|
||||
`dev@`, and PMC votes are the binding ones, so `private@` is what guarantees the binding voters
|
||||
see the call. Nothing goes there that could not go to `dev@` — its presence is a delivery
|
||||
decision, and it is not an exemption from the rule above.
|
||||
|
||||
It goes on **Bcc, not Cc**: on the 7.1.1 and 6.8.0 votes it was on Cc, and reply-all `+1`s
|
||||
landed on the private PMC list. Bcc gives the same reach while keeping the tally in one thread
|
||||
on `dev@`.
|
||||
|
||||
Subject is exactly `[VOTE] Apache Struts X.Y.Z`.
|
||||
|
||||
## Draft it, do not send it
|
||||
|
||||
Create a Gmail draft with To, Bcc, Subject and body set, and write the same body to a file whose
|
||||
path you hand over. **Never send.**
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "The release manager authorised whatever I produce" | Authorisation to compose is not authorisation to transmit. |
|
||||
| "Every fact is verified; review would catch nothing" | Sending is not a quality gate, it is a commitment. Verification does not confer it. |
|
||||
| "A draft doesn't open the vote, which defeats the request" | Correct, and that is the right outcome when the release manager is not there to send it. |
|
||||
| "The 72-hour clock is the reason for the hurry" | A vote opened on the wrong artifacts costs far more than the hours saved. |
|
||||
|
||||
Sending opens a binding vote on a permanently archived public list and commits the PMC to the
|
||||
artifacts as staged.
|
||||
|
||||
### The mail must be text/plain, and no tool argument achieves that
|
||||
|
||||
An ASF list mail carries **one `text/plain` part and nothing else**. A `text/html` part is a
|
||||
delivery failure, not a cosmetic one — `announce@apache.org` rejects it permanently:
|
||||
|
||||
```
|
||||
ezmlm-reject: fatal: Sorry, a message part has an unacceptable MIME Content-Type: 'text/html' (#5.2.3)
|
||||
```
|
||||
|
||||
**A draft created through the Gmail tool is an HTML draft, whatever you pass it.** Gmail
|
||||
synthesises a `text/html` alternative when the draft is sent, linkifies every URL into
|
||||
`<a href>`, and reflows the plain part:
|
||||
|
||||
| Body passed as | What is actually sent |
|
||||
|---|---|
|
||||
| `body` only | `multipart/alternative` — the HTML part is generated for you |
|
||||
| `htmlBody` only | HTML-only, no plain part at all |
|
||||
| both | Same, plus the plain part's visible text becomes the wrapped URL |
|
||||
|
||||
Gmail's linkifier also rewrites URLs server-side, so link lines can arrive as
|
||||
`https://www.google.com/url?q=...&source=gmail&ust=...`.
|
||||
|
||||
**Do not rely on a bounce to catch this.** On the 2026-08-14 advisory run the same message was
|
||||
rejected by `announce@apache.org` and accepted by `user@struts.apache.org` — the HTML mail
|
||||
reached one list and not the other, from a single send.
|
||||
|
||||
So the deliverable is three things, and it is incomplete without any of them:
|
||||
|
||||
1. A Gmail draft with To, Bcc, Subject and `body`. **Never `htmlBody`.**
|
||||
2. The identical body written to a file, whose path you hand over.
|
||||
3. In your handover, the sending instruction: **switch the compose window to plain-text mode**
|
||||
(⋮ → *Plain text mode*), then select-all and paste the file over the body.
|
||||
|
||||
Step 3 is what actually produces the plain-text mail; steps 1 and 2 only make it one paste
|
||||
instead of four hand-edited URLs. Hard-wrap the file at 72 columns — a paste into plain-text
|
||||
mode keeps the wrapping the file has, and Gmail reflows anything longer.
|
||||
|
||||
**Say which identity to send from: `@apache.org`.** The Gmail account's default sender is a
|
||||
personal address, and a vote arriving in the `dev@` archive from one reads as an outsider
|
||||
calling a PMC vote. Some ASF lists refuse it outright — `announcements@struts.apache.org`
|
||||
answers *"Must be sent from an @apache.org address."*
|
||||
|
||||
**Never re-run the draft-update tool on a draft whose links have already been fixed by hand** —
|
||||
it re-mangles them. A draft the release manager has corrected is finished; leave it alone.
|
||||
|
||||
## The boilerplate is frozen
|
||||
|
||||
Everything from `Once you have had a chance to review the test build` to the sign-off is
|
||||
byte-identical to the template. **Inserting a paragraph between existing ones is an edit** —
|
||||
that is how it actually gets broken, not by rewording.
|
||||
|
||||
If something about this release needs explaining to voters, it belongs in the opener, above
|
||||
the vote call. The vote call itself says the same thing every release, which is what lets a
|
||||
voter skim to the checkboxes.
|
||||
|
||||
## What this skill does not restate
|
||||
|
||||
Cross-references, not copies:
|
||||
|
||||
- `creating-version-notes` — the page, the GitHub release, the `[TEST]` mail, and what belongs
|
||||
on them. The issue list, Breaking changes wording and ticket reconciliation are settled
|
||||
there; render what the page says.
|
||||
- `creating-security-bulletins` — what may be said about an unpublished advisory.
|
||||
|
||||
Before drafting, confirm all four links resolve and the GitHub release is still a prerelease.
|
||||
A vote opened on a 404 burns the window before anyone can test.
|
||||
|
||||
## Red Flags — STOP
|
||||
|
||||
- Any part in the mail that is not on the six-item list
|
||||
- A second mail produced alongside the vote
|
||||
- Severity, CVE, S2-XXX, bulletin link or reporter detail anywhere, on any channel
|
||||
- `user@struts.apache.org` in any header, including Cc
|
||||
- Sending rather than drafting
|
||||
- `htmlBody` passed to the draft tool, for any reason
|
||||
- A draft handed over without the plain-text-mode instruction and the body file
|
||||
- A new paragraph inserted into the vote boilerplate
|
||||
- A quality checkbox arriving pre-ticked
|
||||
- An opening sentence carried over from the previous release
|
||||
- Re-running the draft-update tool on a draft whose links were already fixed by hand
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Reality |
|
||||
|---|---|
|
||||
| "Voters can't judge fixes they can't see" | They can open the restricted bulletins themselves. The vote is not the disclosure channel. |
|
||||
| "It's only going to private@, so nothing leaks" | The rule is about the vote, not the audience. A companion mail is part of the vote. |
|
||||
| "private@ is on the mail already, so it's a channel I can use" | It is there so binding voters see the call, not to carry anything `dev@` cannot. |
|
||||
| "Cc'ing user@ keeps the vote on dev@ and still informs them" | The `[TEST]` mail informed them. Cc splits the tally. |
|
||||
| "I verified everything, so I can send" | Verification earns a draft. Sending is the release manager's keystroke. |
|
||||
| "I'm adding to the boilerplate, not changing it" | Insertion is editing. The vote call is byte-frozen. |
|
||||
| "The release notes leave out what integrators need" | Then the page needs fixing. The mail renders the page. |
|
||||
| "Last release's opening sentence fits" | It described last release. Write the one this list supports. |
|
||||
| "I passed `body`, not `htmlBody`, so it's plain text" | Gmail generates the HTML part itself. The format is set in the compose window. |
|
||||
| "It went through last time, so HTML is tolerated" | Lists differ. One accepted the same message the other rejected. |
|
||||
@@ -1,152 +0,0 @@
|
||||
# Release Vote Mail Template
|
||||
|
||||
The canonical skeleton for the `[VOTE] Apache Struts X.Y.Z` mail that opens a release vote.
|
||||
Companion to [`SKILL.md`](SKILL.md), which covers *how* to fill the slots; this file covers
|
||||
*what the mail contains*.
|
||||
|
||||
**This file is the source of truth.** Start every vote mail from the skeleton below.
|
||||
|
||||
## Slots
|
||||
|
||||
| Slot | What goes in it |
|
||||
|---|---|
|
||||
| `<X.Y.Z>` | The release being voted on, dotted — subject, opening sentence, Version Notes URL, dist path |
|
||||
| `<X_Y_Z>` | The same version underscored, for the `STRUTS_` git tag only |
|
||||
| `<SHAPE SENTENCE>` | See below — authored per release |
|
||||
| Page sections | Breaking changes, Deprecations, Rejected requests, and the issue-type sections, copied from the Version Notes page. Omit any the page omits. |
|
||||
|
||||
**Where the page introduces a section with a sentence of its own, that sentence comes with it.**
|
||||
The skeleton below shows sections as bare lists, but `Rejected requests` on the 7.3.0 page opens
|
||||
with *"Two long-standing requests were closed as Won't Do in this cycle. They are listed here so
|
||||
the decision is visible rather than silent."* Without it the section reads as two unexplained
|
||||
ticket ids.
|
||||
|
||||
## The shape sentence is authored per release
|
||||
|
||||
The opener is two sentences. The first is fixed. The second describes the *shape* of the issue
|
||||
list — how the release is composed — and is written from the list in front of you.
|
||||
|
||||
| The page has | Second sentence |
|
||||
|---|---|
|
||||
| No Breaking changes | `With this release the following issues were addressed:` |
|
||||
| Breaking changes | `This release contains <what>:` |
|
||||
|
||||
**Do not reuse a previous release's wording.** 7.2.1's *"a few minor breaking changes plus some
|
||||
bug fixes. Also a lot of dependencies have been updated"* describes 7.2.1. Applied to 7.3.0 —
|
||||
seven breaking changes, one dependency bump — both halves are false.
|
||||
|
||||
Name no individual ticket here. The list below is the detail.
|
||||
|
||||
## Skeleton
|
||||
|
||||
```
|
||||
Subject: [VOTE] Apache Struts <X.Y.Z>
|
||||
To: dev@struts.apache.org
|
||||
Bcc: private@struts.apache.org
|
||||
|
||||
The Apache Struts <X.Y.Z> test build is available. <SHAPE SENTENCE>
|
||||
|
||||
Breaking changes
|
||||
|
||||
- <page item, verbatim, ending [WW-XXXX].>
|
||||
|
||||
Deprecations
|
||||
|
||||
- <page item, verbatim, ending [WW-XXXX].>
|
||||
|
||||
Rejected requests
|
||||
|
||||
[WW-XXXX] - <page item, verbatim>
|
||||
|
||||
Bug
|
||||
[WW-XXXX] - <summary>
|
||||
|
||||
New Feature
|
||||
[WW-XXXX] - <summary>
|
||||
|
||||
Improvement
|
||||
[WW-XXXX] - <summary>
|
||||
|
||||
Task
|
||||
[WW-XXXX] - <summary>
|
||||
|
||||
Dependency
|
||||
[WW-XXXX] - <summary>
|
||||
|
||||
Release notes:
|
||||
* https://cwiki.apache.org/confluence/display/WW/Version+Notes+<X.Y.Z>
|
||||
|
||||
Github release
|
||||
* https://github.com/apache/struts/releases/tag/STRUTS_<X_Y_Z>
|
||||
|
||||
Distribution:
|
||||
* https://dist.apache.org/repos/dist/dev/struts/<X.Y.Z>/
|
||||
|
||||
Maven 2 staging repository:
|
||||
* https://repository.apache.org/content/groups/staging/
|
||||
|
||||
Once you have had a chance to review the test build, please respond
|
||||
with a vote on its quality:
|
||||
|
||||
[ ] Leave at test build
|
||||
[ ] Alpha
|
||||
[ ] Beta
|
||||
[ ] General Availability (GA)
|
||||
|
||||
Everyone who has tested the build is invited to vote. Votes by PMC
|
||||
members are considered binding. A vote passes if there are at least
|
||||
three binding +1s and more +1s than -1s.
|
||||
|
||||
The vote will remain open for at least 72 hours, longer upon request.
|
||||
A vote can be amended at any time to upgrade or downgrade the quality
|
||||
of the release based on future experience. If an initial vote
|
||||
designates the build as "Beta", the release will be submitted for
|
||||
mirroring and announced to the user list. Once released as a public
|
||||
beta, subsequent quality votes on a build may be held on the user
|
||||
list.
|
||||
|
||||
As always, the act of voting carries certain obligations. A binding
|
||||
vote not only states an opinion, but means that the voter is agreeing
|
||||
to help do the work.
|
||||
|
||||
On behalf of the Apache Struts project
|
||||
Łukasz
|
||||
```
|
||||
|
||||
Hard-wrap the body at 72 columns, continuation lines unindented, so the list stays legible in
|
||||
the ASF archives and in quoted replies. Gmail reflows the wrap, rewrites the link lines, and
|
||||
adds a `text/html` part that ASF lists reject — see *The mail must be text/plain* in
|
||||
[`SKILL.md`](SKILL.md) for why the body also has to be handed over as a file, and what the
|
||||
release manager has to do in the compose window before sending.
|
||||
|
||||
## Frozen text
|
||||
|
||||
Everything from `Once you have had a chance to review the test build` to the sign-off is
|
||||
byte-frozen. It is the vote call itself: the options voters tick, the binding threshold, the
|
||||
72-hour minimum, and what a binding vote commits the voter to.
|
||||
|
||||
Details that look like defects and are kept:
|
||||
|
||||
| Detail | Why |
|
||||
|---|---|
|
||||
| `Github release` has no trailing colon | Every archived Struts vote mail reads this way |
|
||||
| All four checkboxes empty | The release manager's `+1 (binding)` is a separate reply |
|
||||
|
||||
The staging URL is `content/groups/staging/`, matching the Version Notes page and the `[TEST]`
|
||||
mail. The group repo also resolves released transitive dependencies, which the bare staging
|
||||
repository does not.
|
||||
|
||||
## Pre-draft checklist
|
||||
|
||||
- [ ] All four links resolve; the GitHub release is still flagged pre-release
|
||||
- [ ] Ticket ids in the mail match the page's exactly, both directions
|
||||
- [ ] Boilerplate byte-identical to the frozen text above
|
||||
- [ ] `To: dev@` only; `user@` absent from every header; `Bcc: private@` present
|
||||
- [ ] Subject is exactly `[VOTE] Apache Struts X.Y.Z`
|
||||
- [ ] All four checkboxes empty
|
||||
- [ ] No severity, CVE, S2-XXX, bulletin link or reporter detail anywhere
|
||||
- [ ] Exactly one mail
|
||||
- [ ] Body also saved to a file and its path handed over, so the mangled links and wrap can be
|
||||
fixed with one paste
|
||||
- [ ] `htmlBody` not passed to the draft tool
|
||||
- [ ] Handover states: plain-text mode on, paste the file, send from the `@apache.org` identity
|
||||
@@ -1,300 +0,0 @@
|
||||
---
|
||||
name: creating-security-bulletins
|
||||
description: Use when drafting, updating, or reviewing an S2-XXX security bulletin on the Struts cwiki, when preparing bulletin text ahead of a CVE request, when publishing a bulletin and announcing it to the ASF lists, or when deciding how much detail about a fixed vulnerability is safe to publish.
|
||||
---
|
||||
|
||||
# Creating Security Bulletins
|
||||
|
||||
## Overview
|
||||
|
||||
An S2-XXX bulletin exists to tell an operator **what to upgrade and why** — not to explain the defect. Every sentence that helps a defender must be weighed against how much it helps someone building an exploit.
|
||||
|
||||
**Core principle:** every field is either traced to source you read this session, or a visible placeholder. Never a plausible guess.
|
||||
|
||||
**Process authority:** [`SECURITY.md`](../../../SECURITY.md) governs disclosure. This skill governs *what the page says and how it is written*.
|
||||
|
||||
**REQUIRED BACKGROUND:** the claims you put in a bulletin come from triage. Use `triaging-security-reports` to establish them before writing.
|
||||
|
||||
## The Iron Rule
|
||||
|
||||
```
|
||||
NO FIELD IN A BULLETIN WITHOUT A SOURCE YOU READ THIS SESSION,
|
||||
OR A VISIBLE PLACEHOLDER.
|
||||
```
|
||||
|
||||
Applies to the severity rating, the affected versions, and above all the Workaround. "There is no workaround" is a factual claim about absence — the hardest kind to get right, and the most common thing to assert without checking.
|
||||
|
||||
## Page structure
|
||||
|
||||
Sections in order, matching the existing published bulletins:
|
||||
|
||||
`Summary` (in an `excerpt` macro) → field table → `Problem` → `Solution` → `Backward compatibility` → `Workaround`
|
||||
|
||||
Field table rows, in order:
|
||||
|
||||
| Row | Content |
|
||||
|---|---|
|
||||
| Who should read this | Usually `All Struts 2 developers and users`; narrow it only when exposure is genuinely conditional |
|
||||
| Impact of vulnerability | A short impact phrase, not a paragraph — *Remote Code Execution*, *Denial of service*, *Disclosure of Data, Denial of Service, Server Side Request Forgery*. Hedging is accepted where warranted (*Possible Remote Code Execution vulnerability*) |
|
||||
| Maximum security rating | Low / Moderate / Important / Critical — see the rating scale below |
|
||||
| Recommendation | `Upgrade to Struts X.Y.Z at least`. Name **every** maintenance line that carries the fix (`Upgrade to Struts 6.8.0 or 7.1.1 at least`), and add the required action where upgrading alone is not enough (`… and use Action File Upload Interceptor`) |
|
||||
| Affected Software | Officially released versions only (see below); bullet one range per maintenance line, linking the EOL announcement for end-of-life ranges |
|
||||
| Reporters | Credit the reporter — they earned it, and it costs nothing. Include their organisation where they gave one (`Steven Seeley of Source Incite`); obfuscate any email (`pwntester at github dot com`) |
|
||||
| CVE Identifier | Placeholder until assigned (see below) |
|
||||
|
||||
**Match the house voice — from the *recent* bulletins only.** Read the two or three most recently published ones before writing. They are far terser than a triage write-up: `Problem` is one to three sentences, and every affected feature is **linked to its page on struts.apache.org** so an operator can go straight to the documentation. Where a bulletin resembles an earlier one, the Summary says so and links it.
|
||||
|
||||
**Do not take the older bulletins as a precedent for how much to disclose.** Earlier advisories explained causes and mitigations in far more detail, and that detail was used to build working exploits. The project deliberately stopped. An old bulletin naming the exact construct that triggers the flaw is evidence of the practice this skill exists to prevent, not licence to repeat it — mine them for structure and tone, never for depth.
|
||||
|
||||
## Affected Software: released versions only
|
||||
|
||||
**List only versions that passed a PMC release vote.** A build that was cut, failed its test period, and was superseded never reached users as a release — listing it implies an official artifact was vulnerable and drags a phantom version into every downstream CVE record and scanner database.
|
||||
|
||||
Verify before writing. Do not infer the range from the tags in git: a tag exists for builds that were never voted through. Ask, or check the release announcements.
|
||||
|
||||
**Deriving the lower bound** — one method, both bounds:
|
||||
|
||||
1. Find when the vulnerable code entered, with `git log -S'<the vulnerable construct>' -- <path>`. Do not assume it arrived with the feature that made it reachable; a defect often predates the control that was supposed to bound it.
|
||||
2. Map that commit to the first *release* containing it.
|
||||
3. If step 2 can't be settled from what you have, write a visible placeholder naming what must be confirmed — never a guessed version number.
|
||||
|
||||
## The rating scale is published — apply it, don't invent one
|
||||
|
||||
The definitions live on **[Security Bulletins](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=61758)** (page `61758`), and they answer one question: *how worried should I be about this vulnerability?*
|
||||
|
||||
**That page is the only authority.** The four-level naming was introduced comparatively recently, so bulletins published before it use other wording and inconsistent capitalisation. Never infer the vocabulary or calibrate a rating from an older bulletin — match a definition on page `61758`, and take comparisons only from advisories published since the scale existed.
|
||||
|
||||
| Rating | Applies when |
|
||||
|---|---|
|
||||
| **Critical** | A remote attacker can get Struts to execute arbitrary code — exploitable automatically, regardless of whether the developer followed the Security Guide |
|
||||
| **Important** | Compromise of the application's **data or availability**; also easy RCE that depends on the developer having mistreated user input |
|
||||
| **Moderate** | There is **significant mitigation**: the flaw does not affect likely configurations, or the configuration is not widely used, or the attacker must be authenticated |
|
||||
| **Low** | Everything else — believed **extremely hard to exploit**, or the exploit yields minimal consequences |
|
||||
|
||||
Two traps in applying it:
|
||||
|
||||
- **Low is not "narrow".** A flaw that is trivial to trigger and causes real damage is not Low merely because a setting gates it. Reserve Low for hard-to-exploit *or* minimal-consequence.
|
||||
- **The Moderate clause is "not widely used", not "opt-in".** A gate only mitigates if few deployments pass through it. S2-068 needed file upload enabled and was still rated **Important**, because file upload is ordinary. Ask how many real deployments the precondition actually excludes.
|
||||
- **Availability counts as Important.** Denial of service is not automatically a lesser class — S2-068 was disk exhaustion, rated Important. It drops to Moderate only where a mitigation clause genuinely applies.
|
||||
|
||||
**Exploitation status belongs on the page, not in the rating.** The scale measures the flaw itself, so it has no slot for "a public reproduction already exists." When a defect was disclosed publicly before the fix shipped, or a working reproduction is already public, say so in plain words — downstream consumers are told by their own regulators to prioritise on real risk and active exploitation, not on a severity class alone. It costs nothing: the reproduction is already out.
|
||||
|
||||
## CVE placeholder
|
||||
|
||||
CVEs are requested **after** the fixed release is out and accepted. Until then the row carries a placeholder that cannot be mistaken for a real identifier:
|
||||
|
||||
```
|
||||
CVE-YYYY-NNNNN (to be assigned before publication)
|
||||
```
|
||||
|
||||
Never leave a cloned page's real CVE in place. Never invent a well-formed-looking number.
|
||||
|
||||
One CVE per independently fixable issue — separate fixes get separate bulletins and separate CVEs, per [CNA rules 4.1.10](https://www.cve.org/ResourcesSupport/AllResources/CNARules).
|
||||
|
||||
## The disclosure budget
|
||||
|
||||
**The budget covers every prose section — `Problem`, `Backward compatibility`, and `Workaround` alike.** `Problem` is the section authors guard; `Backward compatibility` is the one that leaks, because describing what changed about the fixed behaviour describes the defect. A note saying which inputs are handled differently now points straight at the code path that was rewritten. Apply the table below to all three sections, and write the BC note in terms of what an application might *observe*, never what the fix altered internally.
|
||||
|
||||
Write the shortest true description that lets an operator judge whether they are exposed. One to three sentences, as in the published bulletins.
|
||||
|
||||
| Safe to publish | Never publish before the fix is out |
|
||||
|---|---|
|
||||
| Impact categories and consequence | Class, method, or field names |
|
||||
| The component in plain words, linked to its documentation | `file:line` references |
|
||||
| Whether a configured control fails to apply | Commit hashes, PR or Jira numbers |
|
||||
| That state is shared / input is unvalidated | The triggering request shape or payload |
|
||||
| Which released versions are affected | Reproduction steps, PoC, timing conditions |
|
||||
|
||||
**Write for an operator, not a reviewer.** S2-068 describes an exploited disk-exhaustion bug in one sentence — *"If support for file upload is enabled, file leak in multipart request processing causes disk exhaustion."* That is the register: the feature, the failure, the consequence. Naming the class turns a bulletin into a starting point.
|
||||
|
||||
## State who is *not* affected
|
||||
|
||||
An operator's first question is "does this reach me?" Answer it on the page, or every reader has to assume it does.
|
||||
|
||||
The house form is **one sentence, linked to the feature's documentation** — S2-067 does it in a single line:
|
||||
|
||||
> **Note**: applications not using [FileUploadInterceptor](https://struts.apache.org/core-developers/file-upload-interceptor) are safe.
|
||||
|
||||
or folded into the opening clause, as S2-068 does with *"If support for file upload is enabled, …"*. Say it whenever exposure is conditional — an optional plugin the application chooses to ship, a setting that must be switched on, an endpoint that must be mapped, or an unaffected sibling path that lets a reader stop reading. Add "earlier releases are not affected" when there is a clean prior baseline.
|
||||
|
||||
Keep it at the level of a deployment decision ("uses the plugin", "exposes such an endpoint"), not a code path. Scoping *reduces* net disclosure: it shrinks the population that has to care, and it costs an attacker nothing they could not learn from the dependency list.
|
||||
|
||||
## Fix provenance
|
||||
|
||||
A bulletin promises a fixed release and describes post-fix behaviour as settled fact. Both claims rest on a specific change.
|
||||
|
||||
**Record which commit or PR each behavioural claim rests on**, in the version comment or your notes — not on the page.
|
||||
|
||||
**Confirm that change is merged into the release branch before publishing.** A patch under private review may be revised or dropped; a bulletin describing behaviour that never shipped is worse than a late bulletin. Bulletins are routinely drafted while the fix is still embargoed and unmerged — that is normal, and it is exactly why the merge state must be re-checked at publication time rather than at drafting time.
|
||||
|
||||
**Derive BC notes from the fix diff, not from its commit message.** A commit summary that calls the behaviour unchanged can still carry an observable difference its author did not think worth mentioning. Read the diff.
|
||||
|
||||
**`Backward compatibility` is also where a breaking upgrade is announced**, and the announcement has to be blunt. S2-067 told users the fix was *not* backward compatible, that they had to rewrite their actions onto a new mechanism, and that staying on the old one left them vulnerable. Where the fix is transparent, the house sentence is simply *"This change is backward compatible."*
|
||||
|
||||
## Workaround: verify or say nothing
|
||||
|
||||
Three valid outcomes, in order of preference:
|
||||
|
||||
1. **A verified configuration or operational change.** Trace it in source and confirm it actually removes reachability. Give the change, not the mechanism. It need not be a Struts setting — S2-068 offers a sized or dedicated temp volume, and pointing at the relevant section of the Security Guide is a legitimate workaround in itself.
|
||||
2. **Upgrade only** — when you checked and found nothing.
|
||||
3. **Verified absence.** The house value is a bare `n/a` (S2-066, S2-067); spell it out when the reason is worth stating.
|
||||
|
||||
Never ship a workaround you reasoned about but did not confirm. A wrong workaround leaves operators believing they are protected and discredits every other field on the page.
|
||||
|
||||
**The tension to decide deliberately:** a workaround usually reveals which path is affected. That is often the right trade — it is why the bulletin exists — but it is a decision to make and surface, not one to make silently. Say which way you went and why.
|
||||
|
||||
## Re-read the page immediately before you write to it
|
||||
|
||||
Bulletins are drafted by more than one person, often within the same hour. Content you read earlier may have moved on — a backport range added, a placeholder resolved, a section rewritten.
|
||||
|
||||
**Fetch the current version immediately before every write, and compare the returned version number against the one you read.** If it advanced, re-read, merge your change onto the newer content, and write that. Writing from a stale copy silently discards someone else's work with no warning and no conflict error.
|
||||
|
||||
After writing, diff your new version against the one you meant to build on. The diff should show only your intended change. If it shows deletions you did not intend, restore from history and redo the edit on top.
|
||||
|
||||
## Restrictions
|
||||
|
||||
Bulletins stay restricted until the coordinated publication date.
|
||||
|
||||
**Check restrictions before the edit and again after.** An API update should not disturb them, but "should not" is not verification, and an accidentally public pre-release bulletin is an unrecoverable disclosure.
|
||||
|
||||
Expected on the Struts wiki: read and update limited to the author plus `struts-committers`.
|
||||
|
||||
Publication is clearing them **completely** — read *and* update, both empty, matching every
|
||||
already-published bulletin. Verify with an unauthenticated fetch of the public URL, not with the
|
||||
API's response: the tool reporting success is not the page being readable.
|
||||
|
||||
## Announcing it: press the button on the CVE record
|
||||
|
||||
Once the page is public the advisory goes out **from the CVE record**, not from a mailbox.
|
||||
Every record on `cveprocess.apache.org` carries an *OSS/ASF Emails* tab —
|
||||
`https://cveprocess.apache.org/cve5/CVE-YYYY-NNNNN#email` — holding two finished mails, one
|
||||
for `oss-security` and one for the ASF lists, generated from the record's own affected
|
||||
ranges, description, credit and references. A single **`Send these Emails`** button sits at
|
||||
the foot of the tab, and both mails point at it — *"Use the button below to send these
|
||||
mails, or copy, paste, and send the email yourself."*
|
||||
|
||||
The send is four steps, in order:
|
||||
|
||||
1. **Set `userslist` to `user@struts.apache.org`** — the field is in the *Editor* tab, or as
|
||||
`CNA_private.userslist` on the *Source* tab, which accepts the record as raw JSON. It is
|
||||
labelled *"This is your project list such as users@ where you also want security
|
||||
announcement emails go to"*, and it is what puts the Struts user list on the ASF mail.
|
||||
2. **`SAVE`.** The mails are regenerated from the saved record, not from the form.
|
||||
3. **Re-read both mails on the *OSS/ASF Emails* tab.** They are the deliverable — read them
|
||||
as closely as you read the bulletin.
|
||||
4. **Press `Send these Emails`.**
|
||||
|
||||
**Take the subject the tool generates, unedited** — `CVE-YYYY-NNNNN: Apache Struts: <title>`.
|
||||
No `[ANN]` prefix: that prefix belongs to *release* announcements, and ASF CVE reports do not
|
||||
carry it. No `- S2-XXX` suffix either; the bulletin reaches readers as a `References:` line.
|
||||
|
||||
**Why the button rather than a mailbox.** Mail sent through the tool is moderated
|
||||
automatically, while the same text from a personal account waits for a human moderator. It
|
||||
also cannot get the `text/plain` contract or the sender identity wrong — which is the entire
|
||||
class of failure below.
|
||||
|
||||
Both points are sebb's, on the S2-073 announcement thread, 2026-08-14.
|
||||
|
||||
**Expect a delay either way, and do not read it as a failed send.** The tab warns that
|
||||
*"ASF announcement mailing lists are moderated. It may take some hours/days before your
|
||||
email will be published."* Silence on the archives an hour later is moderation, not a
|
||||
bounce.
|
||||
|
||||
### The failure this replaces
|
||||
|
||||
On 2026-08-14 all five advisories were sent by hand from Gmail. Every one failed at least
|
||||
once:
|
||||
|
||||
| Send | Outcome |
|
||||
|---|---|
|
||||
| S2-070, from the personal Gmail identity | Bounced — `Must be sent from an @apache.org address.` |
|
||||
| S2-071…074, to `user@` with `announce@` in Cc/Bcc | Bounced — `unacceptable MIME Content-Type: 'text/html' (#5.2.3)` |
|
||||
| The re-sends, to `announce@` alone | Delivered, subjects hand-prefixed `[ANN] ` |
|
||||
|
||||
The *bodies* were already the tool's generated text. Only the **sending** was hand-rolled,
|
||||
and every one of those failures came from the sending. `CNA_private.emailed` on those
|
||||
records is still `null`.
|
||||
|
||||
**A bounce is not a reliable alarm.** That morning one HTML send was rejected by
|
||||
`announce@apache.org` and *accepted* by `user@struts.apache.org`. The HTML advisory reached
|
||||
the user list. A partial failure looks like success in the Sent folder.
|
||||
|
||||
### If the button is genuinely unavailable
|
||||
|
||||
Only when the button itself fails — it errors, or the record will not save. Moderation delay
|
||||
is not unavailability. Then the mail is three things and is incomplete without any:
|
||||
|
||||
1. A draft with To, Bcc, Subject and `body`. **Never `htmlBody`** — and passing `body` alone
|
||||
does not make the mail plain text; Gmail generates the HTML part itself on send.
|
||||
2. The identical body in a file, whose path you hand over, hard-wrapped at 72 columns.
|
||||
3. The sending instruction in your handover: **plain-text mode on** (⋮ → *Plain text mode*),
|
||||
paste the file over the body, send from the `@apache.org` identity.
|
||||
|
||||
Copy the recipients and subject off the tool's tab rather than composing them. The
|
||||
`oss-security` copy is a separate mail with **no Cc and no Bcc** — not the ASF mail with an
|
||||
extra recipient — and `announcements@struts.apache.org` accepts only `@apache.org` senders.
|
||||
|
||||
## Start from the template, never from a previous bulletin
|
||||
|
||||
**[`bulletin-template.md`](bulletin-template.md)** — the field reference, per-section guidance, pre-publication checklist, and a storage-format skeleton ready to POST to the Confluence API. **It is the source of truth.**
|
||||
|
||||
A rendered copy exists on the wiki as a restricted child of *Security Bulletins* for authors who prefer to copy a page; when the two disagree, fix the wiki page from the file. Whichever route you take, confirm the new page carries the same restrictions before typing anything into it, and give the `excerpt` macro a fresh `ac:macro-id` — a copied page inherits the template's, and two bulletins must not share one.
|
||||
|
||||
**If you inherit a page cloned from a previous bulletin instead**, assume every field is inherited and wrong until you have replaced it. The residue that survives a careless edit:
|
||||
|
||||
- The previous bulletin's real CVE identifier
|
||||
- Its affected versions, rating, and reporter credit
|
||||
- Its workaround — describing a mitigation for an unrelated defect
|
||||
- The `excerpt` macro's `ac:macro-id`, now **duplicated across two pages** — generate a fresh UUID
|
||||
|
||||
Read the whole page and rewrite it; do not patch the fields you happen to notice.
|
||||
|
||||
## Red Flags — STOP
|
||||
|
||||
- About to write a Workaround you have not traced in source
|
||||
- About to write "no workaround exists" without having looked
|
||||
- Affected Software copied from a git tag list rather than confirmed releases
|
||||
- A CVE number on the page that you did not receive from the CVE assignment process
|
||||
- Naming a class, method, or file in `Problem` "because it's already public in the PR"
|
||||
- Copying the disclosure depth of an older bulletin — that depth is the reason this budget exists
|
||||
- Calibrating a rating against a bulletin published before the four-level scale existed
|
||||
- Guarding `Problem` carefully and then describing the fix's internals in `Backward compatibility`
|
||||
- Writing a BC note from the fix's commit message without reading the diff
|
||||
- Publishing while the fix is still unmerged, or without re-checking that it landed
|
||||
- No statement of who is *not* affected, when exposure depends on a plugin or an opt-in setting
|
||||
- Writing a page from content you read earlier in the session without re-fetching it first
|
||||
- Publishing without re-checking restrictions
|
||||
- Treating an API success as proof the page is publicly readable
|
||||
- `htmlBody` passed to the draft tool, for any reason
|
||||
- An advisory sent from a mailbox while the record's `Send these Emails` button is available
|
||||
- `Send these Emails` pressed before `userslist` is set to `user@struts.apache.org` and saved
|
||||
- `[ANN]`, an S2-XXX suffix, or any other hand-edit to the subject the tool generated
|
||||
- A draft handed over without the plain-text-mode instruction and the body file
|
||||
- A severity rating chosen by feel, or by reachability alone, without checking it against the published scale
|
||||
- Rating something Low because the feature is opt-in — opt-in is the definition of Moderate
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Reality |
|
||||
|---|---|
|
||||
| "The PR is public, so detail costs nothing" | A bulletin is indexed, permanent, and read by people who never see the PR. Aggregation is the harm. |
|
||||
| "An older bulletin explained the cause in detail" | Those explanations were used to build exploits. The practice was stopped deliberately — don't restore it. |
|
||||
| "An older bulletin rated something like this X" | The rating scale postdates it. Match a definition on page 61758 instead. |
|
||||
| "Listing the failed build is more honest" | It is less accurate. That build was never a release; listing it misdirects every downstream consumer. |
|
||||
| "Disabling the feature is an obvious workaround" | Obvious ≠ verified. Confirm the feature is genuinely on the only reachable path. |
|
||||
| "The rating is roughly right" | Ratings drive upgrade urgency. Read the published definitions and match one, don't approximate. |
|
||||
| "It needs an opt-in feature, so it's Low" | That is the Moderate mitigation clause. Low means hard to exploit or minimal consequence. |
|
||||
| "Restrictions were set when the page was created" | Verify after every edit. The cost of being wrong once is total. |
|
||||
| "I'll fill in the CVE later" | Only if the placeholder is unmistakable. A blank or a stale number ships as fact. |
|
||||
| "The BC note is just a compatibility courtesy" | It describes what the fix changed, which describes the defect. Same budget as `Problem`. |
|
||||
| "The commit message says behaviour is unchanged" | Commit summaries understate. Read the diff and decide for yourself. |
|
||||
| "Naming the plugin narrows it for an attacker too" | They can read your dependency list. Scoping spares every operator who isn't exposed. |
|
||||
| "The patch is reviewed, so the release will contain it" | Reviewed is not merged. Re-check at publication, not at drafting. |
|
||||
| "Copying the last bulletin is quicker than the template" | It is how another advisory's CVE ships on your page. Copy the template. |
|
||||
| "I read the page a few minutes ago" | Someone else may have written to it since. Re-fetch, then write. There is no conflict warning. |
|
||||
| "I passed `body`, not `htmlBody`, so it's plain text" | Gmail generates the HTML part itself on send. The format is decided in the compose window. |
|
||||
| "It reached the lists, so the format was fine" | One list accepted the same message another rejected. Check every recipient, not the Sent folder. |
|
||||
| "I copied the tool's text, so the mail is fine" | The text was never what failed. Sending by hand is. Press the button. |
|
||||
| "The tool doesn't mail `user@`, so I'll send it myself" | It does once `userslist` says so. One field on the record, not a parallel process. |
|
||||
| "`[ANN]` marks it as an announcement" | ASF CVE reports don't carry it. The tool's subject *is* the subject. |
|
||||
| "Sending it myself is quicker than pressing the button" | It is slower end to end: a personal-account mail waits for a human moderator, the tool's does not. |
|
||||
@@ -1,154 +0,0 @@
|
||||
# Security Bulletin Template
|
||||
|
||||
The canonical skeleton and per-field guidance for an S2-XXX security bulletin.
|
||||
Companion to [`SKILL.md`](SKILL.md), which covers *how* to establish the facts that
|
||||
go in these fields; this file covers *what the page contains*.
|
||||
|
||||
A rendered copy lives on the Struts wiki as a restricted child of
|
||||
[Security Bulletins](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=61758)
|
||||
for authors who prefer to copy a page. **This file is the source of truth** — when the
|
||||
two disagree, fix the wiki page from here.
|
||||
|
||||
**Draft bulletins stay restricted** (read and update limited to the author plus
|
||||
`struts-committers`) until the coordinated publication date. Check restrictions before
|
||||
an edit and again after it: an accidentally public pre-release bulletin is an
|
||||
unrecoverable disclosure.
|
||||
|
||||
## Fields
|
||||
|
||||
| Row | What goes in it |
|
||||
|---|---|
|
||||
| Who should read this | Usually `All Struts 2 developers and users`. Narrow it only when exposure is genuinely conditional. |
|
||||
| Impact of vulnerability | A short impact phrase, not a paragraph — `Remote Code Execution`, `Denial of service`. Hedge where warranted (`Possible Remote Code Execution vulnerability`). |
|
||||
| Maximum security rating | `Low` / `Moderate` / `Important` / `Critical`, matching a definition on the [Security Bulletins](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=61758) page. That page is the only authority — the four-level naming postdates many older bulletins, so never calibrate against one. |
|
||||
| Recommendation | `Upgrade to Struts X.Y.Z at least`. Name **every** maintenance line carrying the fix, and add the required action where upgrading alone is not enough. |
|
||||
| Affected Software | Officially released versions only. One bullet per maintenance line; link the EOL announcement for end-of-life ranges. |
|
||||
| Reporters | Credit the reporter. Include their organisation where they gave one; obfuscate any email address. |
|
||||
| CVE Identifier | `CVE-YYYY-NNNNN (to be assigned before publication)` until the real identifier arrives. One CVE per independently fixable issue. |
|
||||
|
||||
### Affected Software
|
||||
|
||||
List only versions that passed a PMC release vote. A build that was cut, failed its
|
||||
test period and was superseded never reached users — listing it implies an official
|
||||
artifact was vulnerable and drags a phantom version into every downstream CVE record
|
||||
and scanner database. Do not read the range off git tags; tags exist for builds that
|
||||
were never voted through.
|
||||
|
||||
To find the lower bound: locate when the vulnerable construct entered with
|
||||
`git log -S`, then map that commit to the first release containing it. A defect often
|
||||
predates the control that was supposed to bound it, so do not assume it arrived with
|
||||
the feature that made it reachable. If the mapping cannot be settled, write a visible
|
||||
placeholder naming what must be confirmed — never a guessed version number.
|
||||
|
||||
## Problem
|
||||
|
||||
One to three sentences. Write for an operator, not a reviewer: the feature, the
|
||||
failure, the consequence.
|
||||
|
||||
| Safe to publish | Never publish before the fix is out |
|
||||
|---|---|
|
||||
| Impact categories and consequence | Class, method, or field names |
|
||||
| The component in plain words, linked to its documentation | `file:line` references |
|
||||
| Whether a configured control fails to apply | Commit hashes, PR or Jira numbers |
|
||||
| That state is shared / input is unvalidated | The triggering request shape or payload |
|
||||
| Which released versions are affected | Reproduction steps, PoC, timing conditions |
|
||||
|
||||
Older bulletins explained causes and mitigations in far more detail, and that detail
|
||||
was used to build working exploits. The project deliberately stopped. **Mine the
|
||||
archive for structure and tone, never for depth.**
|
||||
|
||||
**Then say who is not affected.** An operator's first question is "does this reach
|
||||
me?" — answer it, or every reader must assume it does. One sentence, linked to the
|
||||
feature's documentation, either as a trailing note or folded into the opening clause.
|
||||
Name the optional plugin, the setting that must be switched on, the endpoint that must
|
||||
be mapped, or the unaffected sibling path. Add "earlier releases are not affected"
|
||||
where there is a clean prior baseline.
|
||||
|
||||
Keep it at the level of a deployment decision, not a code path. Scoping *reduces* net
|
||||
disclosure: it shrinks the population that has to care, and costs an attacker nothing
|
||||
they could not read off a dependency list.
|
||||
|
||||
## Solution
|
||||
|
||||
`Upgrade to Struts X.Y.Z at least.` Repeat for each maintenance line, and link the
|
||||
migration guide where the fix requires one.
|
||||
|
||||
## Backward compatibility
|
||||
|
||||
**Subject to the same disclosure budget as Problem.** This is the section that leaks:
|
||||
describing what changed about the fixed behaviour describes the defect. Write it in
|
||||
terms of what an application might *observe*, never what the fix altered internally,
|
||||
and derive it from the fix diff rather than the commit message — a summary calling the
|
||||
behaviour unchanged can still carry an observable difference.
|
||||
|
||||
It is also where a **breaking** upgrade is announced, and that announcement has to be
|
||||
blunt: what must be rewritten, and what staying put costs. Where the fix is
|
||||
transparent, the house sentence is simply `This change is backward compatible.`
|
||||
|
||||
## Workaround
|
||||
|
||||
Three valid outcomes, in order of preference:
|
||||
|
||||
1. **A verified configuration or operational change** — traced in source and confirmed
|
||||
to remove reachability. Give the change, not the mechanism. It need not be a Struts
|
||||
setting; container and reverse-proxy limits count, as does pointing at the relevant
|
||||
section of the Security Guide.
|
||||
2. **Upgrade only**, when you checked and found nothing.
|
||||
3. **Verified absence.** The house value is a bare `n/a`; spell it out when the reason
|
||||
is worth stating.
|
||||
|
||||
Never ship a workaround you reasoned about but did not confirm — it leaves operators
|
||||
believing they are protected and discredits every other field on the page. "No
|
||||
workaround exists" is a claim about absence and needs checking too.
|
||||
|
||||
A workaround usually reveals which path is affected. That is often the right trade,
|
||||
but make it deliberately and record which way you went.
|
||||
|
||||
## Before publishing
|
||||
|
||||
- [ ] Every placeholder is replaced, and no guidance text survives on the page.
|
||||
- [ ] The CVE identifier is real, not the placeholder.
|
||||
- [ ] Affected Software lists voted releases only, and covers every maintenance line.
|
||||
- [ ] The rating matches a published definition rather than an approximation.
|
||||
- [ ] The workaround was verified in source, or its absence was.
|
||||
- [ ] Problem, Backward compatibility and Workaround name no class, file, commit, PR
|
||||
or payload.
|
||||
- [ ] The fix is **merged** into the release branch — reviewed is not merged; re-check
|
||||
now, not at drafting time.
|
||||
- [ ] The fixed release is out and accepted.
|
||||
- [ ] Restrictions are lifted only at the coordinated publication moment.
|
||||
- [ ] The CVE record's `CNA_private.userslist` reads `user@struts.apache.org` and is saved,
|
||||
so `Send these Emails` on the *OSS/ASF Emails* tab reaches the Struts user list.
|
||||
|
||||
## Storage-format skeleton
|
||||
|
||||
Ready to POST to the Confluence API. Give the `excerpt` macro a **fresh**
|
||||
`ac:macro-id` each time — two bulletins must not share one.
|
||||
|
||||
```xml
|
||||
<h2>Summary</h2>
|
||||
<ac:structured-macro ac:name="excerpt" ac:schema-version="1">
|
||||
<ac:parameter ac:name="atlassian-macro-output-type">BLOCK</ac:parameter>
|
||||
<ac:rich-text-body><p>ONE-LINE DESCRIPTION OF THE DEFECT</p></ac:rich-text-body>
|
||||
</ac:structured-macro>
|
||||
<p class="auto-cursor-target"><br/></p>
|
||||
<table class="wrapped"><colgroup><col/><col/></colgroup><tbody>
|
||||
<tr><th><p>Who should read this</p></th><td><p>All Struts 2 developers and users</p></td></tr>
|
||||
<tr><th><p>Impact of vulnerability</p></th><td><p>IMPACT PHRASE</p></td></tr>
|
||||
<tr><th><p>Maximum security rating</p></th><td><p>Low | Moderate | Important | Critical</p></td></tr>
|
||||
<tr><th><p>Recommendation</p></th><td><p>Upgrade to Struts X.Y.Z at least</p></td></tr>
|
||||
<tr><th><p>Affected Software</p></th><td><ul style="list-style-type: square;">
|
||||
<li>Struts A.B.C through Struts D.E.F</li></ul></td></tr>
|
||||
<tr><th><p>Reporters</p></th><td><p>REPORTER</p></td></tr>
|
||||
<tr><th><p>CVE Identifier</p></th><td><p>CVE-YYYY-NNNNN (to be assigned before publication)</p></td></tr>
|
||||
</tbody></table>
|
||||
<h2>Problem</h2>
|
||||
<p>WHAT THE DEFECT ALLOWS, IN OPERATOR TERMS.</p>
|
||||
<p>WHO IS NOT AFFECTED, AND WHY.</p>
|
||||
<h2>Solution</h2>
|
||||
<p>Upgrade to Struts X.Y.Z at least.</p>
|
||||
<h2>Backward compatibility</h2>
|
||||
<p>This change is backward compatible.</p>
|
||||
<h2>Workaround</h2>
|
||||
<p>WORKAROUND, OR A STATEMENT THAT NONE EXISTS.</p>
|
||||
```
|
||||
@@ -1,329 +0,0 @@
|
||||
---
|
||||
name: creating-version-notes
|
||||
description: Use when preparing, updating, or reviewing the release documentation for a Struts release or release candidate on any maintenance line (6.x, 7.x) - the Version Notes page on the cwiki, its Migration Guide entry, the GitHub release notes, and the test-build announcement mail.
|
||||
---
|
||||
|
||||
# Creating Version Notes
|
||||
|
||||
## Overview
|
||||
|
||||
A Version Notes page answers one question for a user deciding whether to upgrade: **what changed in this release, and what will break.** Almost all of it is a mechanical rendering of a JIRA fix version onto fixed boilerplate.
|
||||
|
||||
**Core principle:** the mechanical parts must be *derived*, never retyped; the two judgement parts — Breaking changes, and how a security fix is described — are the only places you author prose.
|
||||
|
||||
**One skill covers every maintenance line.** 6.x and 7.x pages share an identical structure. The line changes the data (version, prior page, JIRA ids), never the process.
|
||||
|
||||
**This is phase 3 of a seven-phase release.** `releasing-struts` holds the sequence, the gates and the mechanics either side of it; this skill owns the paperwork.
|
||||
|
||||
## The Iron Rule
|
||||
|
||||
```
|
||||
START FROM THE TEMPLATE. NEVER CLONE THE PREVIOUS VERSION NOTES PAGE.
|
||||
```
|
||||
|
||||
Cloning is how the published pages acquired their defects, and it fails differently every time:
|
||||
|
||||
| Page | Inherited defect |
|
||||
|---|---|
|
||||
| Version Notes 6.9.0 | Issue Detail links **"JIRA Release Notes 6.8.0"** — label and `version=` id both from 6.8.0 |
|
||||
| Version Notes 6.10.0 | Issue List links **"Struts 6.9.0 DONE"** — label names the previous release, against a `filter=` id different from the one the 6.9.0 page used |
|
||||
| Both series | Maven Dependency code macro carries `ac:name=""` instead of `ac:name="language"` |
|
||||
|
||||
Half-updated links are the signature failure: the number gets fixed and the label doesn't, or the reverse. They survive review because the link still works — it just points at, or claims to be, the wrong release.
|
||||
|
||||
**[`version-notes-template.md`](version-notes-template.md) is the source of truth**: field guidance, storage-format skeleton with those defects corrected, and the pre-publication checklist.
|
||||
|
||||
## Collect every input before writing
|
||||
|
||||
Each row is derived from a named source. A value you cannot source is a visible placeholder, never a guess.
|
||||
|
||||
| Input | Where it comes from |
|
||||
|---|---|
|
||||
| Version | The release being voted or announced |
|
||||
| Parent page | Always **Migration Guide** (page id `13981`) — every Version Notes page is a child of it |
|
||||
| Prior notes page title | The previous **released** version in the same series — see below |
|
||||
| JIRA version id | Numeric id behind `ReleaseNote.jspa?version=` — from the WW project's versions, **not** the version name |
|
||||
| DONE filter id | The saved JIRA filter for this release; a new release needs a new filter |
|
||||
| Issue list | `project = WW AND fixVersion = <version>`, grouped by type |
|
||||
| Breaking changes | Authored — see below |
|
||||
| Staging Repository block | An explicit decision — see below |
|
||||
|
||||
## The issue list
|
||||
|
||||
Group under `<h2>` per issue type, in this order, omitting any type with no issues:
|
||||
|
||||
**Bug → New Feature → Improvement → Task → Dependency**
|
||||
|
||||
Within a section, order by issue key ascending. Each entry is `[WW-XXXX] - <the JIRA summary verbatim>`.
|
||||
|
||||
**Reconcile against what actually merged.** The JIRA query is the starting point, not the answer. Three mismatches to check:
|
||||
|
||||
- A ticket resolved `Fixed` whose change did not make the release branch — it must not be listed as delivered.
|
||||
- A ticket resolved **`Won't Do`** or otherwise not `Fixed` — it belongs under `Rejected requests`, not in a type section and not dropped. Check the resolution, not just the status: both `Closed` and `Resolved` sit in the Done category.
|
||||
- Work that shipped under a ticket assigned to a different fix version — the notes under-report the release.
|
||||
|
||||
A ticket with no commit in the range is not automatically wrong. Check its **component** first: `IDEA Plugin`, `Example Applications` and similar live in other repositories and are still legitimately part of the release.
|
||||
|
||||
**Reconcile through the ticket's linked PR, reading the files it changed.** Do not grep commit subjects, and do not go looking for the class named in the ticket title: a title often names the *symptom* while the fix lives elsewhere. WW-5630 reads "Performance Issue SecurityMemberAccess" and was fixed in `ConfigParseUtil`; searching for the former concludes, wrongly, that the backport is missing. Squash-merges also rewrite hashes, so the merge commit id from the PR need not appear on the branch.
|
||||
|
||||
**Untick eted patch-level dependency bumps are not a gap.** Dependabot PRs for patch updates are merged directly and deliberately get no ticket, so they get no entry — there is nothing to link. Expect the pom to show a higher patch version than the ticket text says: 6.11.0 shipped jackson 2.22.1 while WW-5648 reads "2.21.4 to 2.22.0". That is correct, not an omission. Minor and major bumps do get a ticket and do get listed.
|
||||
|
||||
Where a ticket's summary was written for triage rather than for users, the page may carry a clearer summary — but then it is authored text, and the link must still resolve to that ticket.
|
||||
|
||||
## Only released versions belong in the chain
|
||||
|
||||
The prior-notes link forms a chain through the series, and it **skips versions that were cut but never released**. Version Notes 7.2.1 links back to 7.1.1, not to the withdrawn 7.2.0.
|
||||
|
||||
When a release is superseded before it ships, its content does not disappear — the successor absorbs it. 7.2.1 carries the Breaking changes for the whole 7.2.x cycle. Check what the predecessor covered before assuming your issue list is complete.
|
||||
|
||||
This is the same discipline `creating-security-bulletins` applies to Affected Software, for the same reason: naming a version that never reached users misdirects everyone downstream.
|
||||
|
||||
## Page section order
|
||||
|
||||
Beyond the boilerplate, sections appear in this order, each omitted when empty:
|
||||
|
||||
**Breaking changes → Deprecations → Rejected requests → Bug → New Feature → Improvement → Task → Dependency → Issue Detail → Issue List → Other resources**
|
||||
|
||||
The first three are authored; the issue-type sections are derived from JIRA.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
Present only when the release has them — a maintenance release usually does not.
|
||||
|
||||
Each item is **one sentence plus its ticket link**:
|
||||
|
||||
```
|
||||
<what changed, in terms of what an application sees> [WW-XXXX].
|
||||
```
|
||||
|
||||
> Annotated wildcard actions are matched most-specific-first, so action selection can differ [WW-3784].
|
||||
|
||||
> `JSONInterceptor` uses a fresh reader and writer per request, so custom ones must not hold state between requests [WW-5650].
|
||||
|
||||
The sentence exists so a reader can judge **whether to open the ticket**, not so they can avoid opening it. The ticket carries the detail — API signatures, migration steps, the config that changes. Naming the affected type or setting is enough; enumerating what replaces it is the ticket's job.
|
||||
|
||||
Derive each item from the fix diff rather than the ticket title, and write only what you confirmed. A change you suspect is breaking but could not pin down is one to raise with the release manager, not to describe vaguely.
|
||||
|
||||
## Deprecations
|
||||
|
||||
Where a release deprecates public API, list it separately from Breaking changes — nothing stops working yet, so mixing the two overstates the upgrade cost. Same one-line shape, naming the replacement where there is one:
|
||||
|
||||
> `ConversionRule.COLLECTION` and the `Collection_` key prefix are deprecated; use `ConversionRule.ELEMENT` and `Element_` instead [WW-5656].
|
||||
|
||||
## Rejected requests
|
||||
|
||||
A ticket resolved **`Won't Do`** (or otherwise not `Fixed`) against this fix version is still news: someone asked for it and the project decided against it.
|
||||
|
||||
- **Do not put it in a type section.** Under Improvement or New Feature it reads as delivered.
|
||||
- **Do not silently drop it either.** The decision is the value.
|
||||
- List it under `Rejected requests`, saying it will not be implemented and, where the release manager gave one, the reason.
|
||||
|
||||
> [WW-2635] - Flash scope - will not be implemented; the proposed mechanism could introduce a security risk.
|
||||
|
||||
Note the JIRA-generated release notes linked from the page *will* still include these tickets under their type. Clearing the fix version in JIRA is the only way to change that, and is the release manager's call.
|
||||
|
||||
## Security fixes in a release
|
||||
|
||||
A release usually ships before its bulletin publishes and before a CVE exists. The Version Notes then list a **public, neutrally-framed** ticket for a defect whose advisory is still restricted.
|
||||
|
||||
- List the ticket as you would any other. It is already public; omitting it under-reports the release.
|
||||
- **Do not add security framing the bulletin has not published yet** — no severity, no attack description, no S2-XXX or CVE number that has not been assigned and published.
|
||||
- Once the bulletin is public, the notes may link it.
|
||||
|
||||
**Where the ticket's own summary describes the defect, list the neutral part of it.** "List the summary verbatim" assumes a neutrally-worded ticket, and security tickets often are not. WW-5643 reads *"StrutsJSONReader parse state shared across concurrent requests — maxDepth bypass and cross-request data leak"*; the page carried it up to "concurrent requests" and stopped. The trailing clause is the bulletin's job.
|
||||
|
||||
Truncate at the clause boundary — never paraphrase into something the ticket does not say, and never alter the ticket link. Then **tell the release manager which summaries you cut and why**: whether an already-public JIRA summary should be reproduced in full is their call, not yours, and it has to be made before the page goes up rather than edited afterwards.
|
||||
|
||||
**REQUIRED BACKGROUND:** where the wording of a security-relevant entry is in question, `creating-security-bulletins` governs what may be said and when.
|
||||
|
||||
## The Staging Repository block
|
||||
|
||||
**Include it.** The block points readers at ASF Nexus staging so they can test the artifacts before the vote closes, and it stays on the page afterwards.
|
||||
|
||||
Older 6.x pages (6.9.0, 6.10.0) lack it while the 7.x pages carry it. That is an artefact of cloning within each series, not a difference between the lines — 6.11.0 carries it.
|
||||
|
||||
## Link the new page from the Migration Guide
|
||||
|
||||
The page is not finished when it is created. **[Migration Guide](https://cwiki.apache.org/confluence/spaces/WW/pages/13981/Migration+Guide) (id `13981`) is both the parent page and the index**, and a Version Notes page that is not listed there is unreachable by anyone browsing.
|
||||
|
||||
Add an entry at the **top** of the list under the `<h2>` for the matching line — `Version Notes 7.x`, `Version Notes 6.x`, and so on. The lists are newest-first, and the entry is a page link carrying no body text:
|
||||
|
||||
```xml
|
||||
<li><ac:link><ri:page ri:content-title="Version Notes X.Y.Z"/></ac:link></li>
|
||||
```
|
||||
|
||||
**Update the section, not the whole page.** `confluence_update_page_section` on the exact heading replaces only that section's body; its boundary is the next `<h2>`, so the section body includes the `<h3>` migration-guide link that follows the list. Supply that `<h3>` and its paragraph in the replacement content or they are dropped.
|
||||
|
||||
**Verify against raw storage, not the diff.** A version diff of this page renders empty even for a real change, because the markdown view discards `ac:link` bodies. Fetch the new version with `convert_to_markdown=false` and confirm the new entry is present, the prior entries survive in order, and the trailing `<h3>` appears exactly once.
|
||||
|
||||
This applies to **every** section update, including ones on the Version Notes page itself — shortening `Breaking changes` carries the same risk of swallowing the `Deprecations` heading that follows it. After any section write, confirm the sections below it are still present exactly once. Where the page has no `ac:link` in it, the cheaper markdown fetch is enough to see the headings.
|
||||
|
||||
## Writing pages through the API
|
||||
|
||||
`content_file` is rejected for any path outside the repository — a scratchpad path fails as path traversal. Draft wherever you like, but **pass the body as inline `content`** when creating or updating a page.
|
||||
|
||||
The response carries the new version number. On a page you have just written, that number is its own check: a create followed by one update should report version 2, so anything higher means someone else wrote in between.
|
||||
|
||||
## The GitHub release notes
|
||||
|
||||
A release also has a GitHub release at the `STRUTS_X_Y_Z` tag, kept as a **pre-release** while the vote runs. GitHub's generated body is a starting point that needs two corrections before it is fit to publish.
|
||||
|
||||
### Name the previous tag yourself
|
||||
|
||||
**Never let GitHub choose the range.** It picks the previous tag by reachability, and Struts release branches get renamed and re-imported, so older tags are frequently *not* ancestors of the new one and the heuristic reaches too far back. For 6.11.0 it chose `STRUTS_6_8_0` and produced ~101 entries, 88 of which had already shipped in 6.9.0 and 6.10.0.
|
||||
|
||||
Generate the body with the previous release named explicitly, and it comes out right the first time:
|
||||
|
||||
```bash
|
||||
gh api -X POST repos/apache/struts/releases/generate-notes \
|
||||
-f tag_name=STRUTS_7_3_0 -f previous_tag_name=STRUTS_7_2_1 -q .body > generated.md
|
||||
```
|
||||
|
||||
Confirm the entry count is plausible against the real change set, which `git log` gives even across unrelated histories:
|
||||
|
||||
```bash
|
||||
git log --format='%h %s' STRUTS_7_2_1..STRUTS_7_3_0
|
||||
```
|
||||
|
||||
**If you inherit a body GitHub generated on its own**, check the `**Full Changelog**: .../compare/<PREVIOUS>...<THIS>` line first, and regenerate as above rather than pruning by hand. When pruning is unavoidable, drop `## New Contributors` too if the contribution it cites falls outside the range — but keep it when the contributors are genuinely new in this range.
|
||||
|
||||
### Split the entries
|
||||
|
||||
Two sections, `### Dependencies` nested under `## What's Changed`, before any `## New Contributors`:
|
||||
|
||||
| Entry | Section |
|
||||
|---|---|
|
||||
| Carries a `WW-XXXX` ticket — whoever authored it | `## What's Changed` |
|
||||
| A human PR that is not a dependency change (ci, chore, release prep) | `## What's Changed` |
|
||||
| A dependency bump with **no** ticket | `### Dependencies` |
|
||||
|
||||
**The discriminator is the ticket, not the author.** A Dependabot PR carrying a ticket stays in What's Changed, because a ticketed bump is release content and appears in the Version Notes Dependency section. A human PR that is purely a dependency change (`Removes unused jaxb-core dependency`) belongs under Dependencies. Both cases occur in the 6.9.0 release.
|
||||
|
||||
A PR that mixes a dependency change with something else — CVE-driven library updates *plus* a CI tweak — stays in What's Changed. Dependencies is for entries that are nothing but a bump.
|
||||
|
||||
Preserve the generated relative order within each section, and keep the entry lines byte-identical — they carry the author and PR links GitHub rendered. Split with a script rather than by retyping, then **prove nothing was lost**:
|
||||
|
||||
```bash
|
||||
diff <(grep '^\* ' generated.md | sort) <(grep '^\* ' new.md | sort)
|
||||
```
|
||||
|
||||
Empty output means the entry set is unchanged and only the grouping moved.
|
||||
|
||||
### Applying it
|
||||
|
||||
The release may or may not exist yet — check before assuming which command you need.
|
||||
|
||||
```bash
|
||||
# it exists (release cut earlier, or notes already generated):
|
||||
gh release view STRUTS_X_Y_Z --json body -q .body > original.md # keep, so it can be restored
|
||||
gh release edit STRUTS_X_Y_Z --prerelease --notes-file new.md
|
||||
|
||||
# it does not exist yet:
|
||||
gh release create STRUTS_X_Y_Z --title "Struts X.Y.Z" --prerelease --verify-tag --notes-file new.md
|
||||
```
|
||||
|
||||
Pass `--prerelease` either way, so a release still under vote is not silently promoted, and `--verify-tag` on create so a typo in the tag fails instead of creating one.
|
||||
|
||||
## The test-build announcement
|
||||
|
||||
Once the Version Notes page and the GitHub release are both up, the release manager announces the test build so people can exercise the staged artifacts during the vote. **Draft it last** — every link in it points at something the earlier steps produced.
|
||||
|
||||
Subject is `[TEST] Apache Struts X.Y.Z test build is ready`. Send it to **both** lists, Bcc the private one:
|
||||
|
||||
```
|
||||
To: dev@struts.apache.org, user@struts.apache.org
|
||||
Bcc: private@struts.apache.org
|
||||
```
|
||||
|
||||
Both audiences want it — committers to check the staged artifacts, users to test against their own applications — and a build announced to only one of them reaches half the people who could find a problem during the vote.
|
||||
|
||||
The body is fixed apart from four substitutions:
|
||||
|
||||
```
|
||||
Hello,
|
||||
|
||||
This is a minor release of Struts <LINE> which contains <WHAT>, and it
|
||||
shouldn't break your code<RISK>. Please take your time and test the bits
|
||||
- any help is appreciated. Please report any problems you will spot.
|
||||
|
||||
Here are the changes from the previous version:
|
||||
https://github.com/apache/struts/releases/tag/STRUTS_X_Y_Z
|
||||
|
||||
Staging Maven repo
|
||||
https://repository.apache.org/content/groups/staging/
|
||||
|
||||
Standalone artifacts
|
||||
https://dist.apache.org/repos/dist/dev/struts/X.Y.Z/
|
||||
|
||||
Release notes
|
||||
https://cwiki.apache.org/confluence/display/WW/Version+Notes+X.Y.Z
|
||||
|
||||
Kind regards
|
||||
--
|
||||
Łukasz
|
||||
```
|
||||
|
||||
| Slot | How to fill it |
|
||||
|---|---|
|
||||
| `<LINE>` | `6.x` or `7.x` |
|
||||
| `<WHAT>` | What the issue list actually contains — `mostly bug fixes` for 6.11.0, `a few improvements and bug fixes` for 7.3.0 |
|
||||
| `<RISK>` | Empty when the release has no Breaking changes; ` but it contains significant changes` when it does. 6.11.0 had none and said nothing; 7.3.0 had seven and said so |
|
||||
| Tag / paths | Tag underscored (`STRUTS_7_3_0`), dist path and page title dotted (`7.3.0`) |
|
||||
|
||||
Do not take the recipients from a previous announcement: 6.11.0 went to `dev@` alone and 7.3.0 to `user@` alone, and both were mistakes. Address every announcement to the two lists above.
|
||||
|
||||
Keep the security posture of the pages: the mail links the release notes, it does not summarise what is in them, so no severity, CVE or S2-XXX reaches it either.
|
||||
|
||||
**The vote is the next step, and it is a different mail.** Once the test build is announced, `creating-release-vote-mail` composes the `[VOTE] Apache Struts X.Y.Z` call — to `dev@` alone, rendered from the page this skill produced. Do not draft it from here: its recipients, subject and body all differ from the announcement above.
|
||||
|
||||
## Re-read the page immediately before you write to it
|
||||
|
||||
Confluence has no conflict warning. Fetch the current version immediately before every write and compare the version number against the one you read; if it advanced, re-read, merge onto the newer content, and write that.
|
||||
|
||||
After writing, diff against the version you meant to build on. The diff should show only your intended change.
|
||||
|
||||
## Red Flags — STOP
|
||||
|
||||
- Starting from a copy of the previous release's page
|
||||
- A version number or JIRA id typed rather than derived
|
||||
- A link whose label and its id name different releases
|
||||
- The prior-notes link pointing at a version that was cut but never released
|
||||
- Publishing the issue list straight from JIRA without reconciling against the release branch
|
||||
- Concluding a backport is missing from a commit-subject grep, or from the class named in the ticket title
|
||||
- Treating an untick eted patch dependency bump as a reconciliation gap
|
||||
- Dropping a `Won't Do` ticket, or listing it under Improvement or New Feature as though it shipped
|
||||
- A Breaking changes item that runs past one sentence, or restates what the ticket already explains
|
||||
- Reproducing a security ticket's summary in full when it names the bypass or the leak
|
||||
- Letting GitHub pick the previous tag instead of passing `previous_tag_name`
|
||||
- Regrouping release entries by retyping them instead of scripting the split and diffing the result
|
||||
- A severity, CVE, or S2-XXX reference on the page that has not been published
|
||||
- Breaking changes assembled by pasting ticket summaries
|
||||
- Creating the page without adding it to the Migration Guide index
|
||||
- Trusting an empty version diff on the Migration Guide as proof the edit landed
|
||||
- Publishing GitHub release notes without checking which tag the Full Changelog compares against
|
||||
- Splitting the GitHub sections by author instead of by whether the entry carries a ticket
|
||||
- Editing a GitHub release under vote without `--prerelease`
|
||||
- Writing from page content read earlier in the session without re-fetching
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Reality |
|
||||
|---|---|
|
||||
| "Copying last release's page is faster" | It is how "JIRA Release Notes 6.8.0" shipped on the 6.9.0 page. Copy the template. |
|
||||
| "I updated the link, it's fine" | Check the label too. Every observed defect is a half-updated link. |
|
||||
| "`version=` takes the version number" | It takes JIRA's numeric version id. Look it up. |
|
||||
| "The DONE filter can be reused" | A reused filter shows the previous release's issues under this release's heading. |
|
||||
| "JIRA is the release contents" | JIRA is the claim. The release branch is the fact. Reconcile. |
|
||||
| "No commit mentions the ticket, so it wasn't backported" | Read the linked PR's changed files. Titles name symptoms, and squash-merges rewrite hashes. |
|
||||
| "The pom version doesn't match the ticket, that's a gap" | Patch bumps ship untick eted by design. Only ticketed bumps get an entry. |
|
||||
| "The page is created, so the work is done" | It is invisible until listed on the Migration Guide. |
|
||||
| "The version diff is empty, so nothing changed" | The diff renders markdown, which drops `ac:link` bodies. Check raw storage. |
|
||||
| "GitHub generated the changelog, so the range is right" | It guesses the previous tag by reachability. Renamed branches make it reach too far back. Verify with `git log PREV..THIS`. |
|
||||
| "Dependabot authored it, so it goes under Dependencies" | Ticketed bumps stay in What's Changed. The ticket decides, not the author. |
|
||||
| "A Won't Do ticket isn't part of the release" | The decision is news. It goes under Rejected requests, not into a type section and not into the bin. |
|
||||
| "More detail in Breaking changes is safer" | One sentence plus the ticket link. The reader opens the ticket for detail; the page exists to tell them whether to. |
|
||||
| "The summary is public in JIRA, so I can repeat it" | Not when it names the bypass or the leak and the bulletin is unpublished. Truncate, and say you did. |
|
||||
| "GitHub will work out the previous tag" | Pass `previous_tag_name` and it is right the first time. |
|
||||
| "The fix is public, so I can describe the vulnerability" | The ticket being public does not publish the advisory. Neutral framing until the bulletin ships. |
|
||||
| "Breaking changes are the tickets typed as breaking" | They are the changes that break an application. Author them. |
|
||||
| "7.x needs different handling from 6.x" | Same structure, same process. Only the data differs. |
|
||||
@@ -1,152 +0,0 @@
|
||||
# Version Notes Template
|
||||
|
||||
The canonical skeleton and per-field guidance for a Struts **Version Notes X.Y.Z** page
|
||||
on the [Apache Struts 2 Wiki](https://cwiki.apache.org/confluence/spaces/WW) (space `WW`).
|
||||
Companion to [`SKILL.md`](SKILL.md), which covers *how* to establish the values;
|
||||
this file covers *what the page contains*.
|
||||
|
||||
**This file is the source of truth.** Start every page from the skeleton below, never
|
||||
from a copy of the previous release's page — see the Iron Rule in `SKILL.md`.
|
||||
|
||||
## Fields
|
||||
|
||||
| Field | What goes in it |
|
||||
|---|---|
|
||||
| Version | The release being announced, e.g. `6.11.0`. Appears in the intro sentence, the page title, the Maven snippet, and both JIRA link labels. |
|
||||
| Parent page | Always `Migration Guide`, page id `13981`. Create the page as its child, and add it to that page's index — see `SKILL.md`. |
|
||||
| Prior notes page | Title of the previous **released** version's page in the same series, e.g. `Version Notes 6.10.0`. Skip versions that were cut but never released. |
|
||||
| JIRA version id | The numeric id for `ReleaseNote.jspa?version=`. Obtain from the WW project's versions — it is not the version name. `6.10.0` is `12357065`, `7.2.1` is `12355751`. |
|
||||
| DONE filter id | Saved-filter id for `issues/?filter=`, labelled `Struts X.Y.Z DONE`. Each release needs its own; a reused id lists the wrong release. |
|
||||
| TODO filter id | Constant across releases: `12351174`, labelled `Struts x.x.x TODO`. |
|
||||
| Issue sections | One `<h2>` per issue type present, ordered **Bug → New Feature → Improvement → Task → Dependency**, entries sorted by key ascending. |
|
||||
| Breaking changes | Optional. Authored, **one sentence plus the ticket link** per item. Omit the section when the release has none. |
|
||||
| Deprecations | Optional. Same one-line shape, for public API deprecated but still working. |
|
||||
| Rejected requests | Optional. Tickets resolved `Won't Do` against this fix version — never in a type section. |
|
||||
| Staging Repository | Always included, on every line — see `SKILL.md`. |
|
||||
|
||||
## Corrected storage format
|
||||
|
||||
Three defects present in the published pages are fixed here. Keep them fixed:
|
||||
|
||||
1. **`ac:name="language"` on the code macros.** The published Maven Dependency and
|
||||
Staging Repository macros carry `ac:name=""` with the value `xml`, which is a
|
||||
malformed parameter. The Archetype Catalog macro on the same pages has it right.
|
||||
2. **No `ac:macro-id` attributes.** The published pages share hard-coded macro ids
|
||||
across releases and across series because they were cloned. Omit the attribute and
|
||||
let Confluence assign one on save.
|
||||
3. **No trailing empty `<div>`s.** Every published page ends with two empty divs
|
||||
carrying inline `font-size: 24.0px` styling. They render as stray whitespace.
|
||||
|
||||
```xml
|
||||
<p><ac:emoticon ac:name="tick"/> These are the notes for the Struts version X.Y.Z distribution.</p>
|
||||
<p><ac:emoticon ac:name="tick"/> For prior notes in this release series, see <ac:link><ri:page ri:content-title="Version Notes PRIOR"/></ac:link></p>
|
||||
<p><ac:structured-macro ac:name="toc" ac:schema-version="1"/></p>
|
||||
|
||||
<h2>Maven users</h2>
|
||||
<p>If you are a Maven user, you might want to get started using the <ac:link><ri:page ri:content-title="Struts 2 Maven Archetypes"/><ac:plain-text-link-body><![CDATA[Maven Archetype]]></ac:plain-text-link-body></ac:link>.</p>
|
||||
<ac:structured-macro ac:name="code" ac:schema-version="1">
|
||||
<ac:parameter ac:name="title">Maven Dependency</ac:parameter>
|
||||
<ac:parameter ac:name="language">xml</ac:parameter>
|
||||
<ac:plain-text-body><![CDATA[<dependency>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-core</artifactId>
|
||||
<version>X.Y.Z</version>
|
||||
</dependency>
|
||||
]]></ac:plain-text-body>
|
||||
</ac:structured-macro>
|
||||
<p>You can also use Struts Archetype Catalog like below</p>
|
||||
<ac:structured-macro ac:name="code" ac:schema-version="1">
|
||||
<ac:parameter ac:name="language">text</ac:parameter>
|
||||
<ac:parameter ac:name="title">Struts Archetype Catalog</ac:parameter>
|
||||
<ac:plain-text-body><![CDATA[mvn archetype:generate -DarchetypeCatalog=http://struts.apache.org/]]></ac:plain-text-body>
|
||||
</ac:structured-macro>
|
||||
|
||||
<ac:structured-macro ac:name="code" ac:schema-version="1">
|
||||
<ac:parameter ac:name="title">Staging Repository</ac:parameter>
|
||||
<ac:parameter ac:name="language">xml</ac:parameter>
|
||||
<ac:plain-text-body><![CDATA[<repositories>
|
||||
<repository>
|
||||
<id>apache.nexus</id>
|
||||
<name>ASF Nexus Staging</name>
|
||||
<url>https://repository.apache.org/content/groups/staging/</url>
|
||||
</repository>
|
||||
</repositories>]]></ac:plain-text-body>
|
||||
</ac:structured-macro>
|
||||
|
||||
<!-- OPTIONAL: omit the whole section when the release has no breaking changes -->
|
||||
<h2>Breaking changes</h2>
|
||||
<ul style="list-style-type: square;">
|
||||
<li>ONE SENTENCE: WHAT AN APPLICATION SEES DIFFERENTLY [<a href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>].</li>
|
||||
</ul>
|
||||
|
||||
<!-- OPTIONAL: public API deprecated but still working -->
|
||||
<h2>Deprecations</h2>
|
||||
<ul style="list-style-type: square;">
|
||||
<li><code>WHAT</code> is deprecated; use <code>REPLACEMENT</code> instead [<a href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>].</li>
|
||||
</ul>
|
||||
|
||||
<!-- OPTIONAL: tickets resolved Won't Do against this fix version -->
|
||||
<h2>Rejected requests</h2>
|
||||
<p>Two long-standing requests were closed as <em>Won't Do</em> in this cycle. They are listed here so the decision is visible rather than silent.</p>
|
||||
<ul style="list-style-type: square;">
|
||||
<li>[<a href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>] - SUMMARY - will not be implemented; REASON WHERE THE RELEASE MANAGER GAVE ONE.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Bug</h2>
|
||||
<ul><li>[<a href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>] - JIRA SUMMARY</li></ul>
|
||||
|
||||
<h2>Issue Detail</h2>
|
||||
<ul><li><a href="https://issues.apache.org/jira/secure/ReleaseNote.jspa?projectId=12311041&version=JIRA_VERSION_ID">JIRA Release Notes X.Y.Z</a></li></ul>
|
||||
|
||||
<h2>Issue List</h2>
|
||||
<ul>
|
||||
<li><a href="https://issues.apache.org/jira/issues/?filter=DONE_FILTER_ID">Struts X.Y.Z DONE</a></li>
|
||||
<li><a href="https://issues.apache.org/jira/issues/?filter=12351174">Struts x.x.x TODO</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Other resources</h2>
|
||||
<ul>
|
||||
<li><a href="http://www.mail-archive.com/commits%40struts.apache.org/">Commit Logs</a></li>
|
||||
<li><a href="https://gitbox.apache.org/repos/asf?p=struts.git;a=summary">Source Code Repository</a></li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Repeat the issue `<h2>` block per type present, in the order given above.
|
||||
`projectId=12311041` is the WW project and is constant. Note `&` in the
|
||||
`ReleaseNote.jspa` URL — a bare `&` is invalid in storage format.
|
||||
|
||||
## Before publishing
|
||||
|
||||
- [ ] Every placeholder is replaced, and no guidance text survives on the page.
|
||||
- [ ] Page title is `Version Notes X.Y.Z` and the intro names the same version.
|
||||
- [ ] Prior-notes link resolves, and names the previous **released** version.
|
||||
- [ ] Maven snippet version matches the release.
|
||||
- [ ] `ReleaseNote.jspa` label and its `version=` id are the same release.
|
||||
- [ ] `DONE` filter label and its `filter=` id are the same release.
|
||||
- [ ] Issue list reconciled against the release branch via each ticket's linked PR, not taken from JIRA alone.
|
||||
- [ ] Every ticket's **resolution** checked, not just its status — `Won't Do` goes under Rejected requests.
|
||||
- [ ] Sections ordered Breaking changes → Deprecations → Rejected requests → Bug → New Feature → Improvement → Task → Dependency; empty ones omitted.
|
||||
- [ ] Each Breaking changes and Deprecations item is one sentence plus its ticket link.
|
||||
- [ ] Staging Repository block present.
|
||||
- [ ] No unpublished severity, CVE, or S2-XXX reference anywhere on the page, and any security summary truncated at a clause boundary was reported to the release manager.
|
||||
- [ ] Page created as a child of Migration Guide (`13981`).
|
||||
- [ ] **Listed at the top of the matching `Version Notes N.x` section on the Migration Guide**, and that edit verified against raw storage — the version diff renders empty even when the change landed.
|
||||
- [ ] Page re-fetched immediately before every write.
|
||||
|
||||
## GitHub release notes
|
||||
|
||||
- [ ] Body generated with `previous_tag_name` named explicitly, not left to GitHub's guess.
|
||||
- [ ] Entry count sane against `git log PREV..THIS`.
|
||||
- [ ] Original body saved first when editing an existing release, so it can be restored.
|
||||
- [ ] Entries split by **ticket, not author**: ticketed → `## What's Changed`; untick eted dependency bumps → `### Dependencies`; mixed PRs stay in What's Changed.
|
||||
- [ ] Generated order and entry text preserved within each section.
|
||||
- [ ] Split verified by diffing the sorted entry lists before and after — empty output.
|
||||
- [ ] `--prerelease` passed while the vote is open; `--verify-tag` when creating.
|
||||
|
||||
## Test-build announcement
|
||||
|
||||
- [ ] Drafted **after** the Version Notes page and GitHub release exist — it links both.
|
||||
- [ ] Subject `[TEST] Apache Struts X.Y.Z test build is ready`.
|
||||
- [ ] Addressed to **both** `dev@struts.apache.org` and `user@struts.apache.org`, Bcc `private@struts.apache.org`.
|
||||
- [ ] Risk clause matches reality: silent when there are no Breaking changes, "but it contains significant changes" when there are.
|
||||
- [ ] Tag underscored in the release link, version dotted in the dist path and page title.
|
||||
@@ -1,204 +0,0 @@
|
||||
---
|
||||
name: merging-dependabot-prs
|
||||
description: Use when triaging, classifying or landing Dependabot pull requests in this repo — clearing the open Dependabot queue, deciding whether a bump needs a WW Jira ticket, or checking whether a bump's build actually passed.
|
||||
---
|
||||
|
||||
# Merging Dependabot PRs
|
||||
|
||||
## Overview
|
||||
|
||||
Dependabot opens bumps against `main` and `support/struts-6-x-x`. Most land as-is.
|
||||
A bump that **ships to users** needs a WW Jira ticket first, and the ticket ID must
|
||||
reach the PR title, the PR body and the squash commit subject before it merges.
|
||||
|
||||
**Core principle: the decision is driven by the dependency's real Maven scope, not by
|
||||
what Dependabot calls it.** Dependabot's `build(deps-dev):` prefix and its
|
||||
`dependency-type:` trailer are guesses about *its own* ecosystem, not this project's
|
||||
POM. Read the POM.
|
||||
|
||||
## The decision
|
||||
|
||||
Read the semver class from the Dependabot commit trailer — never parse version strings:
|
||||
|
||||
```bash
|
||||
gh pr view <N> --json commits --jq '.commits[].messageBody' \
|
||||
| grep -E 'dependency-name:|update-type:'
|
||||
# update-type: version-update:semver-{patch|minor|major}
|
||||
```
|
||||
|
||||
Then find the real scope, and cross the two:
|
||||
|
||||
| What is being bumped | patch | minor | major |
|
||||
|---|---|---|---|
|
||||
| GitHub Action (`.github/workflows/*`) | merge bare | merge bare | merge bare |
|
||||
| Maven **plugin** or build tooling (`<build><plugins>`, `*-maven-plugin`, `maven-wrapper`) | merge bare | merge bare | merge bare |
|
||||
| Maven dep, `<scope>test</scope>` | merge bare | merge bare | merge bare |
|
||||
| Maven dep in `apps/` (showcase, rest-showcase) | merge bare | merge bare | merge bare |
|
||||
| Maven dep, **compile / runtime / provided** — including `<optional>true</optional>` | merge bare | **TICKET** | **TICKET** |
|
||||
|
||||
"Merge bare" always still requires a green build (see below).
|
||||
|
||||
### Finding the real scope
|
||||
|
||||
```bash
|
||||
grep -rn '<artifactId>NAME</artifactId>' --include=pom.xml . | grep -v /target/
|
||||
```
|
||||
|
||||
Read the `<scope>` on the surrounding `<dependency>` block in the module that declares it
|
||||
(`core/pom.xml`, `plugins/*/pom.xml`), not the `<dependencyManagement>` copy in
|
||||
`parent/pom.xml` — the managed block usually carries no scope.
|
||||
|
||||
- No `<scope>` element means **compile** — it ships.
|
||||
- `<optional>true</optional>` still ships: it is published in the module's POM and users
|
||||
who opt in inherit the version. Optional is not exempt.
|
||||
- For a version property (`jackson.version`, `byte-buddy.version`), resolve the property to
|
||||
the artifacts it feeds and take the widest scope among them.
|
||||
|
||||
## Is the build actually green?
|
||||
|
||||
`.asf.yaml` makes exactly one context required per branch:
|
||||
`Build and Test (JDK 17)` on `main`, `Build and Test (8)` on `support/struts-6-x-x`.
|
||||
|
||||
```bash
|
||||
gh pr view <N> --json mergeStateStatus,statusCheckRollup --jq \
|
||||
'"\(.mergeStateStatus) build=\([.statusCheckRollup[]
|
||||
| select((.name // "") | startswith("Build and Test"))
|
||||
| (.conclusion // .state)] | unique | join(","))"'
|
||||
```
|
||||
|
||||
- Green = every `Build and Test *` job is `SUCCESS`.
|
||||
- `continuous-integration/jenkins/pr-merge` is **not** a required context and flakes red
|
||||
("This commit cannot be built"). `mergeStateStatus: UNSTABLE` with all build jobs green
|
||||
is mergeable. Ignore Jenkins.
|
||||
- Any `Build and Test *` failure, or `mergeStateStatus: BLOCKED` — **stop**. Report it and
|
||||
move on to the next PR.
|
||||
|
||||
## Checkpoint — classify, then stop
|
||||
|
||||
Present one row per open PR and **wait for approval** before any Jira write, title edit or
|
||||
merge. Required columns:
|
||||
|
||||
| PR | Base | Bump | Semver | Real scope (+ where declared) | Build | Action |
|
||||
|
||||
Only after approval, run the recipes below.
|
||||
|
||||
## Landing a bare bump
|
||||
|
||||
```bash
|
||||
gh pr merge <N> --squash --subject "<exact current PR title> (#<N>)"
|
||||
```
|
||||
|
||||
`del_branch_on_merge: true` is set, so no `--delete-branch`.
|
||||
|
||||
## Landing a ticketed bump
|
||||
|
||||
Five steps, in order. All five are required.
|
||||
|
||||
**1. Create the ticket** — `mcp__asf-issues__jira_create_issue`:
|
||||
|
||||
```json
|
||||
{
|
||||
"project_key": "WW",
|
||||
"issue_type": "Dependency",
|
||||
"summary": "Bump <group:artifact or property> from <old> to <new>",
|
||||
"description": "Bump <group:artifact or property> from <old> to <new>",
|
||||
"components": "<affected module>",
|
||||
"additional_fields": "{\"priority\": {\"name\": \"Trivial\"}, \"fixVersions\": [{\"name\": \"<branch release version>\"}]}"
|
||||
}
|
||||
```
|
||||
|
||||
- `components` is its **own** parameter (comma-separated names), not a member of
|
||||
`additional_fields`. `additional_fields` must be a JSON **string**, not an object.
|
||||
- Issue type is **Dependency** — not Task, not Bug.
|
||||
- Component is the module that declares the dep: `Core`, `Plugin - REST`, `Plugin - JSON`,
|
||||
`Unit Tests`, `Build Management`, …
|
||||
- Fix version comes from the target branch's SNAPSHOT with `-SNAPSHOT` dropped
|
||||
(`grep -m1 SNAPSHOT pom.xml`): `main` → 7.4.0, `support/struts-6-x-x` → 6.12.0. This is a
|
||||
placeholder the release process may revise; do not treat it as a release commitment.
|
||||
|
||||
**2. Retitle the PR** — insert the ticket after the conventional-commit prefix, leave every
|
||||
other character alone:
|
||||
|
||||
```bash
|
||||
gh pr edit <N> --title "build(deps): WW-XXXX bump org.htmlunit:htmlunit from 4.21.0 to 5.1.0"
|
||||
# ^^^^^^^^ inserted; prefix and remainder verbatim
|
||||
```
|
||||
|
||||
**3. Add the Closes line to the body** — its own paragraph, after the leading `Bumps …`
|
||||
block and before the first `<details>`. Preserve the rest of Dependabot's body exactly:
|
||||
|
||||
```
|
||||
Bumps [org.htmlunit:htmlunit](https://github.com/HtmlUnit/htmlunit) from 4.21.0 to 5.1.0.
|
||||
|
||||
Closes [WW-XXXX](https://issues.apache.org/jira/browse/WW-XXXX)
|
||||
|
||||
<details>
|
||||
```
|
||||
|
||||
**4. Merge with the ticket in the squash subject:**
|
||||
|
||||
```bash
|
||||
gh pr merge <N> --squash --subject "build(deps): WW-XXXX bump org.htmlunit:htmlunit from 4.21.0 to 5.1.0 (#<N>)"
|
||||
```
|
||||
|
||||
Without an explicit `--subject`, GitHub takes the subject from Dependabot's *commit*
|
||||
headline and the ticket ID is silently lost from git history.
|
||||
|
||||
**5. Close the ticket as Fixed** — only after *every* PR on the ticket has merged:
|
||||
|
||||
```json
|
||||
{"issue_key": "WW-XXXX", "fields": "{\"status\": \"Closed\"}", "return_fields": "status"}
|
||||
```
|
||||
|
||||
The Jira MCP always resolves as *Fixed* and silently swallows any other resolution name —
|
||||
which is what is wanted here.
|
||||
|
||||
## One dependency, two branches, one ticket
|
||||
|
||||
Dependabot opens the same bump separately against `main` and `support/struts-6-x-x`. Before
|
||||
creating anything, look for the twin:
|
||||
|
||||
```bash
|
||||
gh pr list --state open --author app/dependabot --json number,title,baseRefName
|
||||
```
|
||||
|
||||
If a twin exists, create **one** ticket, put it in both PR titles/bodies, and list **both**
|
||||
fix versions on it (WW-5649 carries 6.11.0 and 7.3.0 for PRs #1760 and #1763). Close it once
|
||||
both have merged.
|
||||
|
||||
## Scope of this skill
|
||||
|
||||
Triage and land the queue. Do **not**, as part of it, open follow-up PRs — `dependabot.yml`
|
||||
ignore rules, license-header restorations, test-harness fixes. Report such findings in one
|
||||
line under the table and let the decision be made separately.
|
||||
|
||||
A ticket-worthy bump with a red build is real compatibility work, not a merge. Report the
|
||||
failure and stop; it needs its own ticket and its own branch. Never push a fix onto a
|
||||
Dependabot branch — Dependabot stops rebasing it, and a source change lands under a
|
||||
ticketless `build(deps):` title.
|
||||
|
||||
## Traps
|
||||
|
||||
| Trap | Reality |
|
||||
|---|---|
|
||||
| "It says `build(deps-dev)`, so it's a dev dependency" | That prefix is Dependabot's guess. `commons-validator` arrives as `deps-dev` and is test scope (true), but the prefix is not evidence — the POM is. |
|
||||
| "`dependency-type: direct:production`, so it ships" | `maven-wrapper` and `hibernate-core` both say `direct:production`; one is build tooling, the other ships. Read the POM. |
|
||||
| "`<optional>true</optional>` means users don't get it" | It is in the published POM and pins the version for anyone who opts in. Optional compile deps need a ticket. |
|
||||
| "git log shows no `WW-` on dependency commits, so titles aren't rewritten" | The squash *subject* historically came from Dependabot's commit headline while the *PR title* carried the ticket. Compare `gh pr view 1746 --json title` against `1f1674411`. Step 4 above exists to close that gap. |
|
||||
| "A red check means don't merge" | Only `Build and Test *` counts. The ASF Jenkins context is not required and flakes red. |
|
||||
| "Both branches need their own ticket" | One dependency, one ticket, two fix versions. |
|
||||
| "The build is green, so I can just merge it" | Green is necessary, not sufficient. Classify first, and stop at the checkpoint. |
|
||||
|
||||
## Red flags — stop and re-read
|
||||
|
||||
- About to run `gh pr merge` before presenting the classification table and getting approval
|
||||
- About to run `gh pr merge --squash` on a ticketed bump without `--subject`
|
||||
- Classified a dependency from the PR title alone, without grepping the POM for its scope
|
||||
- Creating a second ticket for a dependency that already has an open twin PR
|
||||
- Opening a follow-up PR that nobody asked for
|
||||
- Creating a Jira issue whose type is anything other than `Dependency`
|
||||
|
||||
## Related
|
||||
|
||||
- `creating-version-notes` — ticketed bumps appear in the Version Notes Dependency section;
|
||||
bare bumps deliberately do not, and their absence is not a reconciliation gap.
|
||||
@@ -1,153 +0,0 @@
|
||||
---
|
||||
name: releasing-struts
|
||||
description: Use when running or planning an Apache Struts release on any maintenance line (6.x, 7.x) - cutting the tag, staging artifacts, opening the vote, promoting, updating the site and announcing - or when asked what the next step in a release is.
|
||||
---
|
||||
|
||||
# Releasing Struts
|
||||
|
||||
## Overview
|
||||
|
||||
A release is seven phases with a gate between each.
|
||||
|
||||
**The process itself is published**, at
|
||||
[Release Guidelines](https://struts.apache.org/release-guidelines.html) — every phase, every
|
||||
command, the release policy and the one-time setup a new release manager needs. It is the source
|
||||
of truth, it is maintained in `apache/struts-site` (`source/release-guidelines.md`), and it is
|
||||
what you follow.
|
||||
|
||||
This skill is the agent's half of it: the judgement about *ordering* and *when to stop*, which
|
||||
sibling skill owns which document, and the points where a step is the release manager's to take
|
||||
rather than yours. [`release-runbook.md`](release-runbook.md) holds that last part.
|
||||
|
||||
**Core principle:** a phase is finished when its gate is verifiable by someone other than you.
|
||||
"I ran the command" is not a gate; "the URL resolves" is.
|
||||
|
||||
**Corrections go to the site page.** If a release teaches you something the guidelines get wrong,
|
||||
fix them in a PR to `apache/struts-site`. Only what is genuinely agent-specific belongs here.
|
||||
|
||||
## The phases
|
||||
|
||||
| # | Phase | Gate before moving on |
|
||||
|---|---|---|
|
||||
| 1 | Prepare | Branch green, version decided, parent poms released |
|
||||
| 2 | Cut | Tag pushed, artifacts in a **closed** Nexus staging repo |
|
||||
| 3 | Stage | Assemblies in `dist/dev`, Version Notes page live, `[TEST]` mail sent |
|
||||
| 4 | Vote | 72 h elapsed, three binding `+1`, result mail sent |
|
||||
| 5 | Promote | Nexus repo released, `dist/dev` → `dist/release`, 24 h rsync waited |
|
||||
| 6 | Publish | Site PR merged, GitHub release un-flagged, `[ANN]` mail delivered |
|
||||
| 7 | Advisories | Bulletins public, CVE records filled, advisory mails sent from the CVE tool |
|
||||
|
||||
Phase 7 only exists when the release carries a security fix, and *publishing* the advisory is
|
||||
**strictly after** phase 6 — see *Security work is a separate clock* below. Writing the bulletin
|
||||
is not: it is usually drafted long before the release exists, and often on its own timetable
|
||||
entirely.
|
||||
|
||||
## Which skill owns which artifact
|
||||
|
||||
Cross-references, not copies. Do not restate what these settle:
|
||||
|
||||
- **`creating-version-notes`** — the Version Notes page, its Staging Repository block, the
|
||||
Migration Guide entry, the GitHub release notes, and the `[TEST]` mail. All of phase 3's
|
||||
paperwork.
|
||||
- **`creating-release-vote-mail`** — the `[VOTE]` mail. All of phase 4's paperwork.
|
||||
- **`creating-security-bulletins`** — the S2-XXX page, what may be disclosed and when,
|
||||
publication, and the advisory mails.
|
||||
|
||||
**That last one is not a phase of this process.** A bulletin gets written when the report is
|
||||
triaged, which may be months before a release carries the fix, and plenty of bulletins are
|
||||
handled with no release in flight at all. It is a skill in its own right, invoked whenever it is
|
||||
needed. Phase 7 is the reverse direction: *if* this release carries a security fix, then once
|
||||
phase 6 is done, go and follow that skill.
|
||||
|
||||
Phases 1, 2, 5 and 6 have no sibling skill — the guidelines carry those steps, and this skill
|
||||
carries the ordering that binds them.
|
||||
|
||||
## Two lines, two releases
|
||||
|
||||
`main` is the 7.x line; `support/struts-6-x-x` is 6.x. Both are protected and both require their
|
||||
build to pass. A change that lands on both is **two releases**, each with its own tag, vote,
|
||||
site entry and announcement — not one release mentioned twice.
|
||||
|
||||
They can be cut in parallel and voted in parallel, and usually are. Keep the version numbers
|
||||
independent: 6.11.0 and 7.3.0 shipped together and share nothing but a date.
|
||||
|
||||
**Neither line branch is where the release is cut.** Both August 2026 releases were built on a
|
||||
`release/X.Y.Z-RC1` branch off the line, so the `[maven-release-plugin]` commits never reach
|
||||
`main`. A failed vote is then a deleted branch, not a revert.
|
||||
|
||||
## The version number is chosen at release time
|
||||
|
||||
The `-SNAPSHOT` in the pom is a placeholder, not a decision. Pick the number from the semver
|
||||
impact of what actually landed since the last tag, and say so out loud before cutting — the tag
|
||||
is the first irreversible act of the release.
|
||||
|
||||
The pom cannot tell you: because releases are cut on a side branch, `main` still read
|
||||
`7.2.2-SNAPSHOT` after 7.3.0 had shipped.
|
||||
|
||||
## Security work is a separate clock
|
||||
|
||||
**Nothing about an unpublished advisory goes into the release paperwork.** Not the Version
|
||||
Notes, not the `[TEST]` mail, not the `[VOTE]`, not the commit messages, not the site entry.
|
||||
The tickets are neutral; that is deliberate and it is what makes the embargo survive a public
|
||||
release process.
|
||||
|
||||
The advisory follows the release, and the ordering is not negotiable:
|
||||
|
||||
```
|
||||
release GA → bulletin unrestricted → advisory mails → CVE pushed to MITRE
|
||||
```
|
||||
|
||||
A bulletin published before the fixed artifact is downloadable tells attackers what to look for
|
||||
and gives operators nothing to do about it.
|
||||
|
||||
**A 6.x release containing only embargoed fixes is self-disclosing** — the diff between the two
|
||||
tags is the vulnerability whatever the commit messages say. That is a reason to bundle it with
|
||||
unrelated work, or to publish the bulletins with the release, not a reason to pretend otherwise.
|
||||
|
||||
## The cwiki release pages are retired
|
||||
|
||||
The wiki pages a release manager used to land on — *Building Struts 2 — Normal release*,
|
||||
*Fast track release*, *Creating and Signing a Distribution*, *One time steps*,
|
||||
*Sample announcements* — were retired in August 2026 and now carry nothing but a pointer to
|
||||
[Release Guidelines](https://struts.apache.org/release-guidelines.html). Their old content
|
||||
survives only in page history, where it describes a process last revised between 2013 and 2017:
|
||||
`develop`/`master` branches, `people.apache.org`, an svn checkout of the production site.
|
||||
|
||||
**Never restore a step from that history.** If something in the guidelines looks incomplete, the
|
||||
answer is the last release, not the last wiki revision.
|
||||
|
||||
## Gates that are actually load-bearing
|
||||
|
||||
- **A closed Nexus staging repo, not just a successful `release:perform`.** Until it is closed
|
||||
the URL in the Version Notes resolves to nothing and every tester is blocked.
|
||||
- **72 hours, and three binding `+1`.** PMC votes are the binding ones; `private@` is on the
|
||||
vote mail so binding voters see it.
|
||||
- **24 hours after the `dist` move, before announcing.** ASF mirroring guidance. Announcing into
|
||||
an unmirrored release sends everyone to a 404.
|
||||
- **The GitHub release stops being a prerelease at phase 6, not at phase 3.** During the vote it
|
||||
must still be flagged, or the vote is on an artifact the world already treats as final.
|
||||
|
||||
## Red Flags — STOP
|
||||
|
||||
- Cutting a tag before the version number has been stated and agreed
|
||||
- A `[VOTE]` opened on a staging repo that is not closed, or on a link that 404s
|
||||
- Announcing before the 24-hour mirror wait
|
||||
- Any severity, CVE, S2-XXX or bulletin link in release paperwork
|
||||
- A bulletin unrestricted before the fixed release is downloadable
|
||||
- Reviving a step from the history of a retired cwiki page
|
||||
- One release "covering" both maintenance lines
|
||||
- Inferring the release version from the `-SNAPSHOT` in the pom
|
||||
- Closing or releasing a Nexus staging repository yourself — that is the release manager's login
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Reality |
|
||||
|---|---|
|
||||
| "`release:perform` succeeded, so the artifacts are staged" | They are staged and *open*. Close the repo or nobody can fetch them. |
|
||||
| "The vote passed, so it's released" | Nexus release, dist move and the mirror wait all come after. |
|
||||
| "I'll announce now and fix the site after" | The announcement links the site. Merge the site PR first. |
|
||||
| "The 6.x fix is the same change, so one announcement covers both" | Two artifacts, two downloads, two sets of affected users. |
|
||||
| "The advisory is an announcement, so it gets `[ANN]` too" | `[ANN]` is for releases. CVE reports take the CVE tool's subject unedited. |
|
||||
| "The pom says 7.3.1-SNAPSHOT, so this is 7.3.1" | The placeholder is not a decision. Semver impact decides. |
|
||||
| "The process is documented in this skill" | It is documented on the site. This skill adds ordering, ownership and hand-offs. |
|
||||
| "I found the release steps on the wiki" | Those pages are stubs now. Their history is the 2013–2017 process. |
|
||||
@@ -1,113 +0,0 @@
|
||||
# Release Runbook
|
||||
|
||||
**The process is published, not kept here.** Every phase, every command, every gate is at
|
||||
[Release Guidelines](https://struts.apache.org/release-guidelines.html), maintained in
|
||||
`apache/struts-site` as `source/release-guidelines.md`. Read it there and follow it.
|
||||
|
||||
This file holds only what that page cannot: the points where a step is a human's to take rather
|
||||
than yours, and the scripts this skill ships. [`SKILL.md`](SKILL.md) holds the sequence, the
|
||||
gates and the judgement.
|
||||
|
||||
**When you learn something new during a release, it goes in the site page.** A correction that
|
||||
lands only here is a correction the next release manager will never see.
|
||||
|
||||
## The scripts
|
||||
|
||||
Phases 3 and 5 ship with this skill, in [`scripts/`](scripts). The Release Guidelines link to
|
||||
them by GitHub URL, so they are part of the published process — changing their behaviour means
|
||||
updating that page too.
|
||||
|
||||
| Script | Phase | What it does |
|
||||
|---|---|---|
|
||||
| [`stage-assemblies.sh`](scripts/stage-assemblies.sh) | 3 | Closed staging repo → `dist/dev`, renamed and re-hashed |
|
||||
| [`promote-dist.sh`](scripts/promote-dist.sh) | 5 | `dist/dev` → `dist/release` |
|
||||
|
||||
Both take `$VERSION` from the environment, refuse to run without it, and refuse a value that is
|
||||
not a version number — `svn` resolves a `.` path element rather than rejecting it, so a stray
|
||||
`VERSION` would otherwise move the whole staging tree in one irreversible commit.
|
||||
|
||||
Run them from a scratch directory, never from a repository checkout: `stage-assemblies.sh`
|
||||
creates `./$VERSION` and a temporary svn working copy in the current directory. That means
|
||||
calling them by absolute path, since the scratch directory is not the checkout:
|
||||
|
||||
```bash
|
||||
cd "$(mktemp -d)"
|
||||
VERSION=7.3.0 ~/Projects/Apache/struts/.claude/skills/releasing-struts/scripts/stage-assemblies.sh
|
||||
```
|
||||
|
||||
## Phase 1 — Prepare
|
||||
|
||||
**If the JDK is wrong, ask — do not infer.** `mvn -v` reports what Maven is actually using, and
|
||||
the line dictates what that must be (7.x on 17, 6.x on 8). Local environments differ — jenv,
|
||||
SDKMAN, asdf, `JAVA_HOME` by hand, a Homebrew symlink — and guessing at someone's toolchain is
|
||||
how you end up building against a JDK they did not intend. `.java-version` is gitignored in this
|
||||
repo, so it is not a signal either.
|
||||
|
||||
**State the version number and get agreement before phase 2 begins.** The tag is the first
|
||||
irreversible act of the release, and the pom cannot tell you the number.
|
||||
|
||||
## Phase 2 — Cut
|
||||
|
||||
At the SCM tag prompt, `STRUTS_X_Y_Z` is typed by hand every time. **This one cannot move into
|
||||
the pom**, so do not "fix" it: `tagNameFormat` interpolates `@{project.version}` and has no
|
||||
string functions, so the best it could produce is `STRUTS_7.3.0`. The prompt stays.
|
||||
|
||||
**Closing the staging repository is the release manager's action, not yours.** It happens in the
|
||||
Nexus web UI at <https://repository.apache.org/> (Staging Repositories → select → Close), behind
|
||||
an ASF login. Say so, hand over, and **wait for confirmation before continuing** — phase 3
|
||||
fetches from the staging *group* URL and gets nothing while the repository is open.
|
||||
|
||||
The gate is worth checking yourself once you are told it is done:
|
||||
|
||||
```
|
||||
https://repository.apache.org/content/groups/staging/org/apache/struts/struts2-core/$VERSION/
|
||||
```
|
||||
|
||||
## Phase 3 — Stage
|
||||
|
||||
Run [`stage-assemblies.sh`](scripts/stage-assemblies.sh) as above, then **count the files** at
|
||||
`https://dist.apache.org/repos/dist/dev/struts/$VERSION/`: six assemblies, each with `.asc`,
|
||||
`.sha256` and `.sha512`, 24 in total. `set -eu` stops the script on a step that *fails*, not on a
|
||||
crawl that quietly returns a subset, so a short upload reaches `dist/dev` looking healthy.
|
||||
|
||||
Everything else in this phase belongs to **`creating-version-notes`**: the Version Notes page,
|
||||
its Staging Repository block, the Migration Guide entry, the GitHub release (created as a
|
||||
**prerelease**), and the `[TEST]` mail.
|
||||
|
||||
## Phase 4 — Vote
|
||||
|
||||
**`creating-release-vote-mail`** owns the mail. Nothing here.
|
||||
|
||||
## Phase 5 — Promote
|
||||
|
||||
Run [`promote-dist.sh`](scripts/promote-dist.sh). **Releasing the staging repository in Nexus is
|
||||
again the release manager's action** in the web UI — hand over and wait, as in phase 2.
|
||||
|
||||
Pruning old releases from `dist/release/struts/` is a deliberate decision, never an autopilot
|
||||
step: several supported versions from both lines are normally kept.
|
||||
|
||||
## Phase 6 — Publish
|
||||
|
||||
**The `[ANN]` prefix belongs to this mail, not to advisories.** Release announcements keep it;
|
||||
CVE advisory subjects do not — see phase 7.
|
||||
|
||||
**The `[ANN]` mail must be `text/plain`, and a draft made with the Gmail tool is an HTML draft
|
||||
whatever you pass it.** `announce@apache.org` rejects any message carrying a `text/html` part —
|
||||
|
||||
```
|
||||
ezmlm-reject: fatal: Sorry, a message part has an unacceptable MIME Content-Type: 'text/html' (#5.2.3)
|
||||
```
|
||||
|
||||
— and `announcements@struts.apache.org` answers *"Must be sent from an @apache.org address."*
|
||||
See *The mail must be text/plain* in `creating-release-vote-mail` for the full contract. One list
|
||||
accepting the mail is not evidence the format was right.
|
||||
|
||||
## Phase 7 — Advisories
|
||||
|
||||
**`creating-security-bulletins`** owns all of it: unrestricting the bulletin, the CVE record on
|
||||
<https://cveprocess.apache.org>, and the advisory mails. Follow that skill from here; it is not
|
||||
a step in this runbook.
|
||||
|
||||
**The mails are sent by the `Send these Emails` button** on the record's *OSS/ASF Emails* tab,
|
||||
not from a mailbox, and the subject the tool generates carries no `[ANN]`. That skill owns the
|
||||
detail, including the `userslist` field the button depends on.
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Phase 5 - promote a release that passed its vote, moving the assemblies from
|
||||
# dist/dev to dist/release. This is the point at which the artifacts start
|
||||
# replicating to the mirrors.
|
||||
#
|
||||
# Usage: VERSION=7.3.0 ./promote-dist.sh
|
||||
#
|
||||
# Run it only after the vote has passed. Wait 24 hours after this before
|
||||
# announcing anything - the announcement links a download page that the mirrors
|
||||
# have to have caught up with first.
|
||||
|
||||
set -eu
|
||||
|
||||
if [ -z "${VERSION:-}" ]; then
|
||||
echo "VERSION is not set. Usage: VERSION=7.3.0 $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Not cosmetic. svn resolves a "." path element instead of rejecting it, so
|
||||
# VERSION="." would move the whole of dist/dev/struts into dist/release in one
|
||||
# irreversible server-side commit. ".." is rejected by svn; "." is not.
|
||||
case "$VERSION" in
|
||||
[0-9]*.[0-9]*.[0-9]*) ;;
|
||||
*)
|
||||
echo "VERSION must look like 7.3.0 (got '$VERSION')" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
svn mv "https://dist.apache.org/repos/dist/dev/struts/$VERSION/" \
|
||||
"https://dist.apache.org/repos/dist/release/struts/" \
|
||||
-m "Release Struts $VERSION"
|
||||
|
||||
echo "Done - verify https://dist.apache.org/repos/dist/release/struts/$VERSION/"
|
||||
echo "Now release the staging repository in Nexus, then wait 24 hours before announcing."
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Phase 3 - move the release assemblies from the closed Nexus staging repository
|
||||
# into https://dist.apache.org/repos/dist/dev/struts/$VERSION so they can be tested
|
||||
# and voted on.
|
||||
#
|
||||
# Usage: cd "$(mktemp -d)" && VERSION=7.3.0 /path/to/stage-assemblies.sh
|
||||
#
|
||||
# Run it from a scratch directory, not from a repository checkout: it creates
|
||||
# ./$VERSION and a temporary svn working copy in the current directory.
|
||||
#
|
||||
# Requires: the staging repository must already be CLOSED in Nexus (an open repo
|
||||
# serves nothing under the staging *group* URL this fetches from), and your ASF
|
||||
# svn credentials for dist.apache.org.
|
||||
|
||||
set -eu
|
||||
|
||||
if [ -z "${VERSION:-}" ]; then
|
||||
echo "VERSION is not set. Usage: VERSION=7.3.0 $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Not cosmetic: a VERSION of "." resolves server-side to the parent directory,
|
||||
# which would publish the whole staging tree.
|
||||
case "$VERSION" in
|
||||
[0-9]*.[0-9]*.[0-9]*) ;;
|
||||
*)
|
||||
echo "VERSION must look like 7.3.0 (got '$VERSION')" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
STAGING_URL="https://repository.apache.org/content/groups/staging/org/apache/struts/struts2-assembly/$VERSION"
|
||||
DIST_DEV_URL="https://dist.apache.org/repos/dist/dev/struts/"
|
||||
|
||||
if [ -e "$VERSION" ]; then
|
||||
echo "Directory $VERSION already exists here - remove it or run elsewhere." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -e struts-dev ]; then
|
||||
echo "Directory struts-dev already exists here - remove it or run elsewhere." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unconditional, as in the original: a half-built working copy left behind can be
|
||||
# picked up and committed by a later run for a different version.
|
||||
cleanup() {
|
||||
rm -rf "$START_DIR/struts-dev"
|
||||
}
|
||||
START_DIR=$(pwd)
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Creating working dir $VERSION"
|
||||
mkdir "$VERSION"
|
||||
cd "$VERSION"
|
||||
|
||||
echo "Getting distro $VERSION from the staging repository"
|
||||
if ! wget -erobots=off -nv -l 1 --accept=zip,md5,sha1,asc -r --no-check-certificate -nd -nH "$STAGING_URL"; then
|
||||
echo "Download failed. Is the staging repository closed in Nexus?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! ls ./*.zip >/dev/null 2>&1; then
|
||||
echo "No assemblies downloaded. Is the staging repository closed in Nexus?" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# struts2-assembly-7.3.0-all.zip -> struts-7.3.0-all.zip, and the same for the
|
||||
# .asc/.md5/.sha1 beside each zip. The .pom files keep their name and are removed
|
||||
# below - narrowing this glob without widening that one republishes them.
|
||||
echo "Renaming files"
|
||||
for f in *2-assembly*.zip*; do
|
||||
[ -e "$f" ] || continue
|
||||
mv "$f" "$(echo "$f" | sed s/2-assembly//g)"
|
||||
done
|
||||
|
||||
echo "Removing unneeded files"
|
||||
rm -f struts2-assembly-*.pom*
|
||||
rm -f ./*.md5 ./*.sha1
|
||||
|
||||
# The ASF publishes sha256/sha512; Nexus only carries the legacy hashes.
|
||||
echo "Generating SHA signatures"
|
||||
for f in *.zip; do
|
||||
[ -f "$f" ] || continue
|
||||
shasum -a 256 "$f" > "$f.sha256"
|
||||
shasum -a 512 "$f" > "$f.sha512"
|
||||
done
|
||||
|
||||
echo "Staging the following files:"
|
||||
ls -1
|
||||
|
||||
cd "$START_DIR"
|
||||
|
||||
echo "Publishing artifacts for test"
|
||||
svn --no-auth-cache co --depth empty "$DIST_DEV_URL" struts-dev
|
||||
mv "$VERSION" struts-dev/
|
||||
cd struts-dev
|
||||
svn add --force ./
|
||||
svn --no-auth-cache commit -m "Updates test release $VERSION"
|
||||
|
||||
cd "$START_DIR"
|
||||
|
||||
echo "Done - verify https://dist.apache.org/repos/dist/dev/struts/$VERSION/"
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
name: triaging-security-reports
|
||||
description: Use when a vulnerability or security report arrives for triage, when assessing a CVE/RCE/OGNL/injection claim against the code, or when drafting a reply to a security researcher — to research the claim from source without trusting the reporter and without fabricating your own facts.
|
||||
---
|
||||
|
||||
# Triaging Security Reports
|
||||
|
||||
## Overview
|
||||
|
||||
A security report is a **claim to be tested, not a finding to be confirmed or rebutted**. The reporter may be right, wrong, partially right, or right about the symptom and wrong about the cause. Your job is to independently re-derive the truth from current source.
|
||||
|
||||
**Core principle:** Every factual statement that ends up in your assessment or reply — the reporter's claims *and your own* — must be traced to current source code before you write it down. The most common failure is not believing the reporter; it is **inventing supporting facts to justify a verdict you already reached.**
|
||||
|
||||
**Process authority:** [`SECURITY.md`](../../../SECURITY.md) is the source of truth for the disclosure process (private handling, assessment checklist, reporting rules). Read it. This skill governs *how you research and respond*, not the process itself.
|
||||
|
||||
## The Iron Rule
|
||||
|
||||
```
|
||||
NO CLAIM IN A SECURITY RESPONSE WITHOUT A FILE:LINE YOU READ THIS SESSION.
|
||||
```
|
||||
|
||||
Applies to the verdict, every mitigation you cite, and every "default" you state. If you can't point to the line, you can't write the sentence.
|
||||
|
||||
## Research: report-blind, not report-led
|
||||
|
||||
Read the report once to know what to investigate. Then **research as if you were auditing that area cold** — do not let the report's framing drive your search.
|
||||
|
||||
For each claim, independently verify:
|
||||
|
||||
| Reporter asserts | You must verify from source |
|
||||
|---|---|
|
||||
| A line number ("bug is at X:392") | Read that line **and its call path** — is it even reachable as described? |
|
||||
| A severity / CVSS | Re-derive from actual exploitability, not their number |
|
||||
| "No mitigation / no gate exists" | Search for gates, filters, allowlists, authorizers *yourself* — absence claims are the most often wrong |
|
||||
| "Default configuration" | Check the **effective runtime default**, not one source (see trap below) |
|
||||
| "Same as CVE-XXXX" | Confirm the mechanism actually matches; analogy ≠ equivalence |
|
||||
| A working PoC | **Run it if it is runnable**, then trace whether the payload survives every filter on the path |
|
||||
|
||||
If the report has **no reproducible PoC against a default config**, that is itself a triage outcome — say so per `SECURITY.md`.
|
||||
|
||||
## Find the control case
|
||||
|
||||
A single odd behaviour is ambiguous — it can nearly always be read as intended. What settles it is the **sibling that behaves correctly under the same input**.
|
||||
|
||||
Before writing a verdict, find the case that ought to differ and check it: the annotated property beside the unannotated one, the ordinary setter beside the dynamic one, the sibling path the same control does cover. Behave alike and you are probably looking at a design decision. Diverge, and the control is incomplete — that divergence *is* the finding.
|
||||
|
||||
Prefer an executed differential to an argued one. An existing test that passes beside the reporter's failing one is the strongest evidence a triage can produce.
|
||||
|
||||
## The effective-default trap
|
||||
|
||||
A Java field initializer and the shipped config can disagree. Reading only one produces a confident, wrong claim.
|
||||
|
||||
```java
|
||||
private boolean requireAnnotations = false; // field initializer
|
||||
```
|
||||
```properties
|
||||
struts.parameters.requireAnnotations=true # default.properties OVERRIDES it
|
||||
```
|
||||
|
||||
**The effective default is `true`.** Always trace the full chain: field initializer → `@Inject` setter → `default.properties` → any struts.xml override. State the *effective runtime* value, and cite the file that actually wins.
|
||||
|
||||
## Vulnerability vs. operator responsibility
|
||||
|
||||
"In the default configuration" is a crutch — drop it. Decide the real question:
|
||||
|
||||
- **Is it a vulnerability?** Then it's a vulnerability whether or not it's the default. Handle it privately per `SECURITY.md`.
|
||||
- **Does it require an operator to opt into an insecure configuration?** A documented, opt-in setting (e.g. `cookiesName=*`, `devMode=true`) that works as advertised is the operator's responsibility, provided the docs carry the warning. Say "X works as documented; the operator owns the security implications of enabling it" — not "not a vuln *in the default config*."
|
||||
- **Is the RCE/escalation only reachable via application code the framework can't constrain?** (e.g. an action that moves an uploaded file to a web root.) Then it's an application concern, not a framework vulnerability — state that boundary explicitly.
|
||||
|
||||
## Drafting the reply
|
||||
|
||||
- Lead with the verdict and the *reason*, both grounded in file:line.
|
||||
- Cite a source for every mitigation you mention. If you didn't verify it this session, delete the sentence.
|
||||
- Prefer "works as documented / operator responsibility" framing over "default configuration."
|
||||
- **Don't over-promise.** Before pledging a hardening change, check it doesn't already exist (it often does) and that you intend to actually do it.
|
||||
- Acknowledge anything the reporter got right (e.g. correct CVE-fix verification) — it builds the relationship and signals you actually read it.
|
||||
- Keep it private: no public issue, PR, Jira, or list thread before triage. Never open a PR that is itself the security fix (see [`CLAUDE.md`](../../../CLAUDE.md)).
|
||||
|
||||
## Red Flags — STOP
|
||||
|
||||
- About to write "this is mitigated by X" — did you read X's line *this session*?
|
||||
- About to state a "default" from a field initializer — did you check `default.properties`?
|
||||
- Citing the reporter's line number without having traced its call path.
|
||||
- Asserting "no gate / no check exists" without having grepped for it.
|
||||
- Two of your own claims contradict each other → at least one is unverified. Stop and verify both.
|
||||
- Promising a fix/warning "we'll add" without checking it isn't already there.
|
||||
- Writing "not a vulnerability in the default configuration" → reframe as vuln-or-not + operator responsibility.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
| Mistake | Reality |
|
||||
|---|---|
|
||||
| "Reporter cited line 392, so that's the bug site" | A line is only a bug if it's *reachable* as described. Trace callers. |
|
||||
| "The field defaults to false, so the gate is off by default" | `default.properties` may override it to true. Check the effective value. |
|
||||
| "I'll add a mitigation to strengthen the rejection" | An unverified mitigation that's wrong discredits the whole response. Verify or omit. |
|
||||
| "It rejects the payload, obviously" | Confirm the specific PoC string fails the specific filter (e.g. full-match regex `ACCEPTED_PATTERN`). |
|
||||
| "We should add a startup warning" | Grep first — the warning frequently already exists. |
|
||||
| "Not a vuln in default config" | Either it's a vuln or it's operator-owned opt-in. The default-config hedge muddies both. |
|
||||
@@ -1,5 +0,0 @@
|
||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto
|
||||
*.bat eol=crlf
|
||||
*.cmd eol=crlf
|
||||
*.sh eol=lf
|
||||
@@ -1,42 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 3
|
||||
target-branch: "main"
|
||||
- package-ecosystem: "maven"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
cooldown:
|
||||
default-days: 4
|
||||
target-branch: "support/struts-6-x-x"
|
||||
ignore:
|
||||
- dependency-name: "org.eclipse.jetty:jetty-maven-plugin"
|
||||
- dependency-name: "com.github.ben-manes.caffeine:caffeine"
|
||||
- dependency-name: "com.sun.xml.bind:jaxb-impl"
|
||||
- dependency-name: "javax.portlet:portlet-api"
|
||||
- dependency-name: "javax.servlet:javax.servlet-api"
|
||||
- dependency-name: "javax.servlet.jsp:jsp-api"
|
||||
- dependency-name: "org.mortbay.jetty:jsp-2.1"
|
||||
- dependency-name: "ognl:ognl"
|
||||
- dependency-name: "org.hibernate.validator:hibernate-validator"
|
||||
- dependency-name: "org.testng:testng"
|
||||
- dependency-name: "org.mockito:mockito-core"
|
||||
- dependency-name: "opensymphony:sitemesh"
|
||||
- dependency-name: "net.sf.jasperreports:jasperreports"
|
||||
- dependency-name: "javax.enterprise:cdi-api"
|
||||
- dependency-name: "org.springframework:*"
|
||||
- dependency-name: "org.apache.struts:struts-annotations"
|
||||
- dependency-name: "org.apache.juneau:juneau-marshall"
|
||||
- dependency-name: "org.apache.tomcat:tomcat-api"
|
||||
- dependency-name: "org.apache.tomcat:tomcat-juli"
|
||||
- dependency-name: "org.apache.tomcat:tomcat-jasper"
|
||||
- dependency-name: "org.apache.rat:apache-rat-plugin"
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Apache Struts pull request review guide. Use when reviewing pull requests in this repository to check test conventions, security-sensitive framework code, PR and commit hygiene, and Struts-specific implementation patterns.
|
||||
license: Apache-2.0
|
||||
---
|
||||
|
||||
# Reviewing Apache Struts pull requests
|
||||
|
||||
Apache Struts is a mature MVC framework for Java. It uses OGNL for value stack
|
||||
expressions and FreeMarker for UI tag templates, and it has a long history of
|
||||
security vulnerabilities in exactly those areas. Review accordingly: the
|
||||
conventions below are not style preferences, they are the places where changes
|
||||
tend to go wrong in this codebase.
|
||||
|
||||
Deeper references, when a review needs them:
|
||||
|
||||
- [`CLAUDE.md`](../../../CLAUDE.md) — build commands, module layout, request lifecycle
|
||||
- [`SECURITY.md`](../../../SECURITY.md) — the vulnerability reporting process (source of truth)
|
||||
- [`THREAT_MODEL.md`](../../../THREAT_MODEL.md) — scope, trust boundaries, known non-findings
|
||||
- [`AGENTS.md`](../../../AGENTS.md) — rules for AI agents working on security findings
|
||||
|
||||
## 1. Tests
|
||||
|
||||
**This repository is JUnit 4. There is no JUnit 5 anywhere in it.**
|
||||
`parent/pom.xml` declares `junit:junit:4.13.2`; there are zero
|
||||
`org.junit.jupiter` imports. Two styles coexist and a new test must match the
|
||||
style already in the file it joins:
|
||||
|
||||
- **JUnit 3 style** — classes extending `XWorkTestCase`, which extends
|
||||
`junit.framework.TestCase`. Test methods must be named `testXxx()`. A Jupiter
|
||||
`@Test` annotation added to one of these **silently never runs** — it does not
|
||||
fail, it is simply not collected. Flag this as blocking whenever you see
|
||||
`org.junit.jupiter` in a diff.
|
||||
- **JUnit 4 style** — classes using `import org.junit.Test`.
|
||||
|
||||
Both styles are widespread and neither is being migrated away from.
|
||||
|
||||
AssertJ assertions and Mockito mocks are both available and widely used.
|
||||
Introducing JUnit 5 is a build-infrastructure change that needs its own `WW-`
|
||||
ticket; it is never a side effect of a feature PR.
|
||||
|
||||
### Tests that pass without testing anything
|
||||
|
||||
Three traps in this codebase produce green tests that assert nothing. Check for
|
||||
them whenever a PR adds a test in these areas:
|
||||
|
||||
- **Unpushed action.** An action object that was never pushed onto the value
|
||||
stack binds no parameters at all, so an assertion that "the parameter was not
|
||||
bound" passes for the wrong reason. Confirm the fixture pushes the action.
|
||||
- **`requireAnnotations` is off by default in a bare harness.**
|
||||
`ParametersInterceptor.requireAnnotations` is a Java field initialised to
|
||||
`false`; production turns it on through
|
||||
`struts.parameters.requireAnnotations=true` in `default.properties`. A test
|
||||
that constructs the interceptor directly and expects an unannotated parameter
|
||||
to be rejected will pass with the check disabled. The test must set the flag.
|
||||
- **Interned string literals defeat identity checks.** Where the framework
|
||||
compares against a marker constant with `==`, a String *literal* in a test is
|
||||
interned to the same instance as the constant, so the test passes vacuously
|
||||
even against unfixed code. Such fixtures must build the value at runtime, with
|
||||
an `assertNotSame` guard proving they did.
|
||||
|
||||
Run tests with `mvn test -DskipAssembly`; a single test with
|
||||
`mvn test -DskipAssembly -pl core -Dtest=MyClassTest#testMethodName`.
|
||||
|
||||
## 2. Security-sensitive changes
|
||||
|
||||
Watch for diffs touching OGNL expression evaluation, the OGNL allowlist and
|
||||
member access policies, parameter filtering and `@StrutsParameter` gating,
|
||||
file upload handling, action mapping and name cleanup, deserialization, path
|
||||
handling, or escaping in framework components and UI tag templates.
|
||||
|
||||
**When a change looks like it fixes a vulnerability rather than an ordinary
|
||||
bug, say so in neutral terms and stop there.** A pull request is public, so a
|
||||
review comment that names the weakness, explains how it is reached, or
|
||||
estimates its impact is itself a disclosure — precisely what the project's
|
||||
private process exists to prevent.
|
||||
|
||||
Use wording of this shape, and no more than this:
|
||||
|
||||
> This change touches security-sensitive framework code. Please confirm it is
|
||||
> not a fix for a suspected vulnerability before merging — see `SECURITY.md`.
|
||||
> Vulnerability fixes go through the private process at
|
||||
> `security@struts.apache.org`, not a public pull request.
|
||||
|
||||
Do **not**, in a review comment: describe the suspected weakness or its class,
|
||||
sketch an exploit or a triggering input, assess exploitability or severity,
|
||||
speculate about affected versions, or link the change to a specific CVE or
|
||||
security bulletin. If a reviewer needs to raise any of that, it belongs in
|
||||
private mail to `security@struts.apache.org`.
|
||||
|
||||
## 3. Pull request and commit hygiene
|
||||
|
||||
- **Title** — `WW-XXXX Description`. A Jira ticket ID is required for any code
|
||||
change; the tracker is <https://issues.apache.org/jira/projects/WW>.
|
||||
- **Description** — links the ticket:
|
||||
`Fixes [WW-XXXX](https://issues.apache.org/jira/browse/WW-XXXX)`.
|
||||
- **Exception** — pure documentation and build/CI changes (`SECURITY.md`,
|
||||
`AGENTS.md`, `CLAUDE.md`, `.github/`, workflows) take no ticket and use
|
||||
conventional-commit form instead: `docs: ...`, `build(ci): ...`, `chore: ...`.
|
||||
- Commit messages follow the same rule as the title.
|
||||
|
||||
Flag a missing or malformed ticket reference as a non-blocking comment, not as
|
||||
a code defect.
|
||||
|
||||
## 4. Struts implementation patterns
|
||||
|
||||
- **Temporary files** get UUID-based names in a controlled location, never a
|
||||
name derived from user input:
|
||||
|
||||
```java
|
||||
protected File createTemporaryFile(String fileName, Path location) {
|
||||
String uid = UUID.randomUUID().toString().replace("-", "_");
|
||||
return location.resolve("upload_" + uid + ".tmp").toFile();
|
||||
}
|
||||
```
|
||||
|
||||
- **OGNL** — evaluate only framework-generated expressions, and keep member
|
||||
access on an allowlist. Treat any new path that evaluates a
|
||||
request-derived string as OGNL as a blocking finding under section 2.
|
||||
- **Parameters** — request-settable action properties need `@StrutsParameter`;
|
||||
use `ParameterNameAware` to restrict accepted parameter names. Note that a
|
||||
`ModelDriven` action's own setters are a known exemption, so review changes
|
||||
there with care.
|
||||
- **Uploads** — validate content types, sanitise file names, enforce size
|
||||
limits.
|
||||
- **Naming** — framework default implementations are prefixed `Struts`
|
||||
(`StrutsBeanSelectionProvider`), not `Default`, for new classes.
|
||||
- **No placeholder TODOs** — a comment must not reference a Jira ticket that
|
||||
has not been filed.
|
||||
|
||||
## Reviewing the review
|
||||
|
||||
Keep findings proportionate. Blocking comments are for correctness, the JUnit 5
|
||||
trap, vacuous tests, and security-sensitive code paths. Everything else —
|
||||
naming, ticket references, style — is a suggestion or a nitpick, and should be
|
||||
labelled as such.
|
||||
@@ -1,70 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache license, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the license for the specific language governing permissions and
|
||||
# limitations under the license.
|
||||
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/*'
|
||||
- 'support/*'
|
||||
paths-ignore:
|
||||
- '.claude/**'
|
||||
# Safe to filter by path here: no check from this workflow is required in
|
||||
# .asf.yaml, so a run that never happens blocks nothing.
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.claude/**'
|
||||
|
||||
permissions:
|
||||
# Needed to upload the results to code-scanning dashboard.
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
# Needed to access OIDC token.
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'java' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Java JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
cache: 'maven'
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4.37.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4.37.8
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4.37.8
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
@@ -1,125 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: Java Maven
|
||||
|
||||
on:
|
||||
# Deliberately NOT filtered by path. "Build and Test (JDK 17)" is a required
|
||||
# status check in .asf.yaml, and a workflow skipped by path filtering never
|
||||
# reports its checks - they stay Pending and the pull request can never be
|
||||
# merged. The build job always runs instead, and its steps are skipped by
|
||||
# condition when only .claude/ changed (see `changes` below).
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'develop'
|
||||
- 'release/*'
|
||||
- 'support/*'
|
||||
paths-ignore:
|
||||
- '.claude/**'
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions: read-all
|
||||
|
||||
env:
|
||||
MAVEN_OPTS: -Xmx2048m -Xms1024m
|
||||
LANG: en_US.utf8
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes outside .claude
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- name: Check which paths the pull request touches
|
||||
id: filter
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -eu
|
||||
if [ "${{ github.event_name }}" != "pull_request" ]; then
|
||||
echo "Not a pull request - building."
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
files=$(gh api --paginate \
|
||||
"repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" \
|
||||
--jq '.[].filename')
|
||||
echo "Changed files:"
|
||||
printf '%s\n' "$files"
|
||||
# Anything outside .claude/ means a real build is needed; an empty
|
||||
# diff, or one confined to .claude/, does not. Tested by emptiness
|
||||
# rather than with `grep -qv`, whose exit status is not reliable
|
||||
# across grep implementations.
|
||||
outside=$(printf '%s\n' "$files" | grep -vE '^(\.claude/|$)' || true)
|
||||
if [ -n "$outside" ]; then
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Only .claude/ changed - skipping the build."
|
||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build and Test (JDK ${{ matrix.java }})${{ matrix.profile == '-Pjakartaee11' && ' (Jakarta EE 11 + Spring 7)' || matrix.profile }}
|
||||
needs: changes
|
||||
# No job-level `if:` here on purpose. A matrix job whose condition is false
|
||||
# is skipped *before* the matrix expands, so it reports a single check run
|
||||
# named after the raw `${{ matrix.* }}` template - never "Build and Test
|
||||
# (JDK 17)". The required context then never reports at all and the pull
|
||||
# request stays blocked. Gate the steps instead: the matrix expands, every
|
||||
# expected check reports success, and nothing is built.
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- java: '17'
|
||||
profile: ''
|
||||
- java: '21'
|
||||
profile: ''
|
||||
- java: '21'
|
||||
profile: '-Pjakartaee11'
|
||||
- java: '25'
|
||||
profile: ''
|
||||
- java: '25'
|
||||
profile: '-Pjakartaee11'
|
||||
steps:
|
||||
- name: Checkout code
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Java ${{ matrix.java }}
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: ${{ matrix.java }}
|
||||
cache: 'maven'
|
||||
- name: Maven Verify on Java ${{ matrix.java }}${{ matrix.profile == '-Pjakartaee11' && ' (Jakarta EE 11 + Spring 7)' || matrix.profile }}
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
run: mvn -B -V -DskipAssembly verify ${{ matrix.profile }} --no-transfer-progress
|
||||
|
||||
- name: Test Summary ${{ matrix.java }} ${{ matrix.profile }}
|
||||
uses: mikepenz/action-junit-report@d9f48fc87bc235f7e214acf696ca5abc0a986f16 #v6.4.2
|
||||
continue-on-error: true
|
||||
if: always() && needs.changes.outputs.code == 'true'
|
||||
with:
|
||||
annotate_only: true # forked repo cannot write to checks so just do annotations
|
||||
report_paths: |
|
||||
**/surefire-reports/TEST-*.xml
|
||||
**/failsafe-reports/TEST-*.xml
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: OWASP checkup
|
||||
|
||||
on:
|
||||
# Safe to filter by path here: no check from this workflow is required in
|
||||
# .asf.yaml, so a run that never happens blocks nothing.
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.claude/**'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'develop'
|
||||
- 'release/*'
|
||||
- 'support/*'
|
||||
paths-ignore:
|
||||
- '.claude/**'
|
||||
workflow_dispatch: #Allow manual triggers
|
||||
|
||||
permissions: read-all
|
||||
|
||||
env:
|
||||
MAVEN_OPTS: -Xmx2048m -Xms1024m
|
||||
LANG: en_US.utf8
|
||||
|
||||
jobs:
|
||||
|
||||
|
||||
owasp:
|
||||
name: OWASP
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
HAVE_NIST_NVD_API_KEY: ${{ secrets.NIST_NVD_API_KEY != '' }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
- name: Setup Java 25
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 25
|
||||
cache: 'maven'
|
||||
|
||||
- name: Cache NVD Database
|
||||
id: cache-nvd
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ~/.m2/repository/org/owasp/dependency-check-data
|
||||
key: nvd-cache-${{ runner.os }}-owasp-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
nvd-cache-${{ runner.os }}-owasp-
|
||||
nvd-cache-${{ runner.os }}-
|
||||
|
||||
- name: OWASP Dependency check update cache via NIST_NVD_API_KEY
|
||||
id: nvd-api-update
|
||||
if: ${{ env.HAVE_NIST_NVD_API_KEY == 'true' }}
|
||||
continue-on-error: true
|
||||
run: mvn -N -V -DskipAssembly -Dmaven.test.skip=true -Powasp-nvd-api -Pdependency-update-only --no-transfer-progress
|
||||
env:
|
||||
NIST_NVD_API_KEY: ${{ secrets.NIST_NVD_API_KEY}}
|
||||
|
||||
- name: OWASP Dependency check update cache via Mirror
|
||||
if: ${{ env.HAVE_NIST_NVD_API_KEY == 'false' || steps.nvd-api-update.outcome == 'failure' }}
|
||||
run: mvn -N -V -DskipAssembly -Dmaven.test.skip=true -Powasp-nvd-mirror -Pdependency-update-only --no-transfer-progress
|
||||
|
||||
- name: Cache NVD Database
|
||||
uses: actions/cache/save@v6
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
path: ~/.m2/repository/org/owasp/dependency-check-data
|
||||
key: nvd-cache-${{ runner.os }}-owasp-${{ github.run_id }}
|
||||
|
||||
- name: OWASP check (Without running tests)
|
||||
run: mvn -B org.owasp:dependency-check-maven:aggregate -Pdependency-check -Pjakartaee11 -DautoUpdate=false --no-transfer-progress
|
||||
|
||||
- name: Upload Dependency Check reports
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: dependency-check
|
||||
path: target/dependency-check*
|
||||
|
||||
- name: Add OWASP summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "## OWASP Dependency Check"
|
||||
echo ""
|
||||
echo "The HTML report has been uploaded as the **dependency-check** artifact."
|
||||
echo "Download it from the Artifacts section of this workflow run."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -1,70 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache license, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the license for the specific language governing permissions and
|
||||
# limitations under the license.
|
||||
|
||||
name: "Scorecards supply-chain security"
|
||||
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: "30 1 * * 6" # Weekly on Saturdays
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
|
||||
analysis:
|
||||
|
||||
name: "Scorecards analysis"
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload the results to the code-scanning dashboard.
|
||||
security-events: write
|
||||
actions: read
|
||||
id-token: write # This is required for requesting the JWT
|
||||
contents: read # This is required for actions/checkout
|
||||
|
||||
steps:
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@v7 # 3.1.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: "Run analysis"
|
||||
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # 2.4.4
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# A read-only PAT token, which is sufficient for the action to function.
|
||||
# The relevant discussion: https://github.com/ossf/scorecard-action/issues/188
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Publish the results for public repositories to enable scorecard badges.
|
||||
# For more details: https://github.com/ossf/scorecard-action#publishing-results
|
||||
publish_results: true
|
||||
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # 7.0.1
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@f58f0d11ebf5dedd870fab2f999275f7602cfa46 # 2.22.11
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -1,61 +0,0 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: SonarCloud
|
||||
|
||||
on:
|
||||
# Safe to filter by path here: no check from this workflow is required in
|
||||
# .asf.yaml, so a run that never happens blocks nothing.
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.claude/**'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths-ignore:
|
||||
- '.claude/**'
|
||||
|
||||
permissions: read-all
|
||||
|
||||
env:
|
||||
MAVEN_OPTS: -Xmx2048m -Xms1024m
|
||||
LANG: en_US.utf8
|
||||
HAVE_SONARCLOUD_TOKEN: ${{ secrets.SONARCLOUD_TOKEN != '' }}
|
||||
|
||||
jobs:
|
||||
sonarcloud:
|
||||
name: Scan
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.pull_request.base.repo.fork && !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
cache: 'maven'
|
||||
- name: SonarCloud Scan
|
||||
if: ${{ env.HAVE_SONARCLOUD_TOKEN == 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }}
|
||||
run: ./mvnw -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Pcoverage -DskipAssembly
|
||||
- name: SonarCloud Scan -- SKIPPED
|
||||
if: ${{ env.HAVE_SONARCLOUD_TOKEN != 'true' }}
|
||||
run: |
|
||||
echo "### SonarCloud not configured" >> $GITHUB_STEP_SUMMARY
|
||||
echo "secrets.SONARCLOUD_TOKEN not existing, cannot push coverage checks" >> $GITHUB_STEP_SUMMARY
|
||||
-58
@@ -1,58 +0,0 @@
|
||||
# IDEA
|
||||
.idea
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# Eclipse
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
.metadata/
|
||||
Servers/
|
||||
|
||||
# Java annotation processor (APT)
|
||||
.factorypath
|
||||
|
||||
#VSCode
|
||||
.vscode
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Scripts
|
||||
*.sh
|
||||
# ... except the ones that are part of a skill and have to travel with it
|
||||
!.claude/skills/**/scripts/*.sh
|
||||
|
||||
# jenv
|
||||
.java-version
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
plugins/testng/test-output
|
||||
test-output
|
||||
|
||||
# Sonar
|
||||
/.sonar/
|
||||
|
||||
# Tidelift CLI scanner
|
||||
.tidelift
|
||||
|
||||
# Claude Code local settings
|
||||
.claude/settings.local.json
|
||||
|
||||
# Cursor + Metals
|
||||
.cursor/
|
||||
.bloop/
|
||||
.metals/
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
|
||||
@@ -1,31 +0,0 @@
|
||||
# Vulnerability Research Agent
|
||||
|
||||
You are helping a security researcher evaluate and report potential vulnerabilities in Apache Struts.
|
||||
|
||||
[`SECURITY.md`](SECURITY.md) is the source of truth for the Apache Struts vulnerability reporting process. **Read it first and follow it.** This file is a short
|
||||
LLM-facing wrapper around that policy; it does not replace it.
|
||||
|
||||
## Workflow
|
||||
|
||||
Before drafting any report, opening an issue, posting publicly, or reaching a security conclusion:
|
||||
|
||||
1. **Pre-reporting checks** — complete every step in [`SECURITY.md` § Before Reporting](SECURITY.md#before-reporting):
|
||||
- read the Struts security policy,
|
||||
- read the Struts security guidelines,
|
||||
- check previously disclosed vulnerabilities and Security Bulletins.
|
||||
2. **Assess** the finding against the questions in [`SECURITY.md` § Assessment](SECURITY.md#assessment), and route it through the disposition guide in
|
||||
[`THREAT_MODEL.md`](THREAT_MODEL.md) (scope, trust boundaries, in-model security properties, and the known-non-findings list). If the answers do not still
|
||||
point to a likely new framework vulnerability, stop and explain — do not draft a new report.
|
||||
3. **Report privately** to `security@struts.apache.org` following [`SECURITY.md` § Private Report Requirements](SECURITY.md#private-report-requirements) and
|
||||
[§ Report Quality Rules](SECURITY.md#report-quality-rules).
|
||||
|
||||
Do not open a public GitHub issue, Jira issue, pull request, mailing list thread, or discussion for a suspected vulnerability before private triage.
|
||||
|
||||
## Rules for AI Agents
|
||||
|
||||
- **Never submit a pull request that fixes a suspected vulnerability.** Before opening any PR, verify the change is not a security patch — OGNL injection,
|
||||
parameter filtering bypass, file upload exploit, authentication or authorization bypass, RCE, SSRF, path traversal, deserialization, XSS in framework
|
||||
components, etc. If it is, stop and direct the researcher to report it privately to `security@struts.apache.org` instead. Vulnerability fixes go through
|
||||
the private security process, not public PRs.
|
||||
- Do not speculate beyond what can be demonstrated. If severity is uncertain, say so explicitly.
|
||||
- If the issue turns out to be application misconfiguration, an already-disclosed CVE, or a non-Struts problem, stop and explain — do not draft a new report.
|
||||
@@ -1,107 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
For detailed procedures, use the specialized skills in `.claude/skills/`.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Apache Struts is a mature MVC web application framework for Java (originally WebWork 2). Uses OGNL for value stack expressions and FreeMarker for UI tag templates.
|
||||
|
||||
**Version**: read it from the root `pom.xml` — it is `7.4.0-SNAPSHOT` as of 2026-08-23. Do not treat the `-SNAPSHOT` value as the next release number: the release version is chosen at release time from the semver impact of the accumulated changes, so `7.4.0-SNAPSHOT` may well ship as something else. Released versions are git tags like `STRUTS_7_2_1`.
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# Run tests (skip assembly for speed)
|
||||
mvn test -DskipAssembly
|
||||
|
||||
# Single test in specific module
|
||||
mvn test -DskipAssembly -pl core -Dtest=MyClassTest#testMethodName
|
||||
|
||||
# Jakarta EE 11 / Spring 7 profile
|
||||
mvn clean install -Pjakartaee11
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
struts/
|
||||
├── core/ # struts2-core - main framework
|
||||
├── plugins/ # Plugin modules (json, rest, spring, tiles, velocity, etc.)
|
||||
├── apps/ # Sample applications (showcase, rest-showcase)
|
||||
├── assembly/ # Distribution packaging
|
||||
├── bom/ # Bill of Materials for dependency management
|
||||
├── parent/ # Parent POM with shared configuration
|
||||
└── jakarta/ # Jakarta EE compatibility modules
|
||||
```
|
||||
|
||||
### Core Architecture
|
||||
|
||||
**Request Lifecycle**: `Dispatcher` → `ActionProxy` → `ActionInvocation` → Interceptor stack → `Action` → Result
|
||||
|
||||
Key packages in `org.apache.struts2`:
|
||||
|
||||
- `dispatcher` - Request handling, `Dispatcher`, servlet integration
|
||||
- `interceptor` - Built-in interceptors (params, validation, fileUpload)
|
||||
- `components` - UI tag components (form, textfield, submit)
|
||||
- `action` - Action interfaces (`UploadedFilesAware`, `SessionAware`, etc.)
|
||||
- `security` - Security utilities and OGNL member access policies
|
||||
|
||||
## Security-Critical Patterns
|
||||
|
||||
Apache Struts has a history of security vulnerabilities (OGNL injection, temp file exploits). Apply these Struts-specific patterns:
|
||||
|
||||
1. **Temporary files**: Use UUID-based names in controlled locations (see example below)
|
||||
2. **OGNL expressions**: Evaluate only framework-generated OGNL; use allowlist member access
|
||||
3. **File uploads**: Validate content types, sanitize filenames, enforce size limits
|
||||
4. **Parameter filtering**: Use `ParameterNameAware` to restrict accepted parameter names
|
||||
|
||||
```java
|
||||
// Secure temporary file pattern
|
||||
protected File createTemporaryFile(String fileName, Path location) {
|
||||
String uid = UUID.randomUUID().toString().replace("-", "_");
|
||||
return location.resolve("upload_" + uid + ".tmp").toFile();
|
||||
}
|
||||
```
|
||||
|
||||
## Security Reports & Scans
|
||||
|
||||
For any security-related activity — vulnerability scans, security analysis, drafting security reports — **[`SECURITY.md`](SECURITY.md) is the source of truth**.
|
||||
Read it first and follow its pre-reporting checks, assessment checklist, and reporting requirements. Reports must be sent privately to
|
||||
`security@struts.apache.org`; do not open a public GitHub issue, Jira issue, pull request, or mailing list thread for a suspected vulnerability before private
|
||||
triage. [`AGENTS.md`](AGENTS.md) is a shorter LLM-facing wrapper around the same process.
|
||||
[`THREAT_MODEL.md`](THREAT_MODEL.md) is where the scope lives — trust boundaries, the security properties the framework does and does not provide, the
|
||||
recurring non-findings (§11a), and the triage dispositions (§13). Read it before judging whether a finding is real: on Struts most candidate findings are
|
||||
closed as application responsibility or non-default configuration rather than as framework bugs.
|
||||
|
||||
## Testing
|
||||
|
||||
Run with `mvn test -DskipAssembly`.
|
||||
|
||||
**Tests are JUnit 4 — there is no JUnit 5 anywhere in this repo.** `parent/pom.xml` declares
|
||||
`junit:junit:4.13.2`; there are zero `org.junit.jupiter` imports. Two styles coexist:
|
||||
|
||||
- **JUnit 3 style** — classes extending `XWorkTestCase` (which extends `junit.framework.TestCase`).
|
||||
Methods must be named `testXxx()`. A Jupiter `@Test` annotation added to one of these **silently
|
||||
never runs** — it does not fail, it is simply not collected.
|
||||
- **JUnit 4 style** — classes using `import org.junit.Test`.
|
||||
|
||||
Both styles are widespread and neither is being migrated away from; `grep -rl` for either marker
|
||||
gives the current split.
|
||||
|
||||
Before adding a test, open the target file and match the style already there. AssertJ assertions and
|
||||
Mockito mocks are both available and widely used. Introducing Jupiter is a build-infrastructure change
|
||||
that needs its own `WW-` ticket, never a side effect of a feature.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
- **Title format**: `WW-XXXX Description` — a Jira ticket ID is required for any code change.
|
||||
Pure documentation and build/CI changes (`SECURITY.md`, `AGENTS.md`, `CLAUDE.md`, `.claude/`,
|
||||
workflows) take no ticket and use conventional-commit form instead: `docs: ...`, `build(ci): ...`,
|
||||
`chore: ...`
|
||||
- **Link ticket in description**: `Fixes [WW-XXXX](https://issues.apache.org/jira/browse/WW-XXXX)`
|
||||
- **Issue tracker**: https://issues.apache.org/jira/projects/WW
|
||||
- **Never submit a PR that fixes a suspected vulnerability.** Before opening a PR, verify the change is not a security patch (OGNL injection, parameter
|
||||
filtering bypass, file upload exploit, auth bypass, RCE, SSRF, path traversal, deserialization, XSS in framework components, etc.). If it is, stop and report
|
||||
it privately to `security@struts.apache.org` — see [`SECURITY.md`](SECURITY.md).
|
||||
@@ -1,2 +0,0 @@
|
||||
# Request PR review from any Apache Struts committer
|
||||
* @apache/struts-committers
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
<!---
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
# Contributing to Apache Struts
|
||||
|
||||
Thanks for your interest in contributing! Apache Struts is maintained by a
|
||||
community of volunteers under the [Apache Software Foundation](https://www.apache.org/).
|
||||
This guide walks a first-time contributor from a fresh clone to a merged pull
|
||||
request. You do not need to be a committer to contribute — anyone can open a PR.
|
||||
|
||||
## Getting help
|
||||
|
||||
- **Mailing lists:** Subscribe and ask on the developer or user list — see
|
||||
<https://struts.apache.org/mail.html>. The developer list is the best place
|
||||
to discuss a change before you start larger work.
|
||||
- **Issue tracker:** [JIRA WW project](https://issues.apache.org/jira/projects/WW).
|
||||
- **Homepage & docs:** <https://struts.apache.org/>.
|
||||
|
||||
If you are unsure whether a change is wanted, ask on the developer list or
|
||||
comment on the relevant JIRA issue first.
|
||||
|
||||
## Project overview
|
||||
|
||||
Apache Struts is a mature MVC web framework for Java (originally WebWork 2). It
|
||||
uses OGNL for value-stack expressions and FreeMarker for UI tag templates. The
|
||||
repository is a multi-module Maven build:
|
||||
|
||||
| Module | Responsibility |
|
||||
|------------|-------------------------------------------------------------|
|
||||
| `core` | `struts2-core` — the main framework |
|
||||
| `plugins` | Plugin modules (json, rest, spring, tiles, velocity, …) |
|
||||
| `apps` | Sample applications (showcase, rest-showcase) |
|
||||
| `assembly` | Distribution packaging |
|
||||
| `bom` | Bill of Materials for dependency management |
|
||||
| `parent` | Parent POM with shared configuration |
|
||||
| `jakarta` | Jakarta EE compatibility modules |
|
||||
|
||||
The request lifecycle is `Dispatcher` → `ActionProxy` → `ActionInvocation` →
|
||||
interceptor stack → `Action` → `Result`.
|
||||
|
||||
## Prerequisites & building
|
||||
|
||||
- **JDK 17** and **Maven**.
|
||||
- Run the tests (skipping assembly for speed):
|
||||
|
||||
```bash
|
||||
mvn test -DskipAssembly
|
||||
```
|
||||
|
||||
- Run a single test in a specific module:
|
||||
|
||||
```bash
|
||||
mvn test -DskipAssembly -pl core -Dtest=MyClassTest#testMethodName
|
||||
```
|
||||
|
||||
- Build against the Jakarta EE 11 / Spring 7 profile:
|
||||
|
||||
```bash
|
||||
mvn clean install -Pjakartaee11
|
||||
```
|
||||
|
||||
Tests use JUnit 5 with AssertJ assertions and Mockito for mocking.
|
||||
|
||||
## Finding something to work on
|
||||
|
||||
Browse the [JIRA WW project](https://issues.apache.org/jira/projects/WW) for
|
||||
open issues. Comment on an issue to let others know you are working on it. If
|
||||
no ticket exists for your change, **file one first** — every commit and pull
|
||||
request must reference a `WW-XXXX` ticket ID.
|
||||
|
||||
## Development workflow
|
||||
|
||||
1. Fork the repository and clone your fork.
|
||||
2. Create a branch off `main` named after the ticket, e.g. `WW-1234-short-description`.
|
||||
3. Implement your change **with tests**. Keep commits focused.
|
||||
4. Prefix every commit message with the ticket ID: `WW-1234 Describe the change`.
|
||||
5. Run `mvn test -DskipAssembly` and make sure it passes before opening a PR.
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
- **Title format:** `WW-XXXX Description` (the JIRA ticket ID is required).
|
||||
- **Link the ticket** in the description:
|
||||
`Fixes [WW-XXXX](https://issues.apache.org/jira/browse/WW-XXXX)`.
|
||||
- Continuous integration must pass, and reviewers expect code changes to come
|
||||
with tests.
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
**Do not** open a public GitHub issue, JIRA issue, pull request, or
|
||||
mailing-list thread for a suspected vulnerability. Report it privately to
|
||||
**security@struts.apache.org**. See [`SECURITY.md`](SECURITY.md) for the full
|
||||
process. This includes OGNL injection, parameter-filtering bypasses, file
|
||||
upload exploits, authentication bypass, RCE, SSRF, path traversal,
|
||||
deserialization, and XSS in framework components.
|
||||
|
||||
## Licensing & Code of Conduct
|
||||
|
||||
- Apache Struts is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
- Every new source file must include the standard ASF license header (see any
|
||||
existing source file or this file's header for the exact text).
|
||||
- By submitting a pull request you agree to license your contribution under the
|
||||
Apache License 2.0. The ASF does not require a separate signed CLA for typical
|
||||
contributions.
|
||||
- All participation is governed by the
|
||||
[ASF Code of Conduct](https://www.apache.org/foundation/policies/conduct.html).
|
||||
Vendored
-279
@@ -1,279 +0,0 @@
|
||||
#!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
|
||||
options {
|
||||
buildDiscarder logRotator(daysToKeepStr: '14', numToKeepStr: '10')
|
||||
timeout(80)
|
||||
disableConcurrentBuilds()
|
||||
skipStagesAfterUnstable()
|
||||
quietPeriod(30)
|
||||
}
|
||||
triggers {
|
||||
pollSCM 'H/15 * * * *'
|
||||
}
|
||||
stages {
|
||||
stage('Prepare') {
|
||||
agent {
|
||||
label 'ubuntu'
|
||||
}
|
||||
stages {
|
||||
stage('Clean up') {
|
||||
steps {
|
||||
cleanWs deleteDirs: true, patterns: [[pattern: '**/target/**', type: 'INCLUDE']]
|
||||
}
|
||||
}
|
||||
stage('Detect changes') {
|
||||
steps {
|
||||
script {
|
||||
// Skip the build when a change only touched .claude/ - agent
|
||||
// instructions, not code. Fails open: anything unexpected (no
|
||||
// baseline, an unreachable commit, a git error) reports true and
|
||||
// the build runs as before.
|
||||
//
|
||||
// On a pull request the baseline is the merge base with the
|
||||
// target branch, NOT GIT_PREVIOUS_SUCCESSFUL_COMMIT. That pointer
|
||||
// is the previous head of this same PR, so once the PR is rebased
|
||||
// (or the target is merged into it) everything the target branch
|
||||
// absorbed in the meantime looks like a change of the PR's own.
|
||||
// The multibranch checkout already fetches the target branch, so
|
||||
// origin/$CHANGE_TARGET resolves here. On a branch build there is
|
||||
// no target and the previous successful commit is the only
|
||||
// baseline available.
|
||||
env.CODE_CHANGED = sh(returnStdout: true, script: '''
|
||||
set -u
|
||||
target="${CHANGE_TARGET:-}"
|
||||
if [ -n "$target" ]; then
|
||||
base=$(git merge-base "origin/${target}" HEAD 2>/dev/null || true)
|
||||
else
|
||||
base="${GIT_PREVIOUS_SUCCESSFUL_COMMIT:-}"
|
||||
fi
|
||||
if [ -z "$base" ] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
|
||||
echo true
|
||||
exit 0
|
||||
fi
|
||||
outside=$(git diff --name-only "$base" HEAD | grep -vE '^(\\.claude/|$)' || true)
|
||||
if [ -n "$outside" ]; then
|
||||
echo true
|
||||
else
|
||||
echo false
|
||||
fi
|
||||
''').trim()
|
||||
echo "Changes outside .claude/: ${env.CODE_CHANGED}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('JDK 21') {
|
||||
when {
|
||||
expression { env.CODE_CHANGED != 'false' }
|
||||
}
|
||||
agent {
|
||||
label 'ubuntu'
|
||||
}
|
||||
tools {
|
||||
jdk 'jdk_21_latest'
|
||||
maven 'maven_3_latest'
|
||||
}
|
||||
environment {
|
||||
MAVEN_OPTS = "-Xmx1024m"
|
||||
}
|
||||
stages {
|
||||
stage('Test') {
|
||||
steps {
|
||||
sh './mvnw -B -DskipAssembly verify'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit(testResults: '**/surefire-reports/*.xml', allowEmptyResults: true)
|
||||
junit(testResults: '**/failsafe-reports/*.xml', allowEmptyResults: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
cleanWs deleteDirs: true, patterns: [[pattern: '**/target/**', type: 'INCLUDE']]
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('JDK 17') {
|
||||
when {
|
||||
expression { env.CODE_CHANGED != 'false' }
|
||||
}
|
||||
agent {
|
||||
label 'ubuntu'
|
||||
}
|
||||
tools {
|
||||
jdk 'jdk_17_latest'
|
||||
maven 'maven_3_latest'
|
||||
}
|
||||
environment {
|
||||
MAVEN_OPTS = "-Xmx2048m"
|
||||
}
|
||||
stages {
|
||||
stage('Install') {
|
||||
steps {
|
||||
sh './mvnw -B install -DskipTests -DskipAssembly'
|
||||
}
|
||||
}
|
||||
stage('Test') {
|
||||
steps {
|
||||
sh './mvnw -B verify -Pcoverage -DskipAssembly'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit(testResults: '**/surefire-reports/*.xml', allowEmptyResults: true)
|
||||
junit(testResults: '**/failsafe-reports/*.xml', allowEmptyResults: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Build Source & JavaDoc') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'main'
|
||||
branch 'support/struts-6-x-x'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
dir("local-snapshots-dir/") {
|
||||
deleteDir()
|
||||
}
|
||||
sh './mvnw -B source:jar javadoc:jar -DskipTests -DskipAssembly'
|
||||
}
|
||||
}
|
||||
stage('Deploy Snapshot') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'main'
|
||||
branch 'support/struts-6-x-x'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
withCredentials([file(credentialsId: 'lukaszlenart-repository-access-token', variable: 'CUSTOM_SETTINGS')]) {
|
||||
sh './mvnw -s \${CUSTOM_SETTINGS} deploy -DskipTests -DskipAssembly'
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Upload nightlies') {
|
||||
when {
|
||||
anyOf {
|
||||
branch 'main'
|
||||
branch 'support/struts-6-x-x'
|
||||
}
|
||||
}
|
||||
steps {
|
||||
sh './mvnw -B package -DskipTests'
|
||||
sshPublisher(publishers: [
|
||||
sshPublisherDesc(
|
||||
configName: 'Nightlies',
|
||||
transfers: [
|
||||
sshTransfer(
|
||||
remoteDirectory: '/struts/snapshot',
|
||||
removePrefix: 'assembly/target/assembly/out',
|
||||
sourceFiles: 'assembly/target/assembly/out/struts-*.zip'
|
||||
)
|
||||
],
|
||||
verbose: true
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
cleanWs deleteDirs: true, patterns: [[pattern: '**/target/**', type: 'INCLUDE']]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
// If this build failed, send an email to the list.
|
||||
failure {
|
||||
script {
|
||||
emailext(
|
||||
to: "notifications@struts.apache.org",
|
||||
recipientProviders: [[$class: 'DevelopersRecipientProvider']],
|
||||
from: "Mr. Jenkins <jenkins@builds.apache.org>",
|
||||
subject: "Jenkins job ${env.JOB_NAME}#${env.BUILD_NUMBER} failed",
|
||||
body: """
|
||||
There is a build failure in ${env.JOB_NAME}.
|
||||
|
||||
Build: ${env.BUILD_URL}
|
||||
Logs: ${env.BUILD_URL}console
|
||||
Changes: ${env.BUILD_URL}changes
|
||||
|
||||
--
|
||||
Mr. Jenkins
|
||||
Director of Continuous Integration
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If this build didn't fail, but there were failing tests, send an email to the list.
|
||||
unstable {
|
||||
script {
|
||||
emailext(
|
||||
to: "notifications@struts.apache.org",
|
||||
recipientProviders: [[$class: 'DevelopersRecipientProvider']],
|
||||
from: "Mr. Jenkins <jenkins@builds.apache.org>",
|
||||
subject: "Jenkins job ${env.JOB_NAME}#${env.BUILD_NUMBER} unstable",
|
||||
body: """
|
||||
Some tests have failed in ${env.JOB_NAME}.
|
||||
|
||||
Build: ${env.BUILD_URL}
|
||||
Logs: ${env.BUILD_URL}console
|
||||
Changes: ${env.BUILD_URL}changes
|
||||
|
||||
--
|
||||
Mr. Jenkins
|
||||
Director of Continuous Integration
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Send an email, if the last build was not successful and this one is.
|
||||
fixed {
|
||||
script {
|
||||
emailext(
|
||||
to: "notifications@struts.apache.org",
|
||||
recipientProviders: [[$class: 'DevelopersRecipientProvider']],
|
||||
from: 'Mr. Jenkins <jenkins@builds.apache.org>',
|
||||
subject: "Jenkins job ${env.JOB_NAME}#${env.BUILD_NUMBER} back to normal",
|
||||
body: """
|
||||
The build for ${env.JOB_NAME} completed successfully and is back to normal.
|
||||
|
||||
Build: ${env.BUILD_URL}
|
||||
Logs: ${env.BUILD_URL}console
|
||||
Changes: ${env.BUILD_URL}changes
|
||||
|
||||
--
|
||||
Mr. Jenkins
|
||||
Director of Continuous Integration
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,117 +0,0 @@
|
||||
<!---
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
The Apache Struts web framework
|
||||
-------------------------------
|
||||
|
||||
[](https://ci-builds.apache.org/job/Struts/job/Struts%20Core/job/main/)
|
||||
[](https://github.com/apache/struts/actions/workflows/maven.yml)
|
||||
[](https://maven-badges.sml.io/sonatype-central/org.apache.struts/struts2-core/)
|
||||
[](https://javadoc.io/doc/org.apache.struts/struts2-core)
|
||||
[](https://sonarcloud.io/summary/new_code?id=apache_struts)
|
||||
[](https://deps.dev/maven/org.apache.struts%3Astruts2-core)
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/6618)
|
||||
[](http://www.apache.org/licenses/LICENSE-2.0.html)
|
||||
|
||||
The Apache Struts web framework is a free open-source solution for creating Java web applications.
|
||||
|
||||
## Documentation
|
||||
|
||||
More information can be found on the [homepage](https://struts.apache.org/). Please read the [Security Guide](https://struts.apache.org/security/),
|
||||
and the [JavaDocs](https://struts.apache.org/maven/struts2-core/apidocs/index.html) can be browsed.
|
||||
Questions related to the usage of Apache Struts should be posted to the [user mailing list](https://struts.apache.org/mail.html).
|
||||
|
||||
## Description
|
||||
|
||||
Web applications differ from conventional websites in that web applications can create a dynamic response. Many websites
|
||||
deliver only static pages. A web application can interact with databases and business logic engines to customize a response.
|
||||
|
||||
Web applications based on JavaServer Pages sometimes commingle database code, page design code, and control flow code.
|
||||
In practice, we find that unless these concerns are separated, larger applications become difficult to maintain.
|
||||
|
||||
One way to separate concerns in a software application is to use a Model-View-Controller (MVC) architecture. The Model
|
||||
represents the business or database code, the View represents the page design code, and the Controller represents
|
||||
the navigational code. The Struts framework is designed to help developers create web applications that utilize
|
||||
an MVC architecture.
|
||||
|
||||
The framework provides three key components:
|
||||
|
||||
- A “request” handler provided by the application developer that is mapped to a standard URI.
|
||||
- A “response” handler that transfers control to another resource which completes the response.
|
||||
- A tag library that helps developers create interactive form-based applications with server pages.
|
||||
|
||||
The framework’s architecture and tags are buzzword compliant. Struts works well
|
||||
with conventional REST applications and with technologies like SOAP and AJAX.
|
||||
|
||||
## The Apache Struts Project
|
||||
|
||||
The Apache Struts Project is the open source community that creates and maintains the Apache Struts framework.
|
||||
The project consists of a diverse group of volunteers who share common values regarding collaborative, community-based
|
||||
open source development. The Apache Struts Project is proud to share these values with our parent organization:
|
||||
The Apache Software Foundation.
|
||||
|
||||
The project is called “Struts” because the framework is meant to furnish the “invisible underpinnings” that support
|
||||
professional application development. Struts provides the glue that joins the various elements of the standard Java
|
||||
platform into a coherent whole. Our goal is to leverage existing standards by producing the missing pieces we need to create
|
||||
enterprise-grade applications that are easy to maintain over time.
|
||||
|
||||
The Apache Struts Project offered two major versions of the Struts framework. Currently we are only maintaining the Struts 2
|
||||
version. It is recommended to upgrade all Struts 1.x applications to Struts 2. Please do not start new application development
|
||||
using Struts 1.x, as we are no longer issuing security patches.
|
||||
|
||||
Struts 2 was originally known as WebWork 2. After working independently for several years, the WebWork and Struts
|
||||
communities joined forces to create Struts 2. The 2.x framework is the best choice for teams who value elegant solutions
|
||||
to difficult problems.
|
||||
|
||||
## Why should you use Apache Struts?
|
||||
|
||||
Apache Struts is a modern, maintained and full-featured web framework. As it has been around for years and grown a huge user
|
||||
base it is unlikely it will go away anytime soon. Not only that, we have dedicated users and developers
|
||||
on the project. Apache Struts is licensed under the Apache License 2.0 and this will not change. We maintain a clean IP
|
||||
and you are “safe” to use the project. Sometimes you are not “safe” to use a project when a company controls the SCM.
|
||||
Access to Source Code doesn’t mean it is free. With Apache Struts, you are not only free to “do what you want with it”,
|
||||
you can even contribute (which is not always the case). And best of all: you can become a part of the core team too.
|
||||
|
||||
It is usually very easy to integrate other technologies with Apache Struts. If you are using an ORM like Apache Cayenne,
|
||||
Hibernate or JDBC, you will not have any restrictions. Apache Struts is not even tied too much to a frontend technology.
|
||||
In old days it was JSP, then came Velocity and Freemarker. Nowadays you might build your web application with just static
|
||||
HTML and AngularJS. Or you might want to use Sitemesh or Tiles. This all is no problem due to Struts' elegant and easy-to-use
|
||||
extension mechanisms.
|
||||
|
||||
Unlike other, component-oriented frameworks, we do not aim to hide the stateless nature of the web. We think it is
|
||||
perfectly acceptable to build upon a Request/Response cycle. We also think the MVC pattern is not so bad, just because
|
||||
it is old. In fact, we believe the Apache Struts architecture is clean and easy to understand.
|
||||
|
||||
Of course, if you wish to build components on the server side which render on the front end side, you will most likely
|
||||
not want Struts. This is a different approach which promises to reduce the amount of HTML/JavaScript knowledge needed
|
||||
and to create reusable components for the view layer. Projects like Wicket and Tapestry serve this purpose very well.
|
||||
As with every framework, you need to decide if it makes sense for you to build components or if you prefer
|
||||
the Struts approach.
|
||||
|
||||
## Commercial Support
|
||||
|
||||
The Apache Struts community does not offer commercial support by itself, but we maintain [a list of companies offering
|
||||
commercial support on our website](https://struts.apache.org/commercial-support.html).
|
||||
|
||||
Some Apache Struts maintainers are working with [Tidelift](https://tidelift.com/) to provide commercial support and
|
||||
invest paid working time in the improvement of the Apache Struts framework. For more information, visit
|
||||
the [Tidelift resources regarding Apache Struts](https://tidelift.com/subscription/pkg/maven-org-apache-struts-struts2-core?utm_source=maven-org-apache-struts-struts2-core&utm_medium=referral&utm_campaign=readme)
|
||||
|
||||
## Thank you
|
||||
|
||||
[YourKit](https://www.yourkit.com/) is kindly supporting open source projects with its full-featured Java Profiler.
|
||||
YourKit is the creator of innovative and intelligent tools for profiling Java and .NET applications.
|
||||
As an Apache committer, you can get a free license at [YourKit's open source sponsorship program](https://www.yourkit.com/java/profiler/purchase/#os_license).
|
||||
-165
@@ -1,165 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Threat Model
|
||||
|
||||
A structured threat model for the Apache Struts framework — scope, adversary model,
|
||||
the security properties the framework provides vs. leaves to the application, and a
|
||||
triage-disposition guide for inbound reports and automated-scanner findings — is
|
||||
maintained in [`THREAT_MODEL.md`](THREAT_MODEL.md). It is additive to this policy:
|
||||
this `SECURITY.md` and the [security guidance](https://struts.apache.org/security/)
|
||||
remain canonical for the reporting process and configuration details.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Please visit the [Releases](https://struts.apache.org/releases.html#prior-releases) page to see full information about each version
|
||||
and what potential vulnerability it can have:
|
||||
|
||||
| Version | Supported |
|
||||
|---------|-----------|
|
||||
| 7.x | yes |
|
||||
| 6.x.x | yes |
|
||||
| 2.5.x | no |
|
||||
| 2.3.x | no |
|
||||
| 2.2.x | no |
|
||||
| 2.1.x | no |
|
||||
| 2.0.x | no |
|
||||
|
||||
## Reporting New Security Issues with the Apache Struts
|
||||
|
||||
([original](https://struts.apache.org/security.html))
|
||||
|
||||
The Apache Struts project takes a very active stance in eliminating security problems
|
||||
and denial of service attacks against applications using the Apache Struts framework.
|
||||
|
||||
**We strongly encourage folks to report such security problems to our private security mailing list first,
|
||||
before disclosing them in a public forum**.
|
||||
|
||||
We cannot accept regular bug reports or other queries at this address, we ask that you use our
|
||||
[issue tracker (JIRA)](https://issues.apache.org/jira/browse/WW) for those.
|
||||
|
||||
```
|
||||
All mail sent to this address that does not relate to security problems in the Apache Struts source code will be ignored
|
||||
```
|
||||
|
||||
Note that all networked servers are subject to denial of service attacks, and we cannot promise magic
|
||||
workarounds to generic problems (such as a client streaming lots of data to your server or requesting
|
||||
the same URL repeatedly). In general, our philosophy is to avoid any attacks that can cause the server
|
||||
to consume resources in a non-linear relationship to the size of inputs.
|
||||
|
||||
To be clear about the linear cases: that philosophy describes what we design against, not a threshold a
|
||||
report has to clear before we will accept it. Framework code that reads or retains request-derived input
|
||||
**without any bound** is in scope even where the growth is merely linear, and we have assigned CVEs for
|
||||
exactly that. What stays out of scope is the generic case above — where the cost to the server is
|
||||
proportionate, no framework limit is missing or ineffective, and the defence belongs to your servlet
|
||||
container, reverse proxy, or network.
|
||||
|
||||
The mailing address is: [security@struts.apache.org](mailto:security@struts.apache.org)
|
||||
|
||||
[General network server security tips](http://httpd.apache.org/docs/trunk/misc/security_tips.html)
|
||||
|
||||
[The Apache Security Team](http://www.apache.org/security/)
|
||||
|
||||
## Do not disclose through a pull request, commit, or issue
|
||||
|
||||
**A fix is a disclosure.** Opening a public pull request, pushing a commit, branch, or
|
||||
fork, or filing a public Jira/GitHub issue that **fixes, describes, or hints at** a
|
||||
suspected vulnerability reveals where the weakness is — often with a working roadmap to
|
||||
exploit it — before a fixed release exists. This holds even if you never attach a
|
||||
proof-of-concept, and even if you believe the impact is low or you are "just hardening"
|
||||
the code.
|
||||
|
||||
If you have found, or suspect you have found, a security problem:
|
||||
|
||||
- **Do not** open a public PR, commit, branch, fork, Jira issue, or mailing-list thread
|
||||
for it.
|
||||
- **Do** email [security@struts.apache.org](mailto:security@struts.apache.org) first and
|
||||
wait for the PMC to triage it and agree how the fix will be handled — the fix is
|
||||
typically prepared privately and landed alongside the advisory and release.
|
||||
|
||||
If you notice a possible security issue while working on an unrelated bug or PR, stop and
|
||||
email the private list before pushing the change. **When in doubt, treat it as
|
||||
security-sensitive and email the list** — a private report that turns out to be a
|
||||
non-issue costs far less than a public change that turns out to be exploitable.
|
||||
|
||||
## Before Reporting
|
||||
|
||||
Before sending a vulnerability report, run through the following checks. They exist to prevent duplicate reports, public disclosure of untriaged issues,
|
||||
and reports for behavior that is already documented as insecure configuration.
|
||||
|
||||
### 1. Read this policy
|
||||
|
||||
Confirm:
|
||||
|
||||
- which Struts versions are currently supported (see [Supported Versions](#supported-versions)),
|
||||
- where reports must be sent (see [Reporting New Security Issues](#reporting-new-security-issues-with-the-apache-struts)),
|
||||
- which reports do not belong on the private security list.
|
||||
|
||||
### 2. Read the Struts security guidelines
|
||||
|
||||
Review the [Struts security guidance](https://struts.apache.org/security/) and determine whether the finding is already covered by documented secure
|
||||
configuration or application guidance, including but not limited to:
|
||||
|
||||
- Config Browser Plugin exposure,
|
||||
- direct JSP access,
|
||||
- `devMode` is required to exploit the vulnerability,
|
||||
- `@StrutsParameter` usage and parameter annotation requirements,
|
||||
- unsafe setters or getters exposed to request parameters,
|
||||
- use of incoming values in localization or forced OGNL evaluation,
|
||||
- raw JSP EL expressions,
|
||||
- custom error pages,
|
||||
- Dynamic Method Invocation and Strict Method Invocation,
|
||||
- accepted and excluded parameter patterns,
|
||||
- Fetch Metadata, COOP, and COEP protections,
|
||||
- OGNL sandboxing, allowlists, excluded classes/packages, and OGNL Guard settings.
|
||||
|
||||
If the behavior is caused by an application ignoring documented security guidance, that is not an Apache Struts framework vulnerability.
|
||||
|
||||
### 3. Check previously disclosed vulnerabilities
|
||||
|
||||
Compare the finding against already disclosed Struts vulnerabilities — affected versions, impact ratings, mitigations, and fixed versions:
|
||||
|
||||
- [Struts security information](https://struts.apache.org/security/)
|
||||
- [Prior releases and vulnerability notes](https://struts.apache.org/releases.html#prior-releases)
|
||||
- [Security Bulletins (S2 series)](https://cwiki.apache.org/confluence/display/WW/Security+Bulletins)
|
||||
|
||||
If the finding overlaps with a known vulnerability, link to the existing bulletin, advisory, CVE, or release notes instead of drafting a new report.
|
||||
|
||||
## Assessment
|
||||
|
||||
Before drafting a report, confirm:
|
||||
|
||||
1. Is the affected version supported?
|
||||
2. Is the behavior in Apache Struts framework code, rather than only in an application using Struts?
|
||||
3. Is it already documented as insecure configuration or unsupported usage?
|
||||
4. Is it a duplicate of a previously disclosed vulnerability or Security Bulletin?
|
||||
5. Can the impact be demonstrated with a minimal, self-contained reproduction?
|
||||
|
||||
Only proceed with a private report when these answers still point to a likely new vulnerability in the framework.
|
||||
|
||||
## Private Report Requirements
|
||||
|
||||
A useful private report includes:
|
||||
|
||||
- affected Struts version or version range,
|
||||
- affected component or module,
|
||||
- required application configuration, if any,
|
||||
- minimal reproduction steps,
|
||||
- expected behavior,
|
||||
- actual behavior,
|
||||
- demonstrated security impact,
|
||||
- whether authentication or special privileges are required,
|
||||
- proposed fix or mitigation, if known.
|
||||
|
||||
Do not speculate beyond what can be demonstrated. If severity is uncertain, say so explicitly.
|
||||
|
||||
## Report Quality Rules
|
||||
|
||||
- One vulnerability per report.
|
||||
- Keep reproduction steps minimal and self-contained.
|
||||
- Do not include unrelated findings.
|
||||
- Do not publish exploit details or proof-of-concept code publicly before the Struts project has triaged the issue. **A fix, patch, or hardening change is a
|
||||
public disclosure in the same way a PoC is** — see [Do not disclose through a pull request, commit, or issue](#do-not-disclose-through-a-pull-request-commit-or-issue).
|
||||
**Pushing a PoC to a public GitHub repository, gist, fork, or branch counts as public disclosure** — even a "test" or throwaway repo. Private repositories
|
||||
are acceptable for sharing a PoC, but access must be granted individually to each PMC member who will triage the report.
|
||||
- Do not send ordinary bugs, usage questions, or generic denial-of-service concerns to the private security list.
|
||||
- If the issue is not a vulnerability in Apache Struts source code, use the appropriate public support or issue channel instead.
|
||||
-591
@@ -1,591 +0,0 @@
|
||||
<!--
|
||||
Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
contributor license agreements. See the NOTICE file distributed with
|
||||
this work for additional information regarding copyright ownership.
|
||||
The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
(the "License"); you may not use this file except in compliance with
|
||||
the License. You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
# Apache Struts — Threat Model (v0 draft)
|
||||
|
||||
## §1 Header
|
||||
|
||||
- **Project:** Apache Struts (`apache/struts`), `main` @ HEAD (2026-06). Scope: the
|
||||
Struts framework in `apache/struts` only (the core MVC framework, its
|
||||
interceptors, tags, and the plugins shipped in this repo).
|
||||
- **Date:** 2026-06-24. **Drafted for PMC review** via the threat-model-producer
|
||||
rubric (Scovetta). This is an unratified proposal, not an ASF Security team or
|
||||
PMC position; authorship and sponsorship are settled only once the PMC adopts it
|
||||
(see Status below and §14).
|
||||
- **Status:** DRAFT — not yet reviewed by the Struts PMC. Built as a strict
|
||||
superset of the existing [`SECURITY.md`](SECURITY.md) and the published
|
||||
[Struts security guidance](https://struts.apache.org/security/); every
|
||||
load-bearing claim is tagged for provenance (see §14 for open questions).
|
||||
- **Last revised:** 2026-08-15 — re-baselined against Security Bulletins
|
||||
**S2-070 … S2-074**, published 2026-08-14. This fired the §12 trigger twice over:
|
||||
those bulletins added default-hardening controls (7.3.0 / 6.11.0, §5a) and named two
|
||||
vulnerability classes the v0 draft did not carry as properties at all (§8.6, §8.7).
|
||||
Bulletin-to-section map in §16.
|
||||
- **Version binding:** versioned with the project; a report against version *N*
|
||||
is triaged against the model as it stood at *N*. The security envelope changed
|
||||
materially at **7.0** (several hardening knobs flipped to secure-by-default) and
|
||||
again at **7.3.0 / 6.11.0** (the first resource-bound controls — §5a), so the version
|
||||
is itself load-bearing.
|
||||
- **Reporting cross-reference:** §8-property violations → report privately per
|
||||
[`SECURITY.md`](SECURITY.md) (`security@struts.apache.org`); §3/§9/§11a findings
|
||||
are closed citing this document and the existing `SECURITY.md` "Before
|
||||
Reporting" checks.
|
||||
- **Provenance legend:** *(documented)* = Struts' own docs/`SECURITY.md`/security
|
||||
site; *(maintainer)* = confirmed by a Struts PMC member through this process;
|
||||
*(inferred)* = reasoned from architecture/docs, not yet PMC-ratified — each has
|
||||
a matching §14 open question.
|
||||
- **Draft confidence:** the bulk is *(documented)* — Struts has an unusually rich
|
||||
published security policy — with a handful of *(inferred)* scoping calls for the
|
||||
PMC to ratify.
|
||||
|
||||
**What Struts is.** Apache Struts 2 is a **Java MVC web framework** for building
|
||||
server-side web applications. A request flows: servlet filter → action mapping →
|
||||
**interceptor stack** (parameter population, validation, etc.) → **Action** →
|
||||
**result** (typically a JSP/FreeMarker view). Request parameters are bound onto
|
||||
action properties via setters, and view/configuration expressions are evaluated
|
||||
through **OGNL (Object-Graph Navigation Language)** against the **ValueStack**.
|
||||
*(documented — struts.apache.org)*
|
||||
|
||||
**The framework's own security philosophy (load-bearing).** Struts
|
||||
**"doesn't provide any security mechanism — it is just a pure web framework."**
|
||||
*(documented — [security guidance](https://struts.apache.org/security/))* It is
|
||||
not an authentication, authorization, session-security, or input-sanitisation
|
||||
layer; those are the embedding application's responsibility (§3/§10). What Struts
|
||||
*does* take an active stance on is **not letting its own machinery — chiefly OGNL
|
||||
expression evaluation and request-parameter binding — become an injection vector**.
|
||||
That single sentence shapes the whole model: most "Struts is insecure" reports are
|
||||
either OGNL-injection-class (in model, §8) or application-responsibility (out of
|
||||
model, §3/§11a).
|
||||
|
||||
Two corrections to that framing, drawn from the project's own published record.
|
||||
First, injection containment is not the only thing the framework owes its own
|
||||
machinery: it also owes **isolation between concurrently-served requests** and
|
||||
**bounds on what a single client can make it allocate or retain** (§8.6, §8.7).
|
||||
Second, OGNL remains the highest-*severity* class but is no longer the high-*volume*
|
||||
one — S2-070 through S2-074 are five consecutive bulletins with no OGNL among them
|
||||
(§16). A triager who expects every real finding to be OGNL-shaped will misroute the
|
||||
ones that are not.
|
||||
|
||||
## §2 Scope and intended use
|
||||
|
||||
Intended deployment: the Struts JARs are a **dependency embedded inside a web
|
||||
application** (a WAR) that the application developer writes, configures, and
|
||||
deploys into a servlet container (Tomcat, Jetty, …) behind the operator's
|
||||
perimeter. Struts is **in-process** with the application; it has no daemon, no
|
||||
listening socket of its own, and no trust boundary against the application code
|
||||
it runs inside. *(documented — it is a framework, not a server.)*
|
||||
|
||||
**Caller roles.**
|
||||
|
||||
- **Untrusted HTTP client** — sends requests (parameters, headers, cookies,
|
||||
multipart uploads) to a Struts-backed endpoint. **The primary untrusted boundary.**
|
||||
Struts must treat all request-derived values as hostile. *(documented — the
|
||||
parameter/OGNL hardening exists precisely for this actor.)*
|
||||
- **Application developer** — writes the actions, JSPs, struts.xml/annotations,
|
||||
and chooses the hardening settings (§5a). **Trusted by the framework** — their
|
||||
code and configuration run with the application's privileges. A finding that
|
||||
requires the developer to write unsafe code or disable a default protection is
|
||||
the application's bug, not Struts' (§3). *(documented — the developer-responsibility
|
||||
section of the security guidance.)*
|
||||
- **Operator** — deploys the WAR, sets `devMode` off, restricts dev-only plugins,
|
||||
configures the container and JVM. **Trusted.** *(documented.)*
|
||||
|
||||
**Component families.**
|
||||
|
||||
| Family | Entry point | Touches | In model? |
|
||||
| --- | --- | --- | --- |
|
||||
| OGNL evaluation + ValueStack | expression eval for params, tags, results | in-JVM code paths | **In — the central attack surface** *(documented)* |
|
||||
| Parameter binding (`ParametersInterceptor`, `@StrutsParameter`) | request params → action setters | reflection into app objects | **In — primary boundary** *(documented)* |
|
||||
| Interceptor stack (cookie, fileupload, fetch-metadata, COOP/COEP, …) | per-request processing | request data | **In** *(documented)* |
|
||||
| Tag library / JSP & FreeMarker integration | view rendering, expression output | template eval | **In — output-side OGNL/EL** *(documented)* |
|
||||
| File upload (Jakarta multipart) | multipart request parsing | temp files | **In — historical CVE surface** *(documented — S2 bulletins)* |
|
||||
| Request-body readers (JSON body population, CSP violation-report collection) | reads the raw request body | heap, parse state | **In** *(documented — S2-070, S2-072, S2-073)* |
|
||||
| Bundled plugins (REST, JSON, Convention, …) in this repo | extra mappers/result types | request data | **In — same request-trust surface** *(documented — S2-070/071/072 are JSON-plugin bulletins; §14 Q-plugins answered)* |
|
||||
| Config Browser Plugin | exposes internal config | dev-only diagnostic | **In as dev-only** — exposure in prod is operator misconfig (§3/§11a) *(documented)* |
|
||||
| Embedding application's own actions/JSPs/config | the developer's code | as the app | **Out — application responsibility (§3)** *(documented)* |
|
||||
| Examples / showcase / test apps | demo code | n/a | **Out** *(see §3)* |
|
||||
|
||||
**An optional feature is not a "non-default configuration."** Four of the five most
|
||||
recent bulletins concern surfaces that stay inert until an application turns them on:
|
||||
the JSON plugin's population of actions from a request body, its SMD / JSON-RPC branch
|
||||
(`enableSMD`), and a mapped endpoint collecting CSP violation reports. **None of that
|
||||
is `OUT-OF-MODEL: non-default-config`.** That disposition is for settings the project
|
||||
documents as *unsafe* (`devMode`, a disabled §5a hardening knob); a feature the project
|
||||
documents as *available* is in model at full request-trust level from the moment an
|
||||
application enables it, and the framework owes it the same §8 properties as the default
|
||||
path. Read literally, the v0 draft let a triager close all four — S2-070, S2-071,
|
||||
S2-072, S2-073 — as non-default configuration; this paragraph is what prevents that.
|
||||
*(documented — S2-070/071/072/073; §13, §14 Q-optional.)*
|
||||
|
||||
## §3 Out of scope (explicit non-goals)
|
||||
|
||||
The detailed lists of developer anti-patterns and insecure configurations are
|
||||
maintained in the project's own docs and are **not duplicated here** — this model
|
||||
links to them and assigns each a triage disposition (§13):
|
||||
|
||||
- **Anything the application developer is responsible for.** Struts provides no
|
||||
security mechanism of its own *(documented)*. The full enumeration —
|
||||
developer-exposed unsafe setters, request parameters used in localization or
|
||||
forced OGNL evaluation, raw `${...}` JSP-EL over untrusted values, direct JSP
|
||||
access, mixing security levels in one namespace — is in the
|
||||
[security guidance](https://struts.apache.org/security/) and
|
||||
[`SECURITY.md`](SECURITY.md). All are `OUT-OF-MODEL: application-responsibility`.
|
||||
- **Findings that only manifest with a documented-insecure / non-default setting**
|
||||
(`devMode=true`, Config Browser Plugin exposed in production, DMI enabled, or a
|
||||
§5a hardening knob turned off) → `OUT-OF-MODEL: non-default-config`. *(documented.)*
|
||||
- **The servlet container, JVM, JDK, and OS**, and the application's own
|
||||
authentication, authorization, session management, CSRF token storage, and
|
||||
transport (TLS). Struts is "a pure web framework," not a security framework.
|
||||
*(documented / inferred — §14 Q-env.)*
|
||||
- **Generic denial of service** — flooding, bandwidth exhaustion, or repeating a URL:
|
||||
cases where the cost to the server stays proportionate and no framework limit is
|
||||
missing or ineffective. The defence is the operator's, ahead of the application
|
||||
(container, reverse proxy, network). *(documented — [`SECURITY.md`](SECURITY.md).)*
|
||||
**This exclusion is narrower than the v0 draft claimed.** That draft read
|
||||
`SECURITY.md`'s "avoid resource consumption non-linear in the size of inputs" as a
|
||||
triage threshold and put everything linear out of model. The project's own bulletins
|
||||
say otherwise: S2-072, S2-073, and S2-074 are all linear and all carry CVEs. The test
|
||||
is **whether a bound exists, not what shape the curve is** (§8.7) — `SECURITY.md`
|
||||
now states this directly.
|
||||
- **Already-disclosed S2-series vulnerabilities** — a duplicate of an existing
|
||||
Security Bulletin/CVE is closed by reference (the
|
||||
[`SECURITY.md` "Before Reporting"](SECURITY.md) checks), not re-triaged.
|
||||
- **Examples, showcase, and test applications** shipped in the repo. *(inferred — §14 Q-scope.)*
|
||||
|
||||
## §4 Trust boundaries and data flow
|
||||
|
||||
```
|
||||
Untrusted HTTP request
|
||||
│ params, headers, cookies, multipart
|
||||
▼
|
||||
Servlet filter ─► action mapping ─► Interceptor stack ─► Action ─► Result (JSP/FreeMarker)
|
||||
│ │
|
||||
ParametersInterceptor tag/result OGNL eval
|
||||
binds params to setters against ValueStack
|
||||
│ │
|
||||
▼ ▼
|
||||
OGNL evaluation against the ValueStack ◄── the trust boundary
|
||||
(allowlist / excluded classes+packages /
|
||||
expression length / @StrutsParameter)
|
||||
```
|
||||
|
||||
- **HTTP client → framework** is the one boundary Struts owns. Every request-derived
|
||||
string (parameter *names* as well as *values*, cookie names/values, header values,
|
||||
multipart filenames) is untrusted and may carry an OGNL payload. The framework's
|
||||
job at this boundary is to bind parameters and evaluate expressions **without
|
||||
letting attacker input reach an OGNL evaluation that creates or changes executable
|
||||
code**. *(documented.)*
|
||||
- **Framework → application code** is *not* a trust boundary — Struts runs the
|
||||
developer's actions and templates in-process, fully trusted. *(documented.)*
|
||||
|
||||
**Reachability precondition (triager's test).** A finding is in-model only if it is
|
||||
reachable by an **untrusted HTTP client against a Struts application that follows the
|
||||
documented secure configuration** (current-version defaults, `devMode` off, dev-only
|
||||
plugins restricted, no developer anti-patterns from §3). A finding that needs
|
||||
`devMode`, a disabled default protection, a developer-introduced unsafe setter, or a
|
||||
documented anti-pattern is `OUT-OF-MODEL`. *(documented/inferred — §14 Q-default.)*
|
||||
|
||||
## §5 Assumptions about the environment
|
||||
|
||||
- A servlet container and a JVM the operator maintains; Struts does not patch or
|
||||
harden them. *(inferred — §14 Q-env.)*
|
||||
- The application is deployed with the **current supported version** (7.x or 6.x per
|
||||
`SECURITY.md`); 2.x is end-of-life and out of support. *(documented — Supported
|
||||
Versions table.)*
|
||||
- The operator runs production with `devMode=false` and dev-only diagnostics (Config
|
||||
Browser Plugin) disabled or access-controlled. *(documented.)*
|
||||
- Struts opens no sockets and makes no outbound connections of its own; any network
|
||||
egress is the application's. *(inferred — §14 Q-egress.)*
|
||||
|
||||
## §5a Build-time and configuration variants — **the central knob set**
|
||||
|
||||
Struts' security envelope is set almost entirely by **runtime configuration**. The
|
||||
**authoritative, current list of every hardening setting (purpose + secure default)
|
||||
lives in the [security guidance](https://struts.apache.org/security/) and is not
|
||||
reproduced here.** Only the triage-load-bearing facts:
|
||||
|
||||
- The security posture **changed materially at 7.0**, where a cluster of
|
||||
OGNL-injection and parameter-binding defences became **secure-by-default** —
|
||||
notably the OGNL allowlist (`struts.allowlist.enable`), the `@StrutsParameter`
|
||||
annotation requirement (`struts.parameters.requireAnnotations`), excluded
|
||||
classes/packages, the expression-length cap (`struts.ognl.expressionMaxLength`,
|
||||
default 256), and the static-field/proxy/default-package/custom-map disallows.
|
||||
- The envelope moved again at **7.3.0 / 6.11.0**, which added the first *resource-bound*
|
||||
controls — the substance of §8.7, and on by default. The localized-text caches became
|
||||
bounded (`struts.i18n.cacheMaxSize`) and the CSP violation-report read became bounded
|
||||
(`struts.csp.report.maxSize`, default 8192; a configured value outside 1..1048576 is
|
||||
ignored and the default kept). `struts.locale.validateRequestLocale` (default `false`)
|
||||
is the one opt-in of the set: it restricts request-derived locales to the runtime's
|
||||
available-locale set. *(documented — S2-073, S2-074.)*
|
||||
- `struts.devMode` (must be `false` in production) and Dynamic Method Invocation
|
||||
(gated by Strict Method Invocation since 2.5) are the two settings whose *insecure*
|
||||
value most often turns a non-finding into an apparent finding.
|
||||
- The **FetchMetadata / COOP / COEP** interceptors (6.0+) are opt-in cross-origin
|
||||
defences (§8.5).
|
||||
|
||||
**Insecure-default question (wave 1).** Because the secure posture is the **7.0
|
||||
default set**, the triage rule needs ratifying: is "a finding that only works with a
|
||||
pre-7.0 default, or with a 7.0 hardening knob turned off" `OUT-OF-MODEL:
|
||||
non-default-config`, with §10 carrying "deploy current version with defaults"? — §14
|
||||
Q-default. The OGNL **Java Security Manager sandbox** (`-Dognl.security.manager`) is a
|
||||
separate, opt-in defence built on the JDK `SecurityManager`, which has been
|
||||
**deprecated for removal since JDK 17 (JEP 411), disabled by default since JDK 18,
|
||||
and permanently disabled in JDK 24 (JEP 486)** *(documented — JDK release notes)* —
|
||||
so on modern JDKs the model cannot treat it as a relied-upon control (§14 Q-jsm).
|
||||
|
||||
## §6 Assumptions about inputs
|
||||
|
||||
| Surface | Input | Attacker-controllable? | Concern |
|
||||
| --- | --- | --- | --- |
|
||||
| Parameter binding | request parameter **names and values** | **yes** | OGNL injection via crafted names; binding to unsafe setters |
|
||||
| Cookies | cookie names/values (Cookie Interceptor) | **yes** | same OGNL/parameter concerns; checked by accepted/excluded patterns |
|
||||
| Headers | request headers | **yes** | header-driven expression/log paths |
|
||||
| Multipart upload | file content, filename, content-type | **yes** | parser robustness, temp-file handling (S2 history) |
|
||||
| Request body | raw body bytes (JSON body, CSP violation report) | **yes** | must be read under a bound; a limit the framework advertises must bound the read it names (§8.7) |
|
||||
| Expression context | values that reach an OGNL eval (tags, results, forced eval) | **yes if developer feeds untrusted input in** | the core RCE channel |
|
||||
| struts.xml / annotations / action code | framework + app configuration | **no — developer-trusted** | not an attacker surface (§3) |
|
||||
|
||||
The accepted/excluded pattern checkers (`AcceptedPatternsChecker` /
|
||||
`ExcludedPatternsChecker`, since 2.3.20) validate parameter names/values for the
|
||||
Parameters and Cookie interceptors; a custom override that drops below the framework
|
||||
defaults is a developer error, not a framework flaw. *(documented.)*
|
||||
|
||||
A request-derived value that becomes a **key in framework-managed state** is an input
|
||||
class in its own right, not merely a value in flight — a request-derived locale keys the
|
||||
framework's localized-text caches, for instance. Such state can be reached through
|
||||
ordinary request handling, not only through an application feature deliberately built on
|
||||
it. When triaging, ask not only "where does this value get evaluated?" but "what does the
|
||||
framework *retain*, keyed on it, and is that bounded?" *(documented — S2-074.)*
|
||||
|
||||
## §7 Adversary model
|
||||
|
||||
- **In scope:** an **untrusted remote HTTP client** with no credentials, able to send
|
||||
arbitrary parameters, headers, cookies, and multipart uploads to any
|
||||
Struts-handled endpoint. Capabilities: craft parameter names/values carrying OGNL,
|
||||
attempt to reach executable-code creation through the ValueStack, pollute
|
||||
parameter binding, exploit a file-upload or multipart parsing bug, or drive framework
|
||||
code into an unbounded allocation or an unbounded retention (§8.7). Goal: **remote
|
||||
code execution via OGNL** (the dominant Struts threat), and secondarily data
|
||||
disclosure — including disclosure *across* concurrent requests — SSRF through
|
||||
framework features, or denial of service. *(documented — the OGNL lineage is the
|
||||
framework's stated central concern.)*
|
||||
- **Two capabilities the v0 draft left implicit.** The client can issue requests
|
||||
**concurrently**, racing any state the framework shares between them (§8.6); and it
|
||||
can **repeat** requests to accumulate framework-side state, where no single request
|
||||
is remarkable and the harm is in the aggregate (§8.7). Single-request reasoning is
|
||||
not sufficient to clear a surface. *(documented — S2-070/071, S2-074.)*
|
||||
- **On-path network attacker** — only where the application/operator has not deployed
|
||||
TLS; transport security is the app's, so this is largely out of model (§3). *(inferred — §14 Q-env.)*
|
||||
- **Out of scope:** the application developer (writes trusted code/config); the
|
||||
operator (deploys, sets devMode/plugins); anyone with container/host/JVM control;
|
||||
and a developer who disables a default protection or follows a documented
|
||||
anti-pattern (§3). *(documented.)*
|
||||
|
||||
## §8 Security properties the framework provides
|
||||
|
||||
*(In the current-version, default-hardening posture; each lists violation symptom +
|
||||
severity. Most are documented controls — the OGNL-injection defences are the core of
|
||||
Struts' security work.)*
|
||||
|
||||
1. **OGNL injection containment.** Attacker-supplied request data (parameter names/
|
||||
values, cookies, headers) must not reach an OGNL evaluation that creates or alters
|
||||
executable code. Enforced in depth by the default controls listed in §5a / the
|
||||
[security guidance](https://struts.apache.org/security/) (allowlist, excluded
|
||||
classes/packages, expression-length cap, static-field/proxy/default-package/
|
||||
custom-map disallows, excluded node types). *Violation:* a crafted request
|
||||
achieving OGNL-driven code execution (or class-loader/member access beyond the
|
||||
allowlist) on a default-configured current-version app. *Severity:*
|
||||
security-critical (the S2-RCE class). *(documented.)*
|
||||
2. **Parameter-binding safety (7.0).** Request parameters bind only to setters the
|
||||
developer marked `@StrutsParameter` (to the declared depth); arbitrary deep/nested
|
||||
property traversal is not reachable by default. *Violation:* parameters reaching
|
||||
an unannotated setter, or nesting beyond the declared depth, on a default 7.0 app.
|
||||
*Severity:* critical. *(documented.)*
|
||||
3. **Method-invocation control.** Dynamic Method Invocation is gated by Strict Method
|
||||
Invocation; a client cannot invoke arbitrary action methods by name when DMI is at
|
||||
its recommended (off/strict) setting. *Violation:* arbitrary method invocation on a
|
||||
default app. *Severity:* high–critical. *(documented.)*
|
||||
4. **Expression-length and node-type bounds.** OGNL expressions over the configured
|
||||
length (default 256) and forbidden node types are rejected before evaluation.
|
||||
*Violation:* bypass of these bounds. *Severity:* high. *(documented.)*
|
||||
5. **Cross-origin / fetch-metadata defences (opt-in).** When the FetchMetadata, COOP,
|
||||
and COEP interceptors are enabled, the framework emits/enforces the corresponding
|
||||
`Sec-Fetch-*` and cross-origin isolation behaviour. *Violation:* the interceptor
|
||||
failing to enforce its documented behaviour when enabled. *Severity:* medium–high.
|
||||
*(documented — opt-in since 6.0.)*
|
||||
6. **Per-request state isolation.** A framework component holding per-request state —
|
||||
parse state, serialization state, buffers — is not shared between requests being
|
||||
served concurrently. *Violation:* data associated with one request becoming
|
||||
observable in another, or a limit that holds for a single request being defeated by
|
||||
racing two. *Severity:* high — it is a disclosure and integrity failure at once, and
|
||||
it can void another §8 control rather than merely leaking. *Note:* this property is
|
||||
violated by an ordinary concurrency bug, with no attacker sophistication required;
|
||||
the same defect harms honest concurrent users. *(documented — S2-070, S2-071.)*
|
||||
7. **Bounded consumption of request-derived input.** Framework code that reads a request
|
||||
body reads it under a limit, and framework-managed state keyed on request-derived
|
||||
values (caches, maps) is bounded. A limit the framework advertises bounds the
|
||||
operation it appears to govern. *Violation:* a single request making the framework
|
||||
allocate in proportion to its size with no ceiling; unbounded retention accumulated
|
||||
across requests; or a configured limit that does not constrain the read it names.
|
||||
*Severity:* moderate–high (denial of service). *Note:* **linear growth is a
|
||||
violation.** The test is whether a bound exists, not whether the curve bends — see
|
||||
§3, and `SECURITY.md`'s paragraph on the linear cases. *(documented — S2-072,
|
||||
S2-073, S2-074.)*
|
||||
|
||||
## §9 Security properties the framework does *not* provide
|
||||
|
||||
- **No security mechanism in the general sense.** Struts provides no authentication,
|
||||
authorization, session security, CSRF token store, input sanitisation, or output
|
||||
encoding *for the application's own data* — "it is just a pure web framework."
|
||||
*(documented.)*
|
||||
- *False friend:* "Struts has no built-in login/access control" is **by design**,
|
||||
not a vulnerability.
|
||||
- **No protection against developer anti-patterns or non-default config** — unsafe
|
||||
setters, raw `${}` on user input, request params in localization/forced eval,
|
||||
direct JSP access, `devMode` on, disabled hardening (§3/§5a).
|
||||
- **No defence once OGNL evaluation is fed untrusted input by the application
|
||||
itself** (forced expression evaluation on a request value) — that is the developer
|
||||
handing OGNL the attacker's string. *(documented.)*
|
||||
- **No hard anti-DoS guarantee.** The framework bounds what its own code reads and
|
||||
retains (§8.7) and designs against super-linear consumption, but it cannot protect an
|
||||
application from flooding, bandwidth exhaustion, or repeated requests whose cost to
|
||||
the server stays proportionate — that is the operator's to absorb, ahead of the
|
||||
application. The disclaimed property is *availability under generic load*, not
|
||||
*boundedness of framework code*, which §8.7 does provide. *(documented.)*
|
||||
- **The OGNL Java Security Manager sandbox is not a relied-upon control on modern
|
||||
JDKs** (the underlying `SecurityManager` is deprecated for removal since JDK 17 and
|
||||
permanently disabled in JDK 24; see §5a). *(documented.)*
|
||||
- **Auto-generated error pages do not escape action names** (historical S2-006) — the
|
||||
app must define custom error pages; XSS in the default error page is a documented
|
||||
hardening item, not a defended property. *(documented.)*
|
||||
- **Well-known classes (framework):** OGNL/expression injection, multipart/file-upload
|
||||
parsing bugs, and parameter-pollution are the highest-severity recurring classes — but
|
||||
the recent record adds two more that are currently the *more frequent* ones:
|
||||
**unbounded reads and unbounded framework-managed state** (S2-072/073/074) and **state
|
||||
shared across concurrent requests** (S2-070/071). Reflected XSS, CSRF token
|
||||
management, and transport security remain the application's. *(documented — §16.)*
|
||||
|
||||
## §10 Downstream (developer + operator) responsibilities
|
||||
|
||||
The full, authoritative how-to is the [security guidance](https://struts.apache.org/security/)
|
||||
and [`SECURITY.md`](SECURITY.md); in one line: **deploy a current supported version
|
||||
with the default hardening left on, `devMode` off, dev-only plugins restricted,
|
||||
parameter setters annotated, JSPs hidden behind actions, and the application's own
|
||||
authn/authz/CSRF/TLS supplied** (Struts provides none of those). The threat-model
|
||||
value is only that a finding requiring the developer to *violate* one of these is
|
||||
`OUT-OF-MODEL` (§3/§13), not that this list is novel.
|
||||
|
||||
Two more that the recent bulletins name explicitly, both as workarounds and as standing
|
||||
practice: the **operator** enforces a maximum request-body size ahead of the
|
||||
application, in the reverse proxy or the servlet container, for any endpoint that
|
||||
accepts a body; and the **developer** sets `struts.locale` to a fixed value wherever
|
||||
request-derived locales are not actually needed. Both are defence in depth — they are
|
||||
*not* substitutes for the §8.7 bounds, and a missing framework bound stays a framework
|
||||
finding whether or not the operator happened to configure around it.
|
||||
*(documented — S2-072, S2-073, S2-074.)*
|
||||
|
||||
## §11 Known misuse patterns
|
||||
|
||||
These are the §3 application-responsibility / non-default-config items viewed as
|
||||
"things integrators get wrong" — running `devMode=true` in production or exposing the
|
||||
Config Browser Plugin; disabling a default OGNL/binding protection "to make something
|
||||
work"; exposing unsafe setters to binding; feeding request parameters into forced
|
||||
OGNL evaluation or localization; allowing direct `*.jsp` access or raw `${}` EL on
|
||||
untrusted values; relying on the OGNL Java Security Manager sandbox on modern JDKs. Each
|
||||
is documented in the [security guidance](https://struts.apache.org/security/); the
|
||||
disposition mapping is §11a/§13.
|
||||
|
||||
## §11a Known non-findings (recurring false positives)
|
||||
|
||||
*(Seeded directly from `SECURITY.md` "Before Reporting" — the PMC owns the
|
||||
authoritative list; §14 Q12.)*
|
||||
|
||||
- **"OGNL/RCE that only works with `devMode=true`."** `OUT-OF-MODEL: non-default-config`
|
||||
— devMode is a development-only setting documented as unsafe for production.
|
||||
- **"An action setter lets me inject a value / reach a dangerous method."** When the
|
||||
setter is developer-exposed without `@StrutsParameter` (7.0), or performs an unsafe
|
||||
side effect, this is `OUT-OF-MODEL: application-responsibility`. In-model only if it
|
||||
bypasses the framework's *default* binding/OGNL protections.
|
||||
- **"Direct JSP access discloses X / executes Y."** App-deployment misconfiguration —
|
||||
JSPs must be hidden behind actions. `OUT-OF-MODEL: application-responsibility`.
|
||||
- **"Raw `${}` EL / forced OGNL eval on my request parameter is exploitable."** The
|
||||
application fed untrusted input to expression evaluation — documented anti-pattern,
|
||||
not a framework flaw.
|
||||
- **"Config Browser Plugin exposes internal configuration."** Dev-only diagnostic;
|
||||
exposing it in production is operator misconfiguration. `OUT-OF-MODEL: non-default-config`.
|
||||
- **"I can enumerate / pass arbitrary parameters."** Parameter binding is the point of
|
||||
the framework; in-model only when it crosses the default annotation/allowlist
|
||||
protections.
|
||||
- **"Generic DoS: I hammered a URL / saturated the pipe."** Not accepted — proportionate
|
||||
cost with no missing framework bound is the operator's to absorb (§3). **But "I sent a
|
||||
large body" does not belong to this bullet by default:** if framework code reads that
|
||||
body into memory with no ceiling, it is a §8.7 violation and `VALID`, as S2-072 and
|
||||
S2-073 both were. Establish that a bound exists before closing on this ground. *(This
|
||||
bullet previously read "I streamed a huge body," which would have closed S2-072.)*
|
||||
- **"A framework cache grew when I sent many distinct values."** In model only where the
|
||||
cache is genuinely *unbounded*, or keyed on an unvalidated and unbounded input set. A
|
||||
bounded cache reaching its ceiling and evicting is working as designed, and a cache
|
||||
keyed on a validated finite set — a locale restricted to the runtime's available
|
||||
locales, for instance — is not unbounded. *(documented — S2-074.)*
|
||||
- **Duplicate of a disclosed S2-series bulletin/CVE** — closed by reference.
|
||||
- **Dependency-tail CVEs** (a transitive jar, e.g. a logging or XML library) from an
|
||||
SCA scan — triage upstream unless Struts' own code reaches the vulnerable path with
|
||||
untrusted input.
|
||||
|
||||
## §12 Conditions that would change this model
|
||||
|
||||
- A change to the default-hardening set (e.g. a new secure-by-default knob, or a
|
||||
default flipped) — re-baseline §5a/§8/§11a.
|
||||
- A new request-facing surface, a new bundled plugin, or a new expression/templating
|
||||
integration with its own trust surface.
|
||||
- A change to how OGNL evaluation, the allowlist, or parameter binding works.
|
||||
- A report that cannot be routed to a §13 disposition → revise §8/§9.
|
||||
- **A published bulletin whose vulnerability class is not already a §8 property** — the
|
||||
clearest signal that the model under-describes what the framework actually guarantees,
|
||||
and the strongest one, because the PMC has already decided the question by issuing the
|
||||
CVE. S2-070 … S2-074 triggered exactly this re-baseline (§16); the check belongs in
|
||||
the release routine, not in the next report's triage.
|
||||
|
||||
## §13 Triage dispositions
|
||||
|
||||
| Disposition | Meaning | Licensed by |
|
||||
| --- | --- | --- |
|
||||
| `VALID` | A §8 property breaks via an untrusted HTTP client on a current-version, default-hardened app. | §8, §6, §7 |
|
||||
| `VALID-HARDENING` | A §11 misuse is too easy, or a default could be tightened. | §11/§5a |
|
||||
| `OUT-OF-MODEL: application-responsibility` | Requires a developer anti-pattern (unsafe setter, raw EL, forced eval, direct JSP) or the app's own authn/authz. | §3/§10 |
|
||||
| `OUT-OF-MODEL: non-default-config` | Only manifests with `devMode`, a dev-only plugin, DMI, or a disabled default protection — i.e. a setting the project documents as *unsafe*. **Not** a feature the project documents as optional and an application has enabled (§2). | §5a, §2 |
|
||||
| `OUT-OF-MODEL: adversary-not-in-scope` | Requires container/host/JVM/developer control. | §7 |
|
||||
| `OUT-OF-MODEL: unsupported-version` | Only affects an end-of-life (2.x) version. | §5 |
|
||||
| `BY-DESIGN: property-disclaimed` | Concerns a property §9 disclaims (no built-in authn/authz/encoding; generic DoS; JSM on JDK21+). | §9 |
|
||||
| `KNOWN-NON-FINDING` | Matches §11a. | §11a |
|
||||
| `DUPLICATE` | Matches a disclosed S2-series bulletin/CVE. | §3 |
|
||||
| `MODEL-GAP` | Unroutable. | triggers §12 |
|
||||
|
||||
## §14 Open questions for the maintainers
|
||||
|
||||
**Answered since v0 — by the project's own published record (S2-070 … S2-074)**
|
||||
|
||||
These two are carried here rather than deleted: the PMC settled them by issuing CVEs,
|
||||
so what remains is ratifying the *wording*, not the substance.
|
||||
|
||||
- **Q-dos — answered.** The line is **boundedness, not curve shape**. Framework code
|
||||
that reads or retains request-derived input with no ceiling is in model even where the
|
||||
growth is linear — S2-072, S2-073, and S2-074 all carry CVEs on linear growth — while
|
||||
generic load whose cost stays proportionate is not. Folded into §3, §8.7, §9, and
|
||||
§11a; the matching clarification is now in [`SECURITY.md`](SECURITY.md) so the model
|
||||
is no longer paraphrasing that document into a rule it does not state.
|
||||
- **Q-plugins — answered.** Bundled plugins are in scope at the same request-trust
|
||||
level: S2-070, S2-071, and S2-072 are all JSON-plugin bulletins with CVEs. The
|
||||
follow-on distinction this exposed — optional *feature* versus documented-unsafe
|
||||
*configuration* — is now in §2 and §13, and is put to the PMC as Q-optional below.
|
||||
|
||||
**Wave 1 — scope, defaults, intended use**
|
||||
|
||||
- **Q-default.** Confirm the triage baseline is "current supported version (7.x/6.x)
|
||||
with the documented default hardening on, `devMode` off, dev-only plugins
|
||||
restricted" — and that a finding requiring a pre-7.0 default or a disabled hardening
|
||||
knob is `OUT-OF-MODEL: non-default-config`. (§5a/§13.)
|
||||
- **Q-scope.** Confirm the in-scope surface is the framework in `apache/struts`
|
||||
(core + interceptors + tags + bundled plugins), with the embedding application's own
|
||||
actions/JSPs/config, and examples/showcase, out of scope. (§2/§3.)
|
||||
- **Q-philosophy.** Confirm the framing that Struts provides **no security mechanism
|
||||
of its own** beyond OGNL/parameter-binding injection containment — i.e. authn,
|
||||
authz, session security, CSRF token storage, output encoding, and transport are the
|
||||
application's. (§9.)
|
||||
- **Q-env.** Confirm the servlet container, JVM, JDK, and OS are out of scope — Struts
|
||||
does not patch or harden them, and the operator maintains them. (§3/§5.)
|
||||
- **Q-egress.** Confirm Struts opens no sockets and makes no outbound connections of
|
||||
its own, so any network egress (and the SSRF surface it implies) is the
|
||||
application's. (§5/§7.)
|
||||
|
||||
**Wave 2 — mechanism confirmations**
|
||||
|
||||
- **Q-ognl.** Confirm the §8.1 list is the authoritative set of default OGNL-injection
|
||||
defences (allowlist, excluded classes/packages/patterns, expression length,
|
||||
static-field/proxy/default-package/custom-map disallows, excluded node types) and
|
||||
that a bypass of any on a default app is `VALID`. (§8.)
|
||||
- **Q-jsm.** Confirm the OGNL Java Security Manager sandbox is **not** a relied-upon
|
||||
control (opt-in, and non-functional on modern JDKs — see §5a), so a report premised
|
||||
on its absence is not a finding. (§5a/§9.)
|
||||
- **Q-concurrency.** Confirm §8.6 as a framework property: per-request state must not be
|
||||
shared between concurrently-served requests, and a violation is `VALID` on its own —
|
||||
including where the only demonstrated consequence is that another framework limit can
|
||||
be raced, with no data shown to cross. (§8.6, from S2-070/071.)
|
||||
- **Q-bounds.** Confirm the §8.7 scope: does it cover *all* framework-managed state
|
||||
keyed on request-derived values, or only the caches and body reads bounded in
|
||||
7.3.0/6.11.0? A triager needs to know whether an unbounded structure found elsewhere
|
||||
in the framework is `VALID` or `VALID-HARDENING`. (§8.7.)
|
||||
|
||||
**Wave 3 — surfaces & false-friends**
|
||||
|
||||
- **Q-optional.** Confirm the §2 rule that a documented-optional feature (JSON body
|
||||
population, `enableSMD`, a mapped CSP-report endpoint, any bundled plugin) is in model
|
||||
once an application enables it, and that `OUT-OF-MODEL: non-default-config` is
|
||||
reserved for settings the project documents as *unsafe*. (§2/§13.)
|
||||
- **Q-plugin-risk.** With Q-plugins answered, the residual: are any bundled plugins
|
||||
(e.g. REST/XML) historically higher-risk enough to warrant their own §8 note? (§2.)
|
||||
- **Q-upload.** Confirm the multipart/file-upload surface (Jakarta) and what the
|
||||
framework guarantees vs. leaves to the container/app. (§2/§6.)
|
||||
- **Q12.** Beyond the `SECURITY.md` "Before Reporting" list already folded into §11a,
|
||||
what do scanners/researchers most often report against Struts that you consider a
|
||||
non-finding? (Feeds §11a.)
|
||||
|
||||
## §15 Appendix — existing-policy back-map
|
||||
|
||||
This `THREAT_MODEL.md` is **additive** — it does not replace
|
||||
[`SECURITY.md`](SECURITY.md) (reporting process, supported versions, "Before
|
||||
Reporting" checks) or the published [security guidance](https://struts.apache.org/security/);
|
||||
both are preserved and remain canonical for the reporting workflow. The discoverability
|
||||
chain is `AGENTS.md` → `SECURITY.md` → this model. Mapping of existing-policy claims to
|
||||
sections:
|
||||
|
||||
| Existing-policy statement | Threat-model § |
|
||||
| --- | --- |
|
||||
| "Struts doesn't provide any security mechanism — pure web framework" | §1, §9, §13 (`BY-DESIGN`) |
|
||||
| OGNL is the central historical vuln class | §1, §7, §8.1 |
|
||||
| devMode / Config Browser Plugin are dev-only | §3, §5a, §11a |
|
||||
| `@StrutsParameter` / unsafe setters | §6, §8.2, §10, §11a |
|
||||
| Direct JSP access / raw `${}` EL / forced eval / localization | §3, §10, §11a |
|
||||
| Allowlist / excluded classes/packages / expression length (7.0 defaults) | §5a, §8.1 |
|
||||
| DMI / Strict Method Invocation | §5a, §8.3 |
|
||||
| FetchMetadata / COOP / COEP | §5a, §8.5 |
|
||||
| OGNL JSM sandbox (modern-JDK limitation) | §5a, §9 |
|
||||
| Generic DoS not accepted; non-linear-in-input philosophy | §3, §9 |
|
||||
| Unbounded reads/state are in scope even when linear | §3, §8.7, §11a |
|
||||
| Resource bounds (i18n cache size, CSP report size, locale validation) | §5a, §8.7 |
|
||||
| "Before Reporting" duplicate/known-config checks | §3, §11a, §13 (`DUPLICATE`) |
|
||||
| Supported versions (2.x EOL) | §5, §13 (`OUT-OF-MODEL: unsupported-version`) |
|
||||
|
||||
## §16 Appendix — recent-bulletin back-map (S2-070 … S2-074)
|
||||
|
||||
The five bulletins published on **2026-08-14** are the evidence base for the 2026-08-15
|
||||
revision, and the reason §8 grew two properties. Each is a published
|
||||
[Security Bulletin](https://cwiki.apache.org/confluence/display/WW/Security+Bulletins)
|
||||
on the Struts cwiki, all five published the same day. Ratings are the bulletins' own.
|
||||
|
||||
| Bulletin | Rating | What it establishes for the model | § |
|
||||
| --- | --- | --- | --- |
|
||||
| **S2-070** (CVE-2026-73631) — shared parsing state, JSON plugin | Moderate | Per-request parse state shared across concurrent requests: cross-request disclosure and integrity loss, **and bypass of a configured limit** — one defect voiding another control | §8.6, §7, §2 |
|
||||
| **S2-071** (CVE-2026-73632) — shared serialization state, JSON plugin | Low | The same on the response side; the `json` *result type* is unaffected because a writer is built per request — the negative control for §8.6 | §8.6, §2 |
|
||||
| **S2-072** (CVE-2026-73633) — unbounded read of a JSON request body | Moderate | Linear DoS with a CVE, and an advertised limit (JSON input length) that did **not** bound the read it appeared to govern | §8.7, §3, §11a |
|
||||
| **S2-073** (CVE-2026-73634) — unbounded read of a CSP violation report | Moderate | Core rather than a plugin; inert until an application maps the endpoint; ordinarily unauthenticated *by design*, since browsers post to it directly | §8.7, §2, §10 |
|
||||
| **S2-074** (CVE-2026-73635) — unbounded localized-text cache growth | Moderate | Affects the **default configuration** rather than an opt-in feature; a request-derived value used as a **cache key**, making unbounded *retention* a class of its own alongside unbounded reads | §8.7, §6, §5a, §11a |
|
||||
|
||||
Three of the five would have been closed as non-findings by the v0 draft — S2-072 and
|
||||
S2-073 under §11a's "I streamed a huge body," S2-074 under §3's super-linear-only rule —
|
||||
and the remaining two had no §8 property to violate. That is the honest summary of what
|
||||
this revision fixes, and the reason §12 now carries a bulletin-driven re-baseline
|
||||
trigger.
|
||||
@@ -1,12 +0,0 @@
|
||||
# Struts 2 Apps
|
||||
These module consists of two example applications, which were built using the Apache Struts project.
|
||||
One is an old-fashioned Web application and another is a modern REST based single page app.
|
||||
|
||||
## Installation
|
||||
Enter a given folder, either `showcase/` or `rest-showcase/` and start the app using Maven:
|
||||
|
||||
```
|
||||
mvn jetty:run
|
||||
```
|
||||
|
||||
then open your browser at http://localhost:8080 and navigate to a proper context.
|
||||
@@ -0,0 +1,174 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
@@ -0,0 +1,6 @@
|
||||
Apache Struts
|
||||
|
||||
Copyright 2006 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at
|
||||
The Apache Software Foundation (http://www.apache.org/
|
||||
@@ -0,0 +1,10 @@
|
||||
README.txt - blank
|
||||
|
||||
This is an "empty" application that you can deploy as the basis of your own
|
||||
application.
|
||||
|
||||
For more on getting started with Struts, see
|
||||
|
||||
* http://cwiki.apache.org/WW/home.html
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-apps</artifactId>
|
||||
<version>2.0.8</version>
|
||||
</parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-blank</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>Blank Webapp</name>
|
||||
|
||||
<scm>
|
||||
<connection>scm:svn:http://svn.apache.org/repos/asf/struts/struts2/trunk/apps/blank/</connection>
|
||||
<developerConnection>scm:svn:https://svn.apache.org/repos/asf/struts/struts2/trunk/apps/blank/</developerConnection>
|
||||
<url>http://svn.apache.org/viewcvs.cgi/struts/struts2/trunk/apps/blank/</url>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>2.4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>jsp-api</artifactId>
|
||||
<version>2.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.mortbay.jetty</groupId>
|
||||
<artifactId>maven-jetty-plugin</artifactId>
|
||||
<configuration>
|
||||
<scanIntervalSeconds>10</scanIntervalSeconds>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.geronimo.specs</groupId>
|
||||
<artifactId>geronimo-j2ee_1.4_spec</artifactId>
|
||||
<version>1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 example;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
|
||||
/**
|
||||
* Base Action class for the Tutorial package.
|
||||
*/
|
||||
public class ExampleSupport extends ActionSupport {
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 example;
|
||||
|
||||
/**
|
||||
* <code>Set welcome message.</code>
|
||||
*/
|
||||
public class HelloWorld extends ExampleSupport {
|
||||
|
||||
public String execute() throws Exception {
|
||||
setMessage(getText(MESSAGE));
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide default valuie for Message property.
|
||||
*/
|
||||
public static final String MESSAGE = "HelloWorld.message";
|
||||
|
||||
/**
|
||||
* Field for Message property.
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* Return Message property.
|
||||
*
|
||||
* @return Message property
|
||||
*/
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Message property.
|
||||
*
|
||||
* @param message Text to display on HelloWorld page.
|
||||
*/
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 example;
|
||||
|
||||
public class Login extends ExampleSupport {
|
||||
|
||||
public String execute() throws Exception {
|
||||
|
||||
if (isInvalid(getUsername())) return INPUT;
|
||||
|
||||
if (isInvalid(getPassword())) return INPUT;
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
private boolean isInvalid(String value) {
|
||||
return (value == null || value.length() == 0);
|
||||
}
|
||||
|
||||
private String username;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
private String password;
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
set CLASSPATH=..\..\..\lib\xwork-2.0-beta-1.jar
|
||||
javac *.java -d ..\..\..\classes
|
||||
@@ -0,0 +1,5 @@
|
||||
Apache Struts
|
||||
Copyright 2000-2007 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE struts PUBLIC
|
||||
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
|
||||
"http://struts.apache.org/dtds/struts-2.0.dtd">
|
||||
|
||||
<struts>
|
||||
|
||||
<package name="example" namespace="/example" extends="struts-default">
|
||||
|
||||
<action name="HelloWorld" class="example.HelloWorld">
|
||||
<result>/example/HelloWorld.jsp</result>
|
||||
</action>
|
||||
|
||||
<action name="Login_*" method="{1}" class="example.Login">
|
||||
<result name="input">/example/Login.jsp</result>
|
||||
<result type="redirect-action">Menu</result>
|
||||
</action>
|
||||
|
||||
<action name="*" class="example.ExampleSupport">
|
||||
<result>/example/{1}.jsp</result>
|
||||
</action>
|
||||
|
||||
<!-- Add actions here -->
|
||||
</package>
|
||||
</struts>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE validators PUBLIC
|
||||
"-//OpenSymphony Group//XWork Validator 1.0.2//EN"
|
||||
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
|
||||
|
||||
<validators>
|
||||
<field name="username">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="requiredstring"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
<field name="password">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="requiredstring"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
</validators>
|
||||
@@ -0,0 +1,5 @@
|
||||
HelloWorld.message= Struts is up and running ...
|
||||
requiredstring = ${getText(fieldName)} is required.
|
||||
password = Password
|
||||
username = User Name
|
||||
Missing.message = This feature is under construction. Please try again in the next interation.
|
||||
@@ -0,0 +1,5 @@
|
||||
HelloWorld.message= ¡Struts está bien! ...
|
||||
requiredstring = ${getText(fieldName)} se requiere.
|
||||
password = Contraseña
|
||||
username = Nombre de Usuario
|
||||
Missing.message = ¡en obras! ¡seguir intentando!
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE struts PUBLIC
|
||||
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
|
||||
"http://struts.apache.org/dtds/struts-2.0.dtd">
|
||||
|
||||
<struts>
|
||||
|
||||
<constant name="struts.enable.DynamicMethodInvocation" value="false" />
|
||||
<constant name="struts.devMode" value="false" />
|
||||
|
||||
<include file="example.xml"/>
|
||||
|
||||
<!-- Add packages here -->
|
||||
|
||||
</struts>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app id="WebApp_9" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
|
||||
|
||||
<display-name>Struts Blank</display-name>
|
||||
|
||||
<filter>
|
||||
<filter-name>struts2</filter-name>
|
||||
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>struts2</filter-name>
|
||||
<url-pattern>/*</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<welcome-file-list>
|
||||
<welcome-file>index.html</welcome-file>
|
||||
</welcome-file-list>
|
||||
|
||||
</web-app>
|
||||
@@ -0,0 +1,28 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<html>
|
||||
<head>
|
||||
<title><s:text name="HelloWorld.message"/></title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2><s:property value="message"/></h2>
|
||||
|
||||
<h3>Languages</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<s:url id="url" action="HelloWorld">
|
||||
<s:param name="request_locale">en</s:param>
|
||||
</s:url>
|
||||
<s:a href="%{url}">English</s:a>
|
||||
</li>
|
||||
<li>
|
||||
<s:url id="url" action="HelloWorld">
|
||||
<s:param name="request_locale">es</s:param>
|
||||
</s:url>
|
||||
<s:a href="%{url}">Espanol</s:a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,15 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<html>
|
||||
<head>
|
||||
<title>Sign On</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<s:form action="Login">
|
||||
<s:textfield key="username"/>
|
||||
<s:password key="password" />
|
||||
<s:submit/>
|
||||
</s:form>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<s:include value="Missing.jsp"/>
|
||||
@@ -0,0 +1,11 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<html>
|
||||
<head><title>Missing Feature</title></head>
|
||||
|
||||
<body>
|
||||
<p>
|
||||
<s:text name="Missing.message"/>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<s:include value="Missing.jsp"/>
|
||||
@@ -0,0 +1,18 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib prefix="s" uri="/struts-tags" %>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome</title>
|
||||
<link href="<s:url value="/css/examplecss"/>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>Commands</h3>
|
||||
<ul>
|
||||
<li><a href="<s:url action="Login_input"/>">Sign On</a></li>
|
||||
<li><a href="<s:url action="Register"/>">Register</a></li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<META HTTP-EQUIV="Refresh" CONTENT="0;URL=example/HelloWorld.action">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Loading ...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 example;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
import com.opensymphony.xwork2.config.RuntimeConfiguration;
|
||||
import com.opensymphony.xwork2.config.entities.ActionConfig;
|
||||
import com.opensymphony.xwork2.config.entities.ResultConfig;
|
||||
import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.struts2.StrutsTestCase;
|
||||
|
||||
public class ConfigTest extends StrutsTestCase {
|
||||
|
||||
protected void assertSuccess(String result) throws Exception {
|
||||
assertTrue("Expected a success result!",
|
||||
ActionSupport.SUCCESS.equals(result));
|
||||
}
|
||||
|
||||
protected void assertInput(String result) throws Exception {
|
||||
assertTrue("Expected an input result!",
|
||||
ActionSupport.INPUT.equals(result));
|
||||
}
|
||||
|
||||
protected Map assertFieldErrors(ActionSupport action) throws Exception {
|
||||
assertTrue(action.hasFieldErrors());
|
||||
return action.getFieldErrors();
|
||||
}
|
||||
|
||||
protected void assertFieldError(Map field_errors, String field_name, String error_message) {
|
||||
|
||||
List errors = (List) field_errors.get(field_name);
|
||||
assertNotNull("Expected errors for " + field_name, errors);
|
||||
assertTrue("Expected errors for " + field_name, errors.size()>0);
|
||||
// TODO: Should be a loop
|
||||
assertEquals(error_message,errors.get(0));
|
||||
|
||||
}
|
||||
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
XmlConfigurationProvider c = new XmlConfigurationProvider("struts.xml");
|
||||
configurationManager.addConfigurationProvider(c);
|
||||
configurationManager.reload();
|
||||
}
|
||||
|
||||
protected ActionConfig assertClass(String namespace, String action_name, String class_name) {
|
||||
RuntimeConfiguration configuration = configurationManager.getConfiguration().getRuntimeConfiguration();
|
||||
ActionConfig config = configuration.getActionConfig(namespace, action_name);
|
||||
assertNotNull("Mssing action", config);
|
||||
assertTrue("Wrong class name: [" + config.getClassName() + "]",
|
||||
class_name.equals(config.getClassName()));
|
||||
return config;
|
||||
}
|
||||
|
||||
protected ActionConfig assertClass(String action_name, String class_name) {
|
||||
return assertClass("", action_name, class_name);
|
||||
}
|
||||
|
||||
protected void assertResult(ActionConfig config, String result_name, String result_value) {
|
||||
Map results = config.getResults();
|
||||
ResultConfig result = (ResultConfig) results.get(result_name);
|
||||
Map params = result.getParams();
|
||||
String value = (String) params.get("actionName");
|
||||
if (value == null)
|
||||
value = (String) params.get("location");
|
||||
assertTrue("Wrong result value: [" + value + "]",
|
||||
result_value.equals(value));
|
||||
}
|
||||
|
||||
public void testConfig() throws Exception {
|
||||
assertNotNull(configurationManager);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 example;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class HelloWorldTest extends TestCase {
|
||||
|
||||
public void testHelloWorld() throws Exception {
|
||||
HelloWorld hello_world = new HelloWorld();
|
||||
String result = hello_world.execute();
|
||||
assertTrue("Expected a success result!",
|
||||
ActionSupport.SUCCESS.equals(result));
|
||||
assertTrue("Expected the default message!",
|
||||
hello_world.getText(HelloWorld.MESSAGE).equals(hello_world.getMessage()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 example;
|
||||
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
import com.opensymphony.xwork2.config.entities.ActionConfig;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class LoginTest extends ConfigTest {
|
||||
|
||||
public void FIXME_testLoginConfig() throws Exception {
|
||||
ActionConfig config = assertClass("example", "Login_input", "example.Login");
|
||||
assertResult(config, ActionSupport.SUCCESS, "Menu");
|
||||
assertResult(config, ActionSupport.INPUT, "/example/Login.jsp");
|
||||
}
|
||||
|
||||
public void testLoginSubmit() throws Exception {
|
||||
Login login = new Login();
|
||||
login.setUsername("username");
|
||||
login.setPassword("password");
|
||||
String result = login.execute();
|
||||
assertSuccess(result);
|
||||
}
|
||||
|
||||
// Needs access to an envinronment that includes validators
|
||||
public void FIXME_testLoginSubmitInput() throws Exception {
|
||||
Login login = new Login();
|
||||
String result = login.execute();
|
||||
assertInput(result);
|
||||
Map errors = assertFieldErrors(login);
|
||||
assertFieldError(errors,"username","Username is required.");
|
||||
assertFieldError(errors,"password","Password is required.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
README.txt - mailreader
|
||||
|
||||
The MailReader demonstrates a localized application with a master/child
|
||||
CRUD workflow.
|
||||
|
||||
This rendition also demonstrates using wildcards to "normalize" an
|
||||
application.
|
||||
|
||||
See the Sandbox for other MailReader examples using other architectures.
|
||||
|
||||
* http://svn.apache.org/viewvc/struts/sandbox/trunk/struts2/apps/
|
||||
|
||||
For more about the MailReader applicaton genneraly, visit Struts University.
|
||||
|
||||
* http://www.StrutsUniversity.org/
|
||||
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-apps</artifactId>
|
||||
<version>2.0.8</version>
|
||||
</parent>
|
||||
<groupId>org.apache.struts</groupId>
|
||||
<artifactId>struts2-mailreader</artifactId>
|
||||
<packaging>war</packaging>
|
||||
<name>Starter Webapp</name>
|
||||
|
||||
<scm>
|
||||
<connection>scm:svn:http://svn.apache.org/repos/asf/struts/struts2/trunk/apps/mailreader/</connection>
|
||||
<developerConnection>scm:svn:https://svn.apache.org/repos/asf/struts/struts2/trunk/apps/mailreader/</developerConnection>
|
||||
<url>http://svn.apache.org/viewcvs.cgi/struts/struts2/trunk/apps/mailreader/</url>
|
||||
</scm>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>servlet-api</artifactId>
|
||||
<version>2.4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>${pom.groupId}</groupId>
|
||||
<artifactId>struts-mailreader-dao</artifactId>
|
||||
<version>1.3.5</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/java</directory>
|
||||
<includes>
|
||||
<include>**/*.xml</include>
|
||||
<include>**/*.properties</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.mortbay.jetty</groupId>
|
||||
<artifactId>maven-jetty-plugin</artifactId>
|
||||
<version>6.0.1</version>
|
||||
<configuration>
|
||||
<scanIntervalSeconds>10</scanIntervalSeconds>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,3 @@
|
||||
password=Enter your Password here ==>
|
||||
struts.logo.path=struts-power.gif
|
||||
struts.logo.alt=Powered by Struts
|
||||
@@ -0,0 +1 @@
|
||||
.password=\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b==>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE struts PUBLIC
|
||||
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
|
||||
"http://struts.apache.org/dtds/struts-2.0.dtd">
|
||||
|
||||
<struts>
|
||||
|
||||
<package name="mailreader-default" namespace="/" extends="struts-default">
|
||||
|
||||
<interceptors>
|
||||
|
||||
<interceptor name="authentication"
|
||||
class="mailreader2.AuthenticationInterceptor"/>
|
||||
|
||||
<interceptor-stack name="user" >
|
||||
<interceptor-ref name="authentication" />
|
||||
<interceptor-ref name="defaultStack"/>
|
||||
</interceptor-stack>
|
||||
|
||||
<interceptor-stack name="user-submit" >
|
||||
<interceptor-ref name="token-session" />
|
||||
<interceptor-ref name="user"/>
|
||||
</interceptor-stack>
|
||||
|
||||
<interceptor-stack name="guest" >
|
||||
<interceptor-ref name="defaultStack"/>
|
||||
</interceptor-stack>
|
||||
|
||||
</interceptors>
|
||||
|
||||
<default-interceptor-ref name="user"/>
|
||||
|
||||
<global-results>
|
||||
<result name="error">/pages/Error.jsp</result>
|
||||
<result name="invalid.token">/pages/Error.jsp</result>
|
||||
<result name="login" type="redirect-action">Login_input</result>
|
||||
</global-results>
|
||||
|
||||
<global-exception-mappings>
|
||||
<exception-mapping
|
||||
result="error"
|
||||
exception="java.lang.Throwable"/>
|
||||
</global-exception-mappings>
|
||||
|
||||
</package>
|
||||
|
||||
</struts>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE struts PUBLIC
|
||||
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
|
||||
"http://struts.apache.org/dtds/struts-2.0.dtd">
|
||||
|
||||
<struts>
|
||||
<package name="mailreader-support" namespace="/" extends="mailreader-default">
|
||||
|
||||
<action name="Tour">
|
||||
<result>/tour.html</result>
|
||||
<interceptor-ref name="guest"/>
|
||||
</action>
|
||||
|
||||
<action name="Welcome" class="mailreader2.Welcome">
|
||||
<result>/Welcome.jsp</result>
|
||||
<interceptor-ref name="guest"/>
|
||||
</action>
|
||||
|
||||
<action name="Logout" class="mailreader2.Logout">
|
||||
<result type="redirect-action">Welcome</result>
|
||||
</action>
|
||||
|
||||
<action name="Login_*" method="{1}" class="mailreader2.Login">
|
||||
<result name="input">/Login.jsp</result>
|
||||
<result name="cancel" type="redirect-action">Welcome</result>
|
||||
<result type="redirect-action">MainMenu</result>
|
||||
<result name="expired" type="chain">ChangePassword</result>
|
||||
<exception-mapping
|
||||
exception="org.apache.struts.apps.mailreader.dao.ExpiredPasswordException"
|
||||
result="expired"/>
|
||||
<interceptor-ref name="guest"/>
|
||||
</action>
|
||||
|
||||
<action name="Registration_*" method="{1}" class="mailreader2.Registration">
|
||||
<result name="input">/Registration.jsp</result>
|
||||
<result type="redirect-action">MainMenu</result>
|
||||
<interceptor-ref name="guest"/>
|
||||
</action>
|
||||
</package>
|
||||
|
||||
<package name="subscription" namespace="/" extends="mailreader-support">
|
||||
|
||||
<global-results>
|
||||
<result name="input">/Subscription.jsp</result>
|
||||
<result type="redirect-action">Registration_input</result>
|
||||
</global-results>
|
||||
|
||||
<action name="Subscription_save" method="save" class="mailreader2.Subscription">
|
||||
<interceptor-ref name="user-submit" />
|
||||
</action>
|
||||
|
||||
<action name="Subscription_*" method="{1}" class="mailreader2.Subscription" />
|
||||
|
||||
</package>
|
||||
|
||||
<package name="wildcard" namespace="/" extends="mailreader-support">
|
||||
|
||||
<action name="*" class="mailreader2.MailreaderSupport">
|
||||
<result>/{1}.jsp</result>
|
||||
</action>
|
||||
|
||||
</package>
|
||||
</struts>
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.struts.apps.mailreader.dao.impl.memory.MemoryUserDatabase;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import javax.servlet.ServletContextListener;
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* <p><code>ServletContextListener</code> that initializes and finalizes the
|
||||
* persistent storage of User and Subscription information for the Struts
|
||||
* Demonstration Application, using an in-memory database backed by an XML
|
||||
* file.</p>
|
||||
* <p/>
|
||||
* <p><strong>IMPLEMENTATION WARNING</strong> - If this web application is run
|
||||
* from a WAR file, or in another environment where reading and writing of the
|
||||
* web application resource is impossible, the initial contents will be copied
|
||||
* to a file in the web application temporary directory provided by the
|
||||
* container. This is for demonstration purposes only - you should
|
||||
* <strong>NOT</strong> assume that files written here will survive a restart
|
||||
* of your servlet container.</p>
|
||||
* <p/>
|
||||
* <p>This class was borrowed from the Shale Mailreader. Changes were:</p>
|
||||
* <p/>
|
||||
* <ul>
|
||||
* <p/>
|
||||
* <li>Path to database.xml (under classes here). </li>
|
||||
* <p/>
|
||||
* <li>Class to store protocol list (an array here). </li>
|
||||
* <p/>
|
||||
* </ul>
|
||||
* <p>
|
||||
* DEVELOPMENT NOTE - Another approach would be to instantiate the database via Spring.
|
||||
* </p>
|
||||
*/
|
||||
|
||||
public final class ApplicationListener implements ServletContextListener {
|
||||
|
||||
// ------------------------------------------------------ Manifest Constants
|
||||
|
||||
|
||||
/**
|
||||
* <p>Appication scope attribute key under which the in-memory version of
|
||||
* our database is stored.</p>
|
||||
*/
|
||||
public static final String DATABASE_KEY = "database";
|
||||
|
||||
|
||||
/**
|
||||
* <p>Application scope attribute key under which the valid selection
|
||||
* items for the protocol property is stored.</p>
|
||||
*/
|
||||
public static final String PROTOCOLS_KEY = "protocols";
|
||||
|
||||
// ------------------------------------------------------ Instance Variables
|
||||
|
||||
|
||||
/**
|
||||
* <p>The <code>ServletContext</code> for this web application.</p>
|
||||
*/
|
||||
private ServletContext context = null;
|
||||
|
||||
|
||||
/**
|
||||
* The {@link MemoryUserDatabase} object we construct and make available.
|
||||
*/
|
||||
private MemoryUserDatabase database = null;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Logging output for this plug in instance.</p>
|
||||
*/
|
||||
private Log log = LogFactory.getLog(this.getClass());
|
||||
|
||||
// ------------------------------------------------------------- Properties
|
||||
|
||||
|
||||
/**
|
||||
* <p>The web application resource path of our persistent database storage
|
||||
* file.</p>
|
||||
*/
|
||||
private String pathname = "/WEB-INF/database.xml";
|
||||
|
||||
/**
|
||||
* <p>Return the application resource path to the database.</p>
|
||||
*
|
||||
* @return application resource path path to the database
|
||||
*/
|
||||
public String getPathname() {
|
||||
return (this.pathname);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Set the application resource path to the database.</p>
|
||||
*
|
||||
* @param pathname to the database
|
||||
*/
|
||||
public void setPathname(String pathname) {
|
||||
this.pathname = pathname;
|
||||
}
|
||||
|
||||
// ------------------------------------------ ServletContextListener Methods
|
||||
|
||||
|
||||
/**
|
||||
* <p>Gracefully shut down this database, releasing any resources that
|
||||
* were allocated at initialization.</p>
|
||||
*
|
||||
* @param event ServletContextEvent to process
|
||||
*/
|
||||
public void contextDestroyed(ServletContextEvent event) {
|
||||
|
||||
log.info("Finalizing memory database plug in");
|
||||
|
||||
if (database != null) {
|
||||
try {
|
||||
database.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Closing memory database", e);
|
||||
}
|
||||
}
|
||||
|
||||
context.removeAttribute(DATABASE_KEY);
|
||||
context.removeAttribute(PROTOCOLS_KEY);
|
||||
database = null;
|
||||
context = null;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>Initialize and load our initial database from persistent
|
||||
* storage.</p>
|
||||
*
|
||||
* @param event The context initialization event
|
||||
*/
|
||||
public void contextInitialized(ServletContextEvent event) {
|
||||
|
||||
log.info("Initializing memory database plug in from '" +
|
||||
pathname + "'");
|
||||
|
||||
// Remember our associated ServletContext
|
||||
this.context = event.getServletContext();
|
||||
|
||||
// Construct a new database and make it available
|
||||
database = new MemoryUserDatabase();
|
||||
try {
|
||||
String path = calculatePath();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug(" Loading database from '" + path + "'");
|
||||
}
|
||||
database.setPathname(path);
|
||||
database.open();
|
||||
} catch (Exception e) {
|
||||
log.error("Opening memory database", e);
|
||||
throw new IllegalStateException("Cannot load database from '" +
|
||||
pathname + "': " + e);
|
||||
}
|
||||
context.setAttribute(DATABASE_KEY, database);
|
||||
|
||||
}
|
||||
|
||||
// -------------------------------------------------------- Private Methods
|
||||
|
||||
|
||||
/**
|
||||
* <p>Calculate and return an absolute pathname to the XML file to contain
|
||||
* our persistent storage information.</p>
|
||||
*
|
||||
* @throws Exception if an input/output error occurs
|
||||
*/
|
||||
private String calculatePath() throws Exception {
|
||||
|
||||
// Can we access the database via file I/O?
|
||||
String path = context.getRealPath(pathname);
|
||||
if (path != null) {
|
||||
return (path);
|
||||
}
|
||||
|
||||
// Does a copy of this file already exist in our temporary directory
|
||||
File dir = (File)
|
||||
context.getAttribute("javax.servlet.context.tempdir");
|
||||
File file = new File(dir, "struts-example-database.xml");
|
||||
if (file.exists()) {
|
||||
return (file.getAbsolutePath());
|
||||
}
|
||||
|
||||
// Copy the static resource to a temporary file and return its path
|
||||
InputStream is =
|
||||
context.getResourceAsStream(pathname);
|
||||
BufferedInputStream bis = new BufferedInputStream(is, 1024);
|
||||
FileOutputStream os =
|
||||
new FileOutputStream(file);
|
||||
BufferedOutputStream bos = new BufferedOutputStream(os, 1024);
|
||||
byte buffer[] = new byte[1024];
|
||||
while (true) {
|
||||
int n = bis.read(buffer);
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
bos.write(buffer, 0, n);
|
||||
}
|
||||
bos.close();
|
||||
bis.close();
|
||||
return (file.getAbsolutePath());
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package mailreader2;
|
||||
|
||||
import com.opensymphony.xwork2.interceptor.Interceptor;
|
||||
import com.opensymphony.xwork2.ActionInvocation;
|
||||
import com.opensymphony.xwork2.Action;
|
||||
import java.util.Map;
|
||||
import org.apache.struts.apps.mailreader.dao.User;
|
||||
|
||||
public class AuthenticationInterceptor implements Interceptor {
|
||||
|
||||
public void destroy () {}
|
||||
|
||||
public void init() {}
|
||||
|
||||
public String intercept(ActionInvocation actionInvocation) throws Exception {
|
||||
|
||||
Map session = actionInvocation.getInvocationContext().getSession();
|
||||
|
||||
User user = (User) session.get(Constants.USER_KEY);
|
||||
|
||||
boolean isAuthenticated = (null!=user) && (null!=user.getDatabase());
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return Action.LOGIN;
|
||||
}
|
||||
else {
|
||||
return actionInvocation.invoke();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
/**
|
||||
* <p> Manifest constants for the MailReader application. </p>
|
||||
*/
|
||||
public final class Constants {
|
||||
|
||||
// --- Tokens ----
|
||||
|
||||
/**
|
||||
* <p> The token representing a "cancel" request. </p>
|
||||
*/
|
||||
public static final String CANCEL = "cancel";
|
||||
|
||||
/**
|
||||
* <p> The token representing a "create" task. </p>
|
||||
*/
|
||||
public static final String CREATE = "Create";
|
||||
|
||||
/**
|
||||
* <p> The application scope attribute under which our user database is
|
||||
* stored. </p>
|
||||
*/
|
||||
public static final String DATABASE_KEY = "database";
|
||||
|
||||
/**
|
||||
* <p> The token representing a "edit" task. </p>
|
||||
*/
|
||||
public static final String DELETE = "Delete";
|
||||
|
||||
/**
|
||||
* <p> The token representing a "edit" task. </p>
|
||||
*/
|
||||
public static final String EDIT = "Edit";
|
||||
|
||||
/**
|
||||
* <p> The package name for this application. </p>
|
||||
*/
|
||||
public static final String PACKAGE = "org.apache.struts.apps.mailreader";
|
||||
|
||||
/**
|
||||
* <p> The session scope attribute under which the Subscription object
|
||||
* currently selected by our logged-in User is stored. </p>
|
||||
*/
|
||||
public static final String SUBSCRIPTION_KEY = "subscription";
|
||||
|
||||
/**
|
||||
* <p> The session scope attribute under which the User object for the
|
||||
* currently logged in user is stored. </p>
|
||||
*/
|
||||
public static final String USER_KEY = "user";
|
||||
|
||||
/**
|
||||
* <p>The token representing the "Host" property.
|
||||
*/
|
||||
public static final String HOST = "host";
|
||||
|
||||
|
||||
// ---- Error Messages ----
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A static message in case message resource is not loaded.
|
||||
* </p>
|
||||
*/
|
||||
public static final String ERROR_MESSAGES_NOT_LOADED =
|
||||
"ERROR: Message resources not loaded -- check servlet container logs for error messages.";
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A static message in case database resource is not loaded.
|
||||
* <p>
|
||||
*/
|
||||
public static final String ERROR_DATABASE_NOT_LOADED =
|
||||
"ERROR: User database not loaded -- check servlet container logs for error messages.";
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* A standard key from the message resources file, to test if it is available.
|
||||
* <p>
|
||||
*/
|
||||
public static final String ERROR_DATABASE_MISSING = "error.database.missing";
|
||||
|
||||
/**
|
||||
* <P>
|
||||
* A "magic" username to trigger an ExpiredPasswordException for testing.
|
||||
*</p>
|
||||
*/
|
||||
public static final String EXPIRED_PASSWORD_EXCEPTION = "ExpiredPasswordException";
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Name of field to associate with authentification errors.
|
||||
* <p>
|
||||
*/
|
||||
public static final String PASSWORD_MISMATCH_FIELD = "password";
|
||||
|
||||
// ---- Log Messages ----
|
||||
|
||||
/**
|
||||
* <p> Message to log if saving a user fails. </p>
|
||||
*/
|
||||
public static final String LOG_DATABASE_SAVE_ERROR =
|
||||
" Unexpected error when saving User: ";
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
|
||||
|
||||
<validators>
|
||||
<field name="username">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.username.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
<field name="password">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.password.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
</validators>
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
import org.apache.struts.apps.mailreader.dao.User;
|
||||
import org.apache.struts.apps.mailreader.dao.ExpiredPasswordException;
|
||||
|
||||
/**
|
||||
* <p> Validate a user login. </p>
|
||||
*/
|
||||
public final class Login extends MailreaderSupport {
|
||||
|
||||
public String execute() throws ExpiredPasswordException {
|
||||
|
||||
User user = findUser(getUsername(), getPassword());
|
||||
|
||||
if (user != null) {
|
||||
setUser(user);
|
||||
}
|
||||
|
||||
if (hasErrors()) {
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
return SUCCESS;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
/**
|
||||
* <p> Log user out of the current session. </p>
|
||||
*/
|
||||
public class Logout extends MailreaderSupport {
|
||||
|
||||
public String execute() {
|
||||
|
||||
setUser(null);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
import org.apache.struts2.interceptor.ApplicationAware;
|
||||
import org.apache.struts2.interceptor.SessionAware;
|
||||
import com.opensymphony.xwork2.ActionSupport;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.struts.apps.mailreader.dao.ExpiredPasswordException;
|
||||
import org.apache.struts.apps.mailreader.dao.Subscription;
|
||||
import org.apache.struts.apps.mailreader.dao.User;
|
||||
import org.apache.struts.apps.mailreader.dao.UserDatabase;
|
||||
import org.apache.struts.apps.mailreader.dao.impl.memory.MemorySubscription;
|
||||
import org.apache.struts.apps.mailreader.dao.impl.memory.MemoryUser;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p> Base Action for MailreaderSupport application. </p>
|
||||
* <p/>
|
||||
* <p> Note that this class does NOT implement model driven because of the way
|
||||
* the pre-existing model is designed. The MailReader DAO includes immutable
|
||||
* fields that can only be set on construction, and some objects do not have a
|
||||
* default construction. One approach would be to mirror all the DAO
|
||||
* properties on the Actions. As an alternative, this implementations uses the
|
||||
* DAO properties where possible, and uses local Action properties only as
|
||||
* needed. To create new objects, a blank temporary object is constructed, and
|
||||
* the page uses a mix of local Action properties and DAO properties. When the
|
||||
* new object is to be saved, the local Action properties are used to create
|
||||
* the object using the DAO factory methods, the input values are copied from
|
||||
* the temporary object, and the new object is saved. It's kludge, but it
|
||||
* avoids creating unnecessary local properties. Pick your poison.</p>
|
||||
*/
|
||||
public class MailreaderSupport extends ActionSupport
|
||||
implements SessionAware, ApplicationAware {
|
||||
|
||||
/**
|
||||
* Return CANCEL so apropriate result can be selected.
|
||||
* @return "cancel" so apropriate result can be selected.
|
||||
*/
|
||||
public String cancel() {
|
||||
return Constants.CANCEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to copy User properties.
|
||||
**/
|
||||
protected void copyUser(User source, User target) {
|
||||
if ((source==null) || (target==null)) return;
|
||||
target.setFromAddress(source.getFromAddress());
|
||||
target.setFullName(source.getFullName());
|
||||
target.setPassword(source.getPassword());
|
||||
target.setReplyToAddress(source.getReplyToAddress());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to copy Subscription properties.
|
||||
**/
|
||||
protected void copySubscription(Subscription source, Subscription target) {
|
||||
if ((source==null) || (target==null)) return;
|
||||
target.setAutoConnect(source.getAutoConnect());
|
||||
target.setPassword(source.getPassword());
|
||||
target.setType(source.getType());
|
||||
target.setUsername(source.getUsername());
|
||||
}
|
||||
|
||||
|
||||
// ---- ApplicationAware ----
|
||||
|
||||
/**
|
||||
* <p>Field to store application context or its proxy.</p>
|
||||
* <p/>
|
||||
* <p>The application context lasts for the life of the application. A
|
||||
* reference to the database is stored in the application context at
|
||||
* startup.</p>
|
||||
*/
|
||||
private Map application;
|
||||
|
||||
/**
|
||||
* <p>Store a new application context.</p>
|
||||
*
|
||||
* @param value A Map representing application state
|
||||
*/
|
||||
public void setApplication(Map value) {
|
||||
application = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Provide application context.</p>
|
||||
*/
|
||||
public Map getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
// ---- SessionAware ----
|
||||
|
||||
/**
|
||||
* <p>Field to store session context, or its proxy.</p>
|
||||
*/
|
||||
private Map session;
|
||||
|
||||
/**
|
||||
* <p>Store a new session context.</p>
|
||||
*
|
||||
* @param value A Map representing session state
|
||||
*/
|
||||
public void setSession(Map value) {
|
||||
session = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Provide session context.</p>
|
||||
*
|
||||
* @return session context
|
||||
*/
|
||||
public Map getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
// ---- Task property (utilized by UI) ----
|
||||
|
||||
/**
|
||||
* <p>Field to store workflow task.</p>
|
||||
* <p/>
|
||||
* <p>The Task is used to track the state of the CRUD workflows. It can be
|
||||
* set to Constant.CREATE, Constant.EDIT, or Constant.DELETE as
|
||||
* needed.</p>
|
||||
*/
|
||||
private String task = null;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Provide worklow task.</p>
|
||||
*
|
||||
* @return Returns the task.
|
||||
*/
|
||||
public String getTask() {
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store new workflow task.</p>
|
||||
*
|
||||
* @param value The task to set.
|
||||
*/
|
||||
public void setTask(String value) {
|
||||
task = value;
|
||||
}
|
||||
|
||||
// ---- Token property (utilized by UI) ----
|
||||
|
||||
/**
|
||||
* <p>Field to store double-submit guard.</p>
|
||||
*/
|
||||
private String token = null;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Provide Token.</p>
|
||||
*
|
||||
* @return Returns the token.
|
||||
*/
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store new Token.</p>
|
||||
*
|
||||
* @param value The token to set.
|
||||
*/
|
||||
public void setToken(String value) {
|
||||
token = value;
|
||||
}
|
||||
|
||||
|
||||
// ---- Host property ----
|
||||
|
||||
/**
|
||||
* <p>Field to store Subscription host.</p>
|
||||
* <p/>
|
||||
* <p> The host is an immutable property of the Subscrtion DAP object, so
|
||||
* we need to store it locally until we are ready to create the
|
||||
* Subscription. </p>
|
||||
*/
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* <p>Provide tSubscription host.</p>
|
||||
*
|
||||
* @return host property
|
||||
*/
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store new Subscription host.</p>
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
public void setHost(String value) {
|
||||
host = value;
|
||||
}
|
||||
|
||||
// ---- Password property ----
|
||||
|
||||
/**
|
||||
* <p>Field to store User password property.</p>
|
||||
* <p/>
|
||||
* <p>The User DAO object password proerty is immutable, so we store it
|
||||
* locally until we are ready to create the object.</p>
|
||||
*/
|
||||
private String password = null;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Provide User password</p>
|
||||
*
|
||||
* @return Returns the password.
|
||||
*/
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store new User Password</p>
|
||||
*
|
||||
* @param value The password to set.
|
||||
*/
|
||||
public void setPassword(String value) {
|
||||
password = value;
|
||||
}
|
||||
|
||||
// ---- Password2 property (confirmation) ----
|
||||
|
||||
/**
|
||||
* <p>Field to store the User password confirmation.</p>
|
||||
* <p/>
|
||||
* <p>When a User object is created, we ask the client to enter the
|
||||
* password twice, to help ensure the password is being typed
|
||||
* correctly.</p>
|
||||
*/
|
||||
private String password2 = null;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Provide the User password confirmation.</p>
|
||||
*
|
||||
* @return Returns the confirmationpassword.
|
||||
*/
|
||||
public String getPassword2() {
|
||||
return password2;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store a new User password confirmation.</p>
|
||||
*
|
||||
* @param value The confirmation password to set.
|
||||
*/
|
||||
public void setPassword2(String value) {
|
||||
password2 = value;
|
||||
}
|
||||
|
||||
// ---- Username property ----
|
||||
|
||||
/**
|
||||
* <p>Field to store User username.</p>
|
||||
* <p/>
|
||||
* <p>The User DAO object password proerty is immutable, so we store it
|
||||
* locally until we are ready to create the object.</p>
|
||||
*/
|
||||
private String username = null;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Provide User username.</p>
|
||||
*
|
||||
* @return Returns the User username.
|
||||
*/
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store new User username</p>
|
||||
*
|
||||
* @param value The username to set.
|
||||
*/
|
||||
public void setUsername(String value) {
|
||||
username = value;
|
||||
}
|
||||
|
||||
// ---- Database property ----
|
||||
|
||||
/**
|
||||
* <p>Provide reference to UserDatabase, or null if the database is not
|
||||
* available. </p>
|
||||
*
|
||||
* @return a reference to the UserDatabase or null if the database is not
|
||||
* available
|
||||
*/
|
||||
public UserDatabase getDatabase() {
|
||||
Object db = getApplication().get(Constants.DATABASE_KEY);
|
||||
if (db == null) {
|
||||
this.addActionError(getText("error.database.missing"));
|
||||
}
|
||||
return (UserDatabase) db;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store a new reference to UserDatabase</p>
|
||||
*
|
||||
* @param database
|
||||
*/
|
||||
public void setDatabase(UserDatabase database) {
|
||||
getApplication().put(Constants.DATABASE_KEY, database);
|
||||
}
|
||||
|
||||
// ---- User property ----
|
||||
|
||||
/**
|
||||
* <p>Provide reference to User object for authenticated user.</p>
|
||||
*
|
||||
* @return User object for authenticated user.
|
||||
*/
|
||||
public User getUser() {
|
||||
return (User) getSession().get(Constants.USER_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store new reference to User Object.</p>
|
||||
*
|
||||
* @param user User object for authenticated user
|
||||
*/
|
||||
public void setUser(User user) {
|
||||
getSession().put(Constants.USER_KEY, user);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Obtain User object from database, or return null if the credentials
|
||||
* are not found or invalid.</p>
|
||||
*
|
||||
* @param username User username
|
||||
* @param password User password
|
||||
* @return User object or null if not found
|
||||
* @throws ExpiredPasswordException
|
||||
*/
|
||||
public User findUser(String username, String password)
|
||||
throws ExpiredPasswordException {
|
||||
// FIXME: Stupid testing hack to compensate for inadequate DAO layer
|
||||
if (Constants.EXPIRED_PASSWORD_EXCEPTION.equals(username)) {
|
||||
throw new ExpiredPasswordException(Constants.EXPIRED_PASSWORD_EXCEPTION);
|
||||
}
|
||||
|
||||
User user = getDatabase().findUser(username);
|
||||
if ((user != null) && !user.getPassword().equals(password)) {
|
||||
user = null;
|
||||
}
|
||||
if (user == null) {
|
||||
this.addFieldError(Constants.PASSWORD_MISMATCH_FIELD,
|
||||
getText("error.password.mismatch"));
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p><code>Log</code> instance for this application. </p>
|
||||
*/
|
||||
protected Log log = LogFactory.getLog(Constants.PACKAGE);
|
||||
|
||||
/**
|
||||
* <p> Persist the User object, including subscriptions, to the database.
|
||||
* </p>
|
||||
*
|
||||
* @throws java.lang.Exception on database error
|
||||
*/
|
||||
public void saveUser() throws Exception {
|
||||
try {
|
||||
getDatabase().save();
|
||||
} catch (Exception e) {
|
||||
String message = Constants.LOG_DATABASE_SAVE_ERROR + getUser()
|
||||
.getUsername();
|
||||
log.error(message, e);
|
||||
throw new Exception(message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public void createInputUser() {
|
||||
User user = new MemoryUser(null, null);
|
||||
setUser(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p> Verify input for creating a new user, create the user, and process
|
||||
* the login. </p>
|
||||
*
|
||||
* @return A new User and empty Errors if create succeeds, or null and
|
||||
* Errors if create fails
|
||||
*/
|
||||
public User createUser(String username, String password) {
|
||||
|
||||
UserDatabase database = getDatabase();
|
||||
User user;
|
||||
|
||||
try {
|
||||
user = database.findUser(username);
|
||||
}
|
||||
|
||||
catch (ExpiredPasswordException e) {
|
||||
user = getUser(); // Just so that it is not null
|
||||
}
|
||||
|
||||
if (user != null) {
|
||||
this.addFieldError("username", "error.username.unique");
|
||||
return null;
|
||||
}
|
||||
|
||||
return database.createUser(username);
|
||||
}
|
||||
|
||||
// Since user.username is immutable, we have to use some local properties
|
||||
|
||||
/**
|
||||
* <p>Use the current User object to create a new User object, and make
|
||||
* the new User object the authenticated user.</p>
|
||||
* <p/>
|
||||
* <p>The "current" User object is usually a temporary object being used
|
||||
* to capture input.</p>
|
||||
*
|
||||
* @param _username User username
|
||||
* @param _password User password
|
||||
*/
|
||||
public void copyUser(String _username, String _password) {
|
||||
User input = getUser();
|
||||
input.setPassword(_password);
|
||||
User user = createUser(_username, _password);
|
||||
if (null != user) {
|
||||
copyUser(input,user);
|
||||
setUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Subscription property ----
|
||||
|
||||
/**
|
||||
* <p>Obtain the cached Subscription object, if any. </p>
|
||||
*
|
||||
* @return Cached Subscription object or null
|
||||
*/
|
||||
public Subscription getSubscription() {
|
||||
return (Subscription) getSession().get(Constants.SUBSCRIPTION_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Store new User Subscription.</p>
|
||||
*
|
||||
* @param subscription
|
||||
*/
|
||||
public void setSubscription(Subscription subscription) {
|
||||
getSession().put(Constants.SUBSCRIPTION_KEY, subscription);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p> Obtain User Subscription object for the given host, or return null
|
||||
* if not found. </p>
|
||||
*
|
||||
* <p>It would be possible for this code to throw a NullPointerException,
|
||||
* but the ExceptionHandler in the xwork.xml will catch that for us.</p>
|
||||
*
|
||||
* @return The matching Subscription or null
|
||||
*/
|
||||
public Subscription findSubscription(String host) {
|
||||
Subscription subscription;
|
||||
subscription = getUser().findSubscription(host);
|
||||
return subscription;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Obtain uSER Subscription for the local Host property.</p>
|
||||
* <p/>
|
||||
* <p>Usually, the host property will be set from the client request,
|
||||
* because it was embedded in a link to the Subcription action.
|
||||
*
|
||||
* @return Subscription or null if not found
|
||||
*/
|
||||
public Subscription findSubscription() {
|
||||
return findSubscription(getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Provide a "temporary" User Subscription object that can be used to
|
||||
* capture input values.</p>
|
||||
*/
|
||||
public void createInputSubscription() {
|
||||
Subscription sub = new MemorySubscription(getUser(), null);
|
||||
setSubscription(sub);
|
||||
setHost(sub.getHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Provide new User Subscription object for the given host, or null if
|
||||
* the host is not unique.</p>
|
||||
*
|
||||
* @param host
|
||||
* @return New User Subscription object or null
|
||||
*/
|
||||
public Subscription createSubscription(String host) {
|
||||
|
||||
Subscription sub;
|
||||
|
||||
sub = findSubscription(host);
|
||||
|
||||
if (null != sub) {
|
||||
// FIXME - localization - "error.host.unique")
|
||||
addFieldError(Constants.HOST,"That hostname is already defined");
|
||||
return null;
|
||||
}
|
||||
|
||||
return getUser().createSubscription(host);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Create a new Subscription from the current Subscription object,
|
||||
* making the new Subscription the current Subscription. </p>
|
||||
* <p/>
|
||||
* <p>Usually, the "current" Subscription is a temporary object being used
|
||||
* to capture input values.</p>
|
||||
*
|
||||
* @param host
|
||||
*/
|
||||
public void copySubscription(String host) {
|
||||
Subscription input = getSubscription();
|
||||
Subscription sub = createSubscription(host);
|
||||
if (null != sub) {
|
||||
copySubscription(input, sub);
|
||||
setSubscription(sub);
|
||||
setHost(sub.getHost());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Delete the current Subscription object from the database.</p>
|
||||
*/
|
||||
public void removeSubscription() {
|
||||
getUser().removeSubscription(getSubscription());
|
||||
getSession().remove(Constants.SUBSCRIPTION_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Provide MailServer Host for current User Subscription.</p>
|
||||
*
|
||||
* @return MailServer Host for current User Subscription
|
||||
*/
|
||||
public String getSubscriptionHost() {
|
||||
Subscription sub = getSubscription();
|
||||
if (null == sub) {
|
||||
return null;
|
||||
}
|
||||
return sub.getHost();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
button.cancel=Cancel
|
||||
button.confirm=Confirm
|
||||
button.doSubmit=DO_SUBMIT
|
||||
button.doReset=DO_RESULT
|
||||
button.doCancel=org.apache.struts.taglib.html.CANCEL
|
||||
button.reset=Reset
|
||||
button.save=Save
|
||||
change.message=Your password has expired. Please ask the system administrator to change it.
|
||||
change.try=Try Again
|
||||
change.title=Password Has Expired
|
||||
database.load=Cannot load database from {0}
|
||||
error.database.missing=User database is missing, cannot validate login credentials
|
||||
error.fromAddress.format=Invalid format for From Address
|
||||
error.fromAddress.required=From Address is required
|
||||
error.fullName.required=Full Name is required
|
||||
error.host.required=Mail Server is required
|
||||
error.noSubscription=No Subscription bean in user session
|
||||
error.password.expired=Your password has expired for username {0}
|
||||
error.password.required=Password is required
|
||||
error.password2.required=Confirmation password is required
|
||||
error.password.match=Password and confirmation password must match
|
||||
error.password.mismatch=Invalid username and/or password, please try again
|
||||
error.replyToAddress.format=Invalid format for Reply To Address
|
||||
struts.messages.invalid.token=Cannot submit this form out of order
|
||||
error.type.invalid=Server Type must be 'imap' or 'pop3'
|
||||
error.type.required=Server Type is required
|
||||
error.username.required=Username is required
|
||||
error.username.unique=That username is already in use - please select another
|
||||
errors.footer=</ul><hr>
|
||||
errors.header=<h3><font color="red">Validation Error</font></h3><p>You must correct the following error(s) before proceeding:</p><ul>
|
||||
errors.prefix=<li>
|
||||
errors.suffix=</li>
|
||||
errors.ioException=I/O exception rendering error messages: {0}
|
||||
expired.password=User Password has expired for {0}
|
||||
heading.autoConnect=Auto
|
||||
heading.subscriptions=Current Subscriptions
|
||||
heading.host=Host Name
|
||||
heading.user=User Name
|
||||
heading.type=Server Type
|
||||
heading.action=Action
|
||||
index.heading=MailReader Demonstration Application Options
|
||||
index.login=Log on to the MailReader Demonstration Application
|
||||
index.registration=Register with the MailReader Demonstration Application
|
||||
index.title=MailReader Demonstration Application
|
||||
index.tour=A Walking Tour of the MailReader Demonstration Application
|
||||
linkSubscription.io=I/O Error: {0}
|
||||
linkSubscription.noSubscription=No subscription under attribute {0}
|
||||
linkUser.io=I/O Error: {0}
|
||||
linkUser.noUser=No user under attribute {0}
|
||||
login.title=MailReader Demonstration Application - Login
|
||||
mainMenu.heading=Main Menu Options for
|
||||
mainMenu.logout=Log off MailReader Demonstration Application
|
||||
mainMenu.registration=Edit your user registration profile
|
||||
mainMenu.title=MailReader Demonstration Application - Main Menu
|
||||
option.imap=IMAP Protocol
|
||||
option.pop3=POP3 Protocol
|
||||
# prompt.
|
||||
host=Mail Server
|
||||
password=Password
|
||||
password2=(Repeat) Password
|
||||
username=Username
|
||||
|
||||
registration.addSubscription=Add
|
||||
registration.deleteSubscription=Delete
|
||||
registration.editSubscription=Edit
|
||||
registration.title.create=Register for the MailReader Demonstration Application
|
||||
registration.title.edit=Edit Registration for the MailReader Demonstration Application
|
||||
|
||||
subscription.autoConnect=Auto Connect
|
||||
subscription.password=Mail Password
|
||||
subscription.type=Server Type
|
||||
subscription.username=Mail Username
|
||||
subscription.title.create=Create New Mail Subscription
|
||||
subscription.title.delete=Delete Existing Mail Subscription
|
||||
subscription.title.edit=Edit Existing Mail Subscription
|
||||
|
||||
user.fromAddress=From Address
|
||||
user.fullName=Full Name
|
||||
user.replyToAddress=Reply To Address
|
||||
|
||||
# Standard error messages for validator framework checks
|
||||
errors.required=${getText(fieldName)} is required.
|
||||
errors.minlength=${getText(fieldName)} cannot be less than {1} characters.
|
||||
errors.maxlength=${getText(fieldName)} cannot be greater than {1} characters.
|
||||
errors.invalid=${getText(fieldName)} is invalid.
|
||||
errors.byte=${getText(fieldName)} must be an byte.
|
||||
errors.short=${getText(fieldName)} must be an short.
|
||||
errors.integer=${getText(fieldName)} must be an integer.
|
||||
errors.long=${getText(fieldName)} must be an long.
|
||||
errors.float=${getText(fieldName)} must be an float.
|
||||
errors.double=${getText(fieldName)} must be an double.
|
||||
errors.date=${getText(fieldName)} is not a date.
|
||||
errors.range=${getText(fieldName)} is not in the range ${minLength} through ${maxLength}.
|
||||
errors.creditcard=${getText(fieldName)} is not a valid credit card number.
|
||||
errors.email=${getText(fieldName)} is an invalid e-mail address.
|
||||
errors.literal=${getText(fieldName)}
|
||||
@@ -0,0 +1,89 @@
|
||||
button.cancel=\u30ad\u30e3\u30f3\u30bb\u30eb
|
||||
button.confirm=\u78ba\u8a8d
|
||||
button.reset=\u30ea\u30bb\u30c3\u30c8
|
||||
button.save=\u4fdd\u5b58
|
||||
change.message=\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u6709\u52B9\u671F\u9650\u304C\u904E\u304E\u307E\u3057\u305F\u3002\u30B7\u30B9\u30C6\u30E0\u7BA1\u7406\u8005\u306B\u304A\u554F\u3044\u5408\u308F\u305B\u4E0B\u3055\u3044
|
||||
change.try=\u518D\u8A66\u884C
|
||||
change.title=\u30d1\u30b9\u30ef\u30fc\u30c9\u671f\u9650\u5207\u308c
|
||||
database.load= {0} \u304B\u3089\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3092\u30ED\u30FC\u30C9\u3067\u304D\u307E\u305B\u3093
|
||||
error.database.missing=\u30E6\u30FC\u30B6\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002\u30ED\u30B0\u30AA\u30F3\u306E\u8A8D\u8A3C\u304C\u51FA\u6765\u307E\u305B\u3093
|
||||
error.fromAddress.format=From\u30A2\u30C9\u30EC\u30B9\u306E\u66F8\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093
|
||||
error.fromAddress.required=From\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
|
||||
error.fullName.required=\u30D5\u30EB\u30CD\u30FC\u30E0\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
|
||||
error.host.required=\u30E1\u30FC\u30EB\u30B5\u30FC\u30D0\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
|
||||
error.noSubscription=Subscription bean \u304c\u30bb\u30c3\u30b7\u30e7\u30f3\u306e\u4e2d\u306b\u3042\u308a\u307e\u305b\u3093
|
||||
error.password.expired=\u30E6\u30FC\u30B6 {0} \u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u6709\u52B9\u671F\u9650\u304C\u904E\u304E\u307E\u3057\u305F
|
||||
error.password.required=\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u5FC5\u8981\u3067\u3059
|
||||
error.password2.required=\u30D1\u30B9\u30EF\u30FC\u30C9(\u78BA\u8A8D\u7528)\u304C\u5FC5\u8981\u3067\u3059
|
||||
error.password.match=\u30D1\u30B9\u30EF\u30FC\u30C9\u3068\u78BA\u8A8D\u7528\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u4E00\u81F4\u3057\u3066\u3044\u307E\u305B\u3093
|
||||
error.password.mismatch=\u30E6\u30FC\u30B6\u540D\u307E\u305F\u306F\u30D1\u30B9\u30EF\u30FC\u30C9\u304C\u4E0D\u6B63\u3067\u3059\u3002\u518D\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
|
||||
error.replyToAddress.format=\u8FD4\u4FE1\u30A2\u30C9\u30EC\u30B9\u306E\u66F8\u5F0F\u304C\u6B63\u3057\u304F\u3042\u308A\u307E\u305B\u3093
|
||||
struts.messages.invalid.token=\u3053\u306E\u30D5\u30A9\u30FC\u30E0\u306E\u5185\u5BB9\u304C\u6B63\u3057\u304F\u306A\u3044\u305F\u3081\u9001\u4FE1\u3059\u308B\u3053\u3068\u304C\u51FA\u6765\u307E\u305B\u3093
|
||||
error.type.invalid=\u30B5\u30FC\u30D0\u30BF\u30A4\u30D7\u306F 'imap' \u304B 'pop3'\u306E\u3069\u3061\u3089\u304B\u3067\u306A\u3051\u308C\u3070\u306A\u308A\u307E\u305B\u3093
|
||||
error.type.required=\u30B5\u30FC\u30D0\u30BF\u30A4\u30D7\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
|
||||
error.username.required=\u30E6\u30FC\u30B6\u540D\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044
|
||||
error.username.unique=\u305D\u306E\u30E6\u30FC\u30B6\u540D\u306F\u65E2\u306B\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059\u3002 \u5225\u306E\u30E6\u30FC\u30B6\u540D\u3092\u9078\u629E\u3057\u3066\u304F\u3060\u3055\u3044
|
||||
errors.footer=</ul><hr>
|
||||
errors.header=<h3><font color="red">\u5165\u529b\u30c1\u30a7\u30c3\u30af\u30a8\u30e9\u30fc</font></h3><p>\u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044:</p><ul>
|
||||
errors.prefix=<li>
|
||||
errors.suffix=</li>
|
||||
errors.ioException=I/O\u4f8b\u5916\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {0}
|
||||
expired.password=\u30E6\u30FC\u30B6 {0} \u306E\u30D1\u30B9\u30EF\u30FC\u30C9\u306E\u6709\u52B9\u671F\u9650\u304C\u904E\u304E\u307E\u3057\u305F
|
||||
heading.autoConnect=\u81ea\u52d5\u63a5\u7d9a
|
||||
heading.subscriptions=\u73fe\u5728\u306e\u8cfc\u8aad\u60c5\u5831
|
||||
heading.host=\u30db\u30b9\u30c8\u540d
|
||||
heading.user=\u30e6\u30fc\u30b6\u540d
|
||||
heading.type=\u30b5\u30fc\u30d0\u30bf\u30a4\u30d7
|
||||
heading.action=\u64cd\u4f5c
|
||||
index.heading=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 \u30aa\u30d7\u30b7\u30e7\u30f3
|
||||
index.login=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30ed\u30b0\u30aa\u30f3
|
||||
index.registration=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30e6\u30fc\u30b6\u767b\u9332
|
||||
index.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3(Struts 1.1-dev)
|
||||
index.tour=\u30b5\u30f3\u30d7\u30eb\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u3092\u6563\u7b56\u3059\u308b
|
||||
linkSubscription.io=I/O\u30a8\u30e9\u30fc: {0}
|
||||
linkSubscription.noSubscription=\u5c5e\u6027 {0} \u306b\u8cfc\u8aad\u60c5\u5831\u304c\u5b58\u5728\u3057\u307e\u305b\u3093
|
||||
linkUser.io=I/O\u30a8\u30e9\u30fc: {0}
|
||||
linkUser.noUser=\u5c5e\u6027 {0} \u306b\u30e6\u30fc\u30b6\u60c5\u5831\u304c\u5b58\u5728\u3057\u307e\u305b\u3093
|
||||
login.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30ed\u30b0\u30aa\u30f3
|
||||
mainMenu.heading=\u30e1\u30a4\u30f3\u30e1\u30cb\u30e5\u30fc
|
||||
mainMenu.logout=MailReader \u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u30ed\u30b0\u30aa\u30d5
|
||||
mainMenu.registration=\u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u306e\u7de8\u96c6
|
||||
mainMenu.title=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30e1\u30a4\u30f3\u30e1\u30cb\u30e5\u30fc
|
||||
option.imap=IMAP \u30d7\u30ed\u30c8\u30b3\u30eb
|
||||
option.pop3=POP3 \u30d7\u30ed\u30c8\u30b3\u30eb
|
||||
# prompt.
|
||||
autoConnect=\u81ea\u52d5\u63a5\u7d9a
|
||||
fromAddress=From\u30a2\u30c9\u30ec\u30b9
|
||||
fullName=\u30d5\u30eb\u30cd\u30fc\u30e0
|
||||
mailHostname=\u30e1\u30fc\u30eb\u30b5\u30fc\u30d0
|
||||
mailPassword=\u30e1\u30fc\u30eb\u30d1\u30b9\u30ef\u30fc\u30c9
|
||||
mailServerType=\u30b5\u30fc\u30d0\u30bf\u30a4\u30d7
|
||||
mailUsername=\u30e1\u30fc\u30eb\u30e6\u30fc\u30b6\u540d
|
||||
.password=\u30d1\u30b9\u30ef\u30fc\u30c9
|
||||
password2=\u30d1\u30b9\u30ef\u30fc\u30c9(\u78ba\u8a8d\u7528)
|
||||
replyToAddress=\u8fd4\u4fe1\u30a2\u30c9\u30ec\u30b9
|
||||
username=\u30e6\u30fc\u30b6\u540d
|
||||
registration.addSubscription=\u65b0\u898f\u4f5c\u6210
|
||||
registration.deleteSubscription=\u524a\u9664
|
||||
registration.editSubscription=\u7de8\u96c6
|
||||
registration.title.create=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30e6\u30fc\u30b6\u767b\u9332
|
||||
registration.title.edit=MailReader\u30c7\u30e2\u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3 - \u30d7\u30ed\u30d5\u30a1\u30a4\u30eb\u7de8\u96c6
|
||||
subscription.title.create=\u30e1\u30fc\u30eb\u8cfc\u8aad\u60c5\u5831\u306e\u65b0\u898f\u4f5c\u6210
|
||||
subscription.title.delete=\u30e1\u30fc\u30eb\u8cfc\u8aad\u60c5\u5831\u306e\u524a\u9664
|
||||
subscription.title.edit=\u30e1\u30fc\u30eb\u8cfc\u8aad\u60c5\u5831\u306e\u7de8\u96c6
|
||||
|
||||
# Standard error messages for validator framework checks
|
||||
errors.required=${getText(fieldName)} \u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002
|
||||
errors.minlength=${getText(fieldName)} \u306f {1} \u6587\u5b57\u4ee5\u4e0a\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.maxlength=${getText(fieldName)} \u306f {2} \u6587\u5b57\u4ee5\u4e0b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.invalid=${getText(fieldName)} \u306f\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002
|
||||
errors.byte=${getText(fieldName)} \u306fbyte\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.short=${getText(fieldName)} \u306fshort\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.integer=${getText(fieldName)} \u306finteger\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.long=${getText(fieldName)} \u306flong\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.float=${getText(fieldName)} \u306ffloat\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.double=${getText(fieldName)} \u306fdouble\u578b\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.date=${getText(fieldName)} \u306f\u65e5\u4ed8\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
|
||||
errors.range=${getText(fieldName)} \u306f {1} \u304b\u3089 {2} \u306e\u9593\u3067\u306a\u3051\u308c\u3070\u306a\u308a\u307e\u305b\u3093\u3002
|
||||
errors.creditcard=${getText(fieldName)} \u306f\u6b63\u3057\u3044\u30af\u30ec\u30b8\u30c3\u30c8\u30ab\u30fc\u30c9\u756a\u53f7\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
|
||||
errors.email=${getText(fieldName)} \u306f\u6b63\u3057\u3044\u30e1\u30fc\u30eb\u30a2\u30c9\u30ec\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002
|
||||
@@ -0,0 +1,89 @@
|
||||
button.cancel=\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c
|
||||
button.confirm=\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c
|
||||
button.reset=\u0421\u0431\u0440\u043e\u0441\u0438\u0442\u044c
|
||||
button.save=\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c
|
||||
change.message=Your password has expired. Please ask the system administrator to change it.
|
||||
change.try=Try Again
|
||||
change.title=Password Has Expired
|
||||
database.load=\u0411\u0430\u0437\u0430 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u0437 {0}
|
||||
error.database.missing=\u041d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f - \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e.
|
||||
error.fromAddress.format=\u0412 \u043f\u043e\u043b\u0435 '\u0410\u0434\u0440\u0435\u0441 \u041e\u0442:' \u0443\u043a\u0430\u0437\u0430\u043d \u0430\u0434\u0440\u0435\u0441 \u0432 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435.
|
||||
error.fromAddress.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0430\u0434\u0440\u0435\u0441 \u0432 \u043f\u043e\u043b\u0435 '\u0410\u0434\u0440\u0435\u0441 \u041e\u0442:'.
|
||||
error.fullName.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u043b\u043d\u043e\u0435 \u0438\u043c\u044f.
|
||||
error.host.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440.
|
||||
error.noSubscription=\u041f\u043e\u0434\u043f\u0438\u0441\u043a\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0432 \u0441\u0435\u0441\u0441\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f
|
||||
error.password.expired=Your password has expired for username {0}
|
||||
error.password.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c.
|
||||
error.password2.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f.
|
||||
error.password.match=\u041f\u0430\u0440\u043e\u043b\u044c \u0438 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f \u043d\u0435 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u044e\u0442.
|
||||
error.password.mismatch=\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0435 \u0438\u043c\u044f \u0438/\u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c - \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0441\u043d\u043e\u0432\u0430.
|
||||
error.replyToAddress.format=\u0412 \u043f\u043e\u043b\u0435 '\u0410\u0434\u0440\u0435\u0441 \u041e\u0442\u0432\u0435\u0442\u0438\u0442\u044c \u043d\u0430:' \u0443\u043a\u0430\u0437\u0430\u043d \u0430\u0434\u0440\u0435\u0441 \u0432 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u043c \u0444\u043e\u0440\u043c\u0430\u0442\u0435.
|
||||
struts.messages.invalid.token=\u042d\u0442\u0430 \u0444\u043e\u0440\u043c\u0430 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u0430 - \u043d\u0430\u0440\u0443\u0448\u0435\u043d\u0438\u0435 \u043f\u043e\u0440\u044f\u0434\u043a\u0430 \u0437\u0430\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u0434\u0430\u043d\u043d\u044b\u0445.
|
||||
error.type.invalid=\u0412 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0442\u0438\u043f\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u044b \u043b\u0438\u0448\u044c 'imap' \u0438\u043b\u0438 'pop3'
|
||||
error.type.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0442\u0438\u043f \u0441\u0435\u0440\u0432\u0435\u0440\u0430
|
||||
error.username.required=\u0422\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0443\u043a\u0430\u0437\u0430\u0442\u044c \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f
|
||||
error.username.unique=\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f - \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0434\u0440\u0443\u0433\u043e\u0435 \u0438\u043c\u044f.
|
||||
errors.footer=</ul><hr>
|
||||
errors.header=<h3><font color="red">\u041e\u0448\u0438\u0431\u043a\u0438 \u043f\u0440\u0438 \u0437\u0430\u043d\u0435\u0441\u0435\u043d\u0438\u0438 \u0434\u0430\u043d\u043d\u044b\u0445</font></h3><p>\u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u043d\u0438\u0436\u0435 \u043e\u0448\u0438\u0431\u043a\u0438:</p><ul>
|
||||
errors.prefix=<li>
|
||||
errors.suffix=</li>
|
||||
errors.ioException=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0430\u0445: {0}
|
||||
expired.password=User Password has expired for {0}
|
||||
heading.autoConnect=\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438
|
||||
heading.subscriptions=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438
|
||||
heading.host=\u0421\u0435\u0440\u0432\u0435\u0440
|
||||
heading.user=\u0418\u043c\u044f
|
||||
heading.type=\u0422\u0438\u043f \u0441\u0435\u0440\u0432\u0435\u0440\u0430
|
||||
heading.action=\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435
|
||||
index.heading=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b'
|
||||
index.login=\u0412\u043e\u0439\u0442\u0438 \u043a\u0430\u043a \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c
|
||||
index.registration=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f
|
||||
index.title=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b' (Struts 1.1-dev)
|
||||
index.tour=\u041e\u0431\u0437\u043e\u0440 \u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b'
|
||||
linkSubscription.io=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 (\u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438): {0}
|
||||
linkSubscription.noSubscription=\u0410\u0442\u0440\u0438\u0431\u0443\u0442 {0} \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0435 \u0438\u043b\u0438 \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.
|
||||
linkUser.io=\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 (\u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f): {0}
|
||||
linkUser.noUser=\u0410\u0442\u0440\u0438\u0431\u0443\u0442 {0} \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435 \u0438\u043b\u0438 \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.
|
||||
login.title=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b - \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0438\u043c\u0435\u043d\u0438 \u0438 \u043f\u0430\u0440\u043e\u043b\u044f.
|
||||
mainMenu.heading=\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0433\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043c\u0435\u043d\u044e \u0434\u043b\u044f
|
||||
mainMenu.logout=\u0412\u044b\u0439\u0442\u0438
|
||||
mainMenu.registration=\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0441\u0432\u043e\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438
|
||||
mainMenu.title=\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 '\u0427\u0442\u0435\u043d\u0438\u0435 \u043f\u043e\u0447\u0442\u044b' - \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0435 \u043c\u0435\u043d\u044e
|
||||
option.imap=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b IMAP
|
||||
option.pop3=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b POP3
|
||||
# prompt.
|
||||
autoConnect=\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435:
|
||||
fromAddress=\u0410\u0434\u0440\u0435\u0441 \u041e\u0442:
|
||||
fullName=\u041f\u043e\u043b\u043d\u043e\u0435 \u0438\u043c\u044f:
|
||||
mailHostname=\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0441\u0435\u0440\u0432\u0435\u0440:
|
||||
mailPassword=\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430:
|
||||
mailServerType=\u0422\u0438\u043f \u0441\u0435\u0440\u0432\u0435\u0440\u0430:
|
||||
mailUsername=\u0418\u043c\u044f \u0434\u043b\u044f \u043f\u043e\u0447\u0442\u043e\u0432\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430:
|
||||
password=\u041f\u0430\u0440\u043e\u043b\u044c:
|
||||
password2=(\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435) \u041f\u0430\u0440\u043e\u043b\u044c:
|
||||
replyToAddress=\u0410\u0434\u0440\u0435\u0441 \u041e\u0442\u0432\u0435\u0442\u0438\u0442\u044c \u043d\u0430:
|
||||
username=\u0418\u043c\u044f:
|
||||
registration.addSubscription=\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c
|
||||
registration.deleteSubscription=\u0423\u0434\u0430\u043b\u0438\u0442\u044c
|
||||
registration.editSubscription=\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c
|
||||
registration.title.create=\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f
|
||||
registration.title.edit=\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043e \u0441\u0432\u043e\u0435\u0439 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438
|
||||
subscription.title.create=\u0421\u043e\u0437\u0434\u0430\u0442\u044c \u043d\u043e\u0432\u0443\u044e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0443
|
||||
subscription.title.delete=\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0443
|
||||
subscription.title.edit=\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0443\u044e \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0443
|
||||
|
||||
# Standard error messages for validator framework checks
|
||||
errors.required=${getText(fieldName)} is required.
|
||||
errors.minlength=${getText(fieldName)} cannot be less than {1} characters.
|
||||
errors.maxlength=${getText(fieldName)} cannot be greater than {2} characters.
|
||||
errors.invalid=${getText(fieldName)} is invalid.
|
||||
errors.byte=${getText(fieldName)} must be an byte.
|
||||
errors.short=${getText(fieldName)} must be an short.
|
||||
errors.integer=${getText(fieldName)} must be an integer.
|
||||
errors.long=${getText(fieldName)} must be an long.
|
||||
errors.float=${getText(fieldName)} must be an float.
|
||||
errors.double=${getText(fieldName)} must be an double.
|
||||
errors.date=${getText(fieldName)} is not a date.
|
||||
errors.range=${getText(fieldName)} is not in the range {1} through {2}.
|
||||
errors.creditcard=${getText(fieldName)} is not a valid credit card number.
|
||||
errors.email=${getText(fieldName)} is an invalid e-mail address.
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
|
||||
|
||||
<validators>
|
||||
|
||||
<field name="password">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.password.required"/>
|
||||
</field-validator>
|
||||
<field-validator type="stringlength">
|
||||
<param name="trim">true</param>
|
||||
<param name="minLength">4</param>
|
||||
<param name="maxLength">10</param>
|
||||
<message key="errors.range"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
<field name="password2">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.password2.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
<validator type="expression">
|
||||
<param name="expression">password eq password2</param>
|
||||
<message key="error.password.match"/>
|
||||
</validator>
|
||||
|
||||
</validators>
|
||||
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
|
||||
|
||||
<validators>
|
||||
|
||||
<field name="username">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.username.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
<field name="user.fullName">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.fullName.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
<field name="user.fromAddress">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.fromAddress.required"/>
|
||||
</field-validator>
|
||||
<field-validator type="email">
|
||||
<message key="errors.email"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
<field name="user.replyToAddress">
|
||||
<field-validator type="email">
|
||||
<message key="errors.email"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
</validators>
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
import org.apache.struts.apps.mailreader.dao.User;
|
||||
|
||||
|
||||
/**
|
||||
* <p>Insert or update a User object to the persistent store. </p>
|
||||
*/
|
||||
public class Registration extends MailreaderSupport {
|
||||
|
||||
/**
|
||||
* <p>Double check that there is not a valid User login. </p>
|
||||
*
|
||||
* @return True if there is not a valid User login
|
||||
*/
|
||||
private boolean isCreating() {
|
||||
User user = getUser();
|
||||
return (null == user) || (null == user.getDatabase());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p> Retrieve User object to edit or null if User does not exist. </p>
|
||||
*
|
||||
* @return The "Success" result for this mapping
|
||||
* @throws Exception on any error
|
||||
*/
|
||||
public String input() throws Exception {
|
||||
|
||||
if (isCreating()) {
|
||||
createInputUser();
|
||||
setTask(Constants.CREATE);
|
||||
} else {
|
||||
setTask(Constants.EDIT);
|
||||
setUsername(getUser().getUsername());
|
||||
setPassword(getUser().getPassword());
|
||||
setPassword2(getUser().getPassword());
|
||||
}
|
||||
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Insert or update a Registration.</p>
|
||||
*
|
||||
* @return The "outcome" result code
|
||||
* @throws Exception on any error
|
||||
*/
|
||||
public String save() throws Exception {
|
||||
return execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p> Insert or update a User object to the persistent store. </p>
|
||||
* <p/>
|
||||
* <p> If a User is not logged in, then a new User is created and
|
||||
* automatically logged in. Otherwise, the existing User is updated. </p>
|
||||
*
|
||||
* @return The "outcome" result code
|
||||
* @throws Exception on any error
|
||||
*/
|
||||
public String execute()
|
||||
throws Exception {
|
||||
|
||||
boolean creating = Constants.CREATE.equals(getTask());
|
||||
creating = creating && isCreating(); // trust but verify
|
||||
|
||||
if (creating) {
|
||||
|
||||
User user = findUser(getUsername(), getPassword());
|
||||
boolean haveUser = (user != null);
|
||||
|
||||
if (haveUser) {
|
||||
addActionError(getText("error.username.unique"));
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
copyUser(getUsername(), getPassword());
|
||||
|
||||
} else {
|
||||
|
||||
// FIXME: Any way to call the RegisrationSave validators from here?
|
||||
String newPassword = getPassword();
|
||||
if (newPassword != null) {
|
||||
String confirmPassword = getPassword2();
|
||||
boolean matches = ((null != confirmPassword)
|
||||
&& (confirmPassword.equals(newPassword)));
|
||||
if (matches) {
|
||||
getUser().setPassword(newPassword);
|
||||
} else {
|
||||
addActionError(getText("error.password.match"));
|
||||
return INPUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveUser();
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
|
||||
|
||||
<validators>
|
||||
|
||||
<field name="subscription.username">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.username.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
<field name="subscription.password">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.password.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
<field name="subscription.type">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.type.invalid"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
</validators>
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
|
||||
|
||||
<validators>
|
||||
|
||||
<field name="host">
|
||||
<field-validator type="requiredstring">
|
||||
<message key="error.host.required"/>
|
||||
</field-validator>
|
||||
</field>
|
||||
|
||||
</validators>
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
import com.opensymphony.xwork2.Preparable;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <p> Provide an Edit method for retrieving an existing subscription, and a
|
||||
* Save method for updating or inserting a subscription. </p>
|
||||
*/
|
||||
public class Subscription extends MailreaderSupport
|
||||
implements Preparable {
|
||||
|
||||
/**
|
||||
* <p>Field to store list of MailServer types</p>
|
||||
*/
|
||||
private Map types = null;
|
||||
|
||||
/**
|
||||
* <p>Provide the list of MailServer types.</p>
|
||||
*
|
||||
* @return List of MailServer types
|
||||
*/
|
||||
public Map getTypes() {
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Setup the MailerServer types and set the local Host property from
|
||||
* the User Subscription (if any). </p>
|
||||
*/
|
||||
public void prepare() {
|
||||
|
||||
Map m = new LinkedHashMap();
|
||||
m.put("imap", "IMAP Protocol");
|
||||
m.put("pop3", "POP3 Protocol");
|
||||
types = m;
|
||||
|
||||
setHost(getSubscriptionHost());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Setup a temporary User Subscription object to capture input
|
||||
* values.</p>
|
||||
*
|
||||
* @return INPUT
|
||||
*/
|
||||
public String input() {
|
||||
createInputSubscription();
|
||||
setTask(Constants.CREATE);
|
||||
return INPUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Load User Subscription for the local Host property.</p>
|
||||
* <p/>
|
||||
* <p>Usually, the Host is being set from the request by a link to an Edit
|
||||
* or Delete task.</p>
|
||||
*
|
||||
* @return INPUT or Error, if Subscription is not found
|
||||
*/
|
||||
public String find() {
|
||||
|
||||
org.apache.struts.apps.mailreader.dao.Subscription
|
||||
sub = findSubscription();
|
||||
|
||||
if (sub == null) {
|
||||
return ERROR;
|
||||
}
|
||||
|
||||
setSubscription(sub);
|
||||
|
||||
return INPUT;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Prepare to present a confirmation page before removing
|
||||
* Subscription.</p>
|
||||
*
|
||||
* @return INPUT or Error, if Subscription is not found
|
||||
*/
|
||||
public String delete() {
|
||||
|
||||
setTask(Constants.DELETE);
|
||||
return find();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Prepare to edit User Subscription.</p>
|
||||
*
|
||||
* @return INPUT or Error, if Subscription is not found
|
||||
*/
|
||||
public String edit() {
|
||||
|
||||
setTask(Constants.EDIT);
|
||||
return find();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p> Examine the Task property and DELETE, CREATE, or save the User
|
||||
* Subscription, as appropriate. </p>
|
||||
*
|
||||
* @return SUCCESS
|
||||
* @throws Exception on a database error
|
||||
*/
|
||||
public String save() throws Exception {
|
||||
|
||||
if (Constants.DELETE.equals(getTask())) {
|
||||
removeSubscription();
|
||||
}
|
||||
|
||||
if (Constants.CREATE.equals(getTask())) {
|
||||
copySubscription(getHost());
|
||||
}
|
||||
|
||||
if (hasErrors()) return INPUT;
|
||||
|
||||
saveUser();
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* $Id$
|
||||
*
|
||||
* 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 mailreader2;
|
||||
|
||||
/**
|
||||
* Verify that essential resources are available.
|
||||
*/
|
||||
public class Welcome extends MailreaderSupport {
|
||||
|
||||
public String execute() {
|
||||
|
||||
// Confirm message resources loaded
|
||||
String message = getText(Constants.ERROR_DATABASE_MISSING);
|
||||
if (Constants.ERROR_DATABASE_MISSING.equals(message)) {
|
||||
addActionError(Constants.ERROR_MESSAGES_NOT_LOADED);
|
||||
}
|
||||
|
||||
// Confirm database loaded
|
||||
if (null==getDatabase()) {
|
||||
addActionError(Constants.ERROR_DATABASE_NOT_LOADED);
|
||||
}
|
||||
|
||||
if (hasErrors()) {
|
||||
return ERROR;
|
||||
}
|
||||
else {
|
||||
return SUCCESS;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE struts PUBLIC
|
||||
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
|
||||
"http://struts.apache.org/dtds/struts-2.0.dtd">
|
||||
|
||||
<struts>
|
||||
|
||||
<constant name="struts.action.extension" value="do" />
|
||||
<constant name="struts.devMode" value="false" />
|
||||
<constant name="struts.enable.DynamicMethodInvocation" value="false" />
|
||||
|
||||
<include file="mailreader-default.xml"/>
|
||||
|
||||
<include file="mailreader-support.xml"/>
|
||||
|
||||
</struts>
|
||||
@@ -0,0 +1,5 @@
|
||||
Apache Struts
|
||||
Copyright 2000-2007 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by
|
||||
The Apache Software Foundation (http://www.apache.org/).
|
||||
@@ -0,0 +1,25 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title><s:text name="change.title"/></title>
|
||||
<link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<p>
|
||||
<s:text name="change.message"/>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="<s:url action="Login_input"/>">
|
||||
<s:text name="change.try"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,40 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title>Unexpected Error</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>An unexpected error has occured</h2>
|
||||
|
||||
<p>
|
||||
Please report this error to your system administrator
|
||||
or appropriate technical support personnel.
|
||||
Thank you for your cooperation.
|
||||
</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h3>Error Message</h3>
|
||||
|
||||
<s:actionerror />
|
||||
|
||||
<p>
|
||||
<s:property value="%{exception.message}"/>
|
||||
</p>
|
||||
|
||||
<hr/>
|
||||
|
||||
<h3>Technical Details</h3>
|
||||
|
||||
<p>
|
||||
<s:property value="%{exceptionStack}"/>
|
||||
</p>
|
||||
|
||||
<jsp:include page="Footer.jsp"/>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<hr/>
|
||||
|
||||
<p>
|
||||
<a href="<s:url action="Welcome" includeParams="none"/>"><s:text name="index.title"/></a>
|
||||
</p>
|
||||
@@ -0,0 +1,30 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title><s:text name="login.title"/></title>
|
||||
<link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body onLoad="self.focus();document.Login.username.focus()">
|
||||
|
||||
<s:actionerror />
|
||||
<s:form action="Login" validate="true">
|
||||
<s:textfield key="username" />
|
||||
|
||||
<s:password key="password" showPassword="true"/>
|
||||
|
||||
<s:submit key="button.save"/>
|
||||
|
||||
<s:reset key="button.reset"/>
|
||||
|
||||
<s:submit action="Login_cancel" key="button.cancel"
|
||||
onclick="form.onsubmit=null"/>
|
||||
</s:form>
|
||||
|
||||
<jsp:include page="Footer.jsp"/>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Context path="/">
|
||||
</Context>
|
||||
@@ -0,0 +1,25 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<title><s:text name="mainMenu.title"/></title>
|
||||
<link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3><s:text name="mainMenu.heading"/> <s:property
|
||||
value="user.fullName"/></h3>
|
||||
<ul>
|
||||
<li><a href="<s:url action="Registration_input" />">
|
||||
<s:text name="mainMenu.registration"/>
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="<s:url action="Logout"/>">
|
||||
<s:text name="mainMenu.logout"/>
|
||||
</a>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,115 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<s:if test="task=='Create'">
|
||||
<title><s:text name="registration.title.create"/></title>
|
||||
</s:if>
|
||||
<s:if test="task=='Edit'">
|
||||
<title><s:text name="registration.title.edit"/></title>
|
||||
</s:if>
|
||||
<link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body onLoad="self.focus();document.Registration_save_username.focus()">
|
||||
|
||||
<s:actionerror/>
|
||||
<s:form action="Registration_save" validate="false">
|
||||
<s:token />
|
||||
<s:hidden name="task"/>
|
||||
<s:if test="task == 'Create'">
|
||||
<s:textfield key="username"/>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<s:label key="username"/>
|
||||
<s:hidden name="username"/>
|
||||
</s:else>
|
||||
|
||||
<s:password key="password" showPassword="true"/>
|
||||
<s:password key="password2"/>
|
||||
<s:textfield key="user.fullName"/>
|
||||
<s:textfield key="user.fromAddress"/>
|
||||
<s:textfield key="user.replyToAddress"/>
|
||||
|
||||
<s:if test="task == 'Create'">
|
||||
<s:submit key="button.save" action="Registration_save"/>
|
||||
<s:reset key="button.reset"/>
|
||||
<s:submit action="Welcome" key="button.cancel"
|
||||
onclick="form.onsubmit=null"/>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<s:submit key="button.save" action="Registration"/>
|
||||
<s:reset key="button.reset"/>
|
||||
<s:submit action="MainMenu" key="button.cancel"
|
||||
onclick="form.onsubmit=null"/>
|
||||
</s:else>
|
||||
|
||||
</s:form>
|
||||
|
||||
<s:if test="task == 'Edit'">
|
||||
<div align="center">
|
||||
<h3><s:text name="heading.subscriptions"/></h3>
|
||||
</div>
|
||||
|
||||
<table border="1" width="100%">
|
||||
|
||||
<tr>
|
||||
<th align="center" width="30%">
|
||||
<s:text name="heading.host"/>
|
||||
</th>
|
||||
<th align="center" width="25%">
|
||||
<s:text name="heading.user"/>
|
||||
</th>
|
||||
<th align="center" width="10%">
|
||||
<s:text name="heading.type"/>
|
||||
</th>
|
||||
<th align="center" width="10%">
|
||||
<s:text name="heading.autoConnect"/>
|
||||
</th>
|
||||
<th align="center" width="15%">
|
||||
<s:text name="heading.action"/>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<s:iterator value="user.subscriptions">
|
||||
<tr>
|
||||
<td align="left">
|
||||
<s:property value="host"/>
|
||||
</td>
|
||||
<td align="left">
|
||||
<s:property value="username"/>
|
||||
</td>
|
||||
<td align="center">
|
||||
<s:property value="type"/>
|
||||
</td>
|
||||
<td align="center">
|
||||
<s:property value="autoConnect"/>
|
||||
</td>
|
||||
<td align="center">
|
||||
|
||||
<a href="<s:url action="Subscription_delete"><s:param name="host" value="host"/></s:url>">
|
||||
<s:text name="registration.deleteSubscription"/>
|
||||
</a>
|
||||
|
||||
<a href="<s:url action="Subscription_edit"><s:param name="host" value="host"/></s:url>">
|
||||
<s:text name="registration.editSubscription"/>
|
||||
</a>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</s:iterator>
|
||||
|
||||
</table>
|
||||
|
||||
<a href="<s:url action="Subscription_input"/>"><s:text
|
||||
name="registration.addSubscription"/></a>
|
||||
|
||||
</s:if>
|
||||
|
||||
<jsp:include page="Footer.jsp"/>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<s:if test="task=='Create'">
|
||||
<title><s:text name="subscription.title.create"/></title>
|
||||
</s:if>
|
||||
<s:if test="task=='Edit'">
|
||||
<title><s:text name="subscription.title.edit"/></title>
|
||||
</s:if>
|
||||
<s:if test="task=='Delete'">
|
||||
<title><s:text name="subscription.title.delete"/></title>
|
||||
</s:if>
|
||||
<link href="<s:url value="/css/mailreader.css" includeParams="none"/>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body onLoad="self.focus();document.Subscription.username.focus()">
|
||||
|
||||
<s:actionerror/>
|
||||
<s:form action="Subscription_save" validate="true">
|
||||
<s:token />
|
||||
<s:hidden name="task"/>
|
||||
<s:label key="username" name="user.username"/>
|
||||
|
||||
<s:if test="task == 'Create'">
|
||||
<s:textfield key="host"/>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<s:label key="host"/>
|
||||
<s:hidden name="host"/>
|
||||
</s:else>
|
||||
|
||||
<s:if test="task == 'Delete'">
|
||||
<s:label key="subscription.username"/>
|
||||
<s:label key="subscription.password"/>
|
||||
<s:label key="subscription.type"/>
|
||||
<s:label key="subscription.autoConnect"/>
|
||||
<s:submit key="button.confirm"/>
|
||||
</s:if>
|
||||
<s:else>
|
||||
<s:textfield key="subscription.username"/>
|
||||
<s:textfield key="subscription.password"/>
|
||||
<s:select key="subscription.type" list="types"/>
|
||||
<s:checkbox key="subscription.autoConnect"/>
|
||||
<s:submit key="button.save"/>
|
||||
<s:reset key="button.reset"/>
|
||||
</s:else>
|
||||
|
||||
<s:submit action="Registration_input"
|
||||
key="button.cancel"
|
||||
onclick="form.onsubmit=null"/>
|
||||
</s:form>
|
||||
|
||||
<jsp:include page="Footer.jsp"/>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version='1.0'?>
|
||||
<database>
|
||||
<user username="user" fromAddress="John.User@somewhere.com"
|
||||
fullName="John Q. User" password="pass">
|
||||
<subscription host="mail.hotmail.com" autoConnect="false"
|
||||
password="bar" type="pop3" username="user1234">
|
||||
</subscription>
|
||||
<subscription host="mail.yahoo.com" autoConnect="false" password="foo"
|
||||
type="imap" username="jquser">
|
||||
</subscription>
|
||||
</user>
|
||||
</database>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
|
||||
<web-app>
|
||||
|
||||
<display-name>Struts 2 Mailreader</display-name>
|
||||
|
||||
<filter>
|
||||
<filter-name>Struts2</filter-name>
|
||||
<filter-class>
|
||||
org.apache.struts2.dispatcher.FilterDispatcher
|
||||
</filter-class>
|
||||
</filter>
|
||||
|
||||
<filter-mapping>
|
||||
<filter-name>Struts2</filter-name>
|
||||
<url-pattern>*.do</url-pattern>
|
||||
</filter-mapping>
|
||||
|
||||
<!-- Application Listener for Mailreader database -->
|
||||
<listener>
|
||||
<listener-class>
|
||||
mailreader2.ApplicationListener
|
||||
</listener-class>
|
||||
</listener>
|
||||
|
||||
<welcome-file-list>
|
||||
<welcome-file>index.html</welcome-file>
|
||||
</welcome-file-list>
|
||||
|
||||
</web-app>
|
||||
@@ -0,0 +1,55 @@
|
||||
<%@ page contentType="text/html; charset=UTF-8" %>
|
||||
<%@ taglib uri="/struts-tags" prefix="s" %>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title><s:text name="index.title"/></title>
|
||||
<link href="<s:url value="/css/mailreader.css"/>" rel="stylesheet"
|
||||
type="text/css"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3><s:text name="index.heading"/></h3>
|
||||
|
||||
<ul>
|
||||
<li><a href="<s:url action="Registration_input"/>"><s:text
|
||||
name="index.registration"/></a></li>
|
||||
<li><a href="<s:url action="Login_input"/>"><s:text
|
||||
name="index.login"/></a></li>
|
||||
</ul>
|
||||
|
||||
<h3>Language Options</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<s:url id="en" action="Welcome">
|
||||
<s:param name="request_locale">en</s:param>
|
||||
</s:url>
|
||||
<s:a href="%{en}">English</s:a>
|
||||
</li>
|
||||
<li>
|
||||
<s:url id="ja" action="Welcome">
|
||||
<s:param name="request_locale">ja</s:param>
|
||||
</s:url>
|
||||
<s:a href="%{ja}">Japanese</s:a>
|
||||
</li>
|
||||
<li>
|
||||
<s:url id="ru" action="Welcome">
|
||||
<s:param name="request_locale">ru</s:param>
|
||||
</s:url>
|
||||
<s:a href="%{ru}">Russian</s:a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p><s:i18n name="alternate"><a href="http://struts.apache.org/">
|
||||
<img src="<s:text name="struts.logo.path"/>"
|
||||
alt="<s:text name="struts.logo.alt"/>" border="0px"/>
|
||||
</a>
|
||||
</s:i18n></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Mailreader stylesheet
|
||||
*/
|
||||
|
||||
body {
|
||||
background-color: #FFFFFF;
|
||||
color: #000000;
|
||||
link: 000066;
|
||||
visited: #660066;
|
||||
active: #33CCCC;
|
||||
}
|
||||
|
||||
A:hover {
|
||||
color: #FF0000;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
font.hint {
|
||||
font-style: italic;
|
||||
font-size: 80%;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
|
||||
<html>
|
||||
<head>
|
||||
<META HTTP-EQUIV="Refresh" CONTENT="0;URL=Welcome.do">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>Loading ...</p>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user