-
Notifications
You must be signed in to change notification settings - Fork 863
WW-5695 Derive HTML5 constraint attributes from validators, deprecate the JS validator #1865
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
lukaszlenart
merged 26 commits into
main
from
feature/WW-5695-html5-constraint-validation
Aug 27, 2026
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
30947b0
WW-5695 docs(validation): design HTML5 constraint validation and reti…
lukaszlenart 510f31a
WW-5695 docs(validation): correct the UIBean hook point and control-t…
lukaszlenart a365be8
WW-5695 docs(validation): add the 7.4.0 implementation plan
lukaszlenart 81db2b2
WW-5694 refactor(validation): deprecate JavaScript client-side valida…
lukaszlenart 0e8b4b3
WW-5694 docs(validation): scope the deprecation banner to the validat…
lukaszlenart 01417ea
WW-5695 feat(components): add HtmlControlType
lukaszlenart 60dfbc7
WW-5695 docs(validation): require the licence header on every new fil…
lukaszlenart 00dd06e
WW-5695 chore(components): add the missing licence header to HtmlCont…
lukaszlenart c13202c
WW-5695 feat(components): add ECMAScript-safe regex detection
lukaszlenart 07dbf7b
WW-5695 fix(validation): drop \s and \S from the ECMAScript-safe esca…
lukaszlenart c67aa67
WW-5695 fix(components): drop \s and \S from the portable-escape allo…
lukaszlenart e9b8fb4
WW-5695 feat(components): map validators onto HTML5 constraint attrib…
lukaszlenart fcdc518
WW-5695 fix(components): close three false-reject and injection gaps …
lukaszlenart 57c92f3
WW-5695 feat(components): add Form.getFieldValidators with per-render…
lukaszlenart 88162c3
WW-5695 docs(validation): make the plan's memoisation test actually t…
lukaszlenart ee8c25a
WW-5695 test(components): make FormFieldValidatorsTest prove memoisation
lukaszlenart 2292e92
WW-5695 feat(components): resolve the HTML control type per component
lukaszlenart 6eec8eb
WW-5695 feat(components): derive constraint attributes during tag eva…
lukaszlenart 584f406
WW-5695 test(components): pin the constraint hook's evaluateExtraPara…
lukaszlenart 79e2772
WW-5695 feat(html5): render derived constraint attributes
lukaszlenart d0d0ce4
WW-5695 docs(validation): drop the ?html builtin from the template gu…
lukaszlenart febd4c6
WW-5695 fix(html5): stop constraints.ftl leaking a newline into every…
lukaszlenart 1245212
WW-5695 fix(components): close false-reject and duplicate-attribute g…
lukaszlenart 184006c
WW-5695 docs(design): correct the spec and plan to match the post-fix…
lukaszlenart 1cf6198
WW-5695 refactor(components): address SonarQube findings
lukaszlenart 2c93a96
WW-5695 fix(components): correct three constraint-derivation defects …
lukaszlenart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
core/src/main/java/org/apache/struts2/components/EcmaScriptSafeRegex.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.apache.struts2.components; | ||
|
|
||
| /** | ||
| * Decides whether a Java regular expression can be handed to a browser as an HTML5 {@code pattern} | ||
| * attribute without changing meaning. | ||
| * <p> | ||
| * This is an allowlist by design. A denylist of Java-only constructs would violate the | ||
| * never-false-reject rule the first time it missed one, because a missed construct becomes a pattern | ||
| * the browser interprets differently and the user cannot get past. Anything not provably common to | ||
| * both engines is rejected, and the field simply gets no client-side check. | ||
| * | ||
| * @since 7.4.0 | ||
| */ | ||
| public final class EcmaScriptSafeRegex { | ||
|
|
||
| /** | ||
| * Escapes with identical meaning in both engines. | ||
| * <p> | ||
| * {@code \s} and {@code \S} are deliberately absent. Java's {@code \s} is ASCII-only by default | ||
| * while ECMAScript's is the wider Unicode set, so {@code ^\S+$} accepts a value containing NBSP | ||
| * on the server and rejects it in the browser. {@code \d} and {@code \w} are safe — both engines | ||
| * are ASCII-only for those, and JavaScript never widens them. | ||
| * <p> | ||
| * {@code \b} and {@code \B} are absent for a sharper reason: their meaning is not even stable | ||
| * across the JDKs Struts supports. Up to Java 18 the boundary was decided by | ||
| * {@code Character.isLetterOrDigit}, making it Unicode-aware while {@code \w} stayed ASCII; | ||
| * JDK 19 resolved that inconsistency. So {@code ^\bäiti\b$} matches {@code äiti} on Java 17 and | ||
| * not on Java 21, while ECMAScript — whose boundary is always ASCII-word based — rejects it in | ||
| * every browser. On the Java 17 baseline that is a false reject, and no version check could fix | ||
| * it: one {@code validation.xml} would have to mean two different things depending on the JVM. | ||
| */ | ||
| private static final String ALLOWED_ESCAPES = "dDwWnrtf\\.*+?()[]{}|^$/-"; | ||
|
|
||
| private EcmaScriptSafeRegex() { | ||
| } | ||
|
|
||
| public static boolean isSafe(String regex) { | ||
| if (regex == null || regex.isEmpty()) { | ||
| return false; | ||
| } | ||
| boolean inCharClass = false; | ||
| int i = 0; | ||
| while (i < regex.length()) { | ||
| char current = regex.charAt(i); | ||
| if (!isPortable(regex, i, current, inCharClass)) { | ||
| return false; | ||
| } | ||
| if (current == '[') { | ||
| inCharClass = true; | ||
| } else if (current == ']') { | ||
| inCharClass = false; | ||
| } | ||
| // an escape consumes the character it escapes, which must not be scanned again | ||
| i += (current == '\\') ? 2 : 1; | ||
| } | ||
| return !inCharClass; | ||
| } | ||
|
|
||
| /** | ||
| * Whether the construct starting at {@code index} means the same thing to both engines. This is | ||
| * the whole allowlist: anything that reaches {@code default} is a character with no special | ||
| * meaning in either engine, or one whose meaning is shared. | ||
| */ | ||
| private static boolean isPortable(String regex, int index, char current, boolean inCharClass) { | ||
| switch (current) { | ||
| case '\\': | ||
| return isAllowedEscape(regex, index); | ||
| case '[': | ||
| // Java allows nested classes and POSIX names; ECMAScript allows neither | ||
| return !inCharClass && !regex.startsWith("[:", index); | ||
| case '&': | ||
| // Java character-class intersection | ||
| return !inCharClass || !isFollowedBy(regex, index, '&'); | ||
| case '(': | ||
| return isPortableGroup(regex, index); | ||
| case '*', '+', '?', '}': | ||
| // possessive quantifier | ||
| return !isFollowedBy(regex, index, '+'); | ||
| default: | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| private static boolean isAllowedEscape(String regex, int index) { | ||
| return index + 1 < regex.length() && ALLOWED_ESCAPES.indexOf(regex.charAt(index + 1)) >= 0; | ||
| } | ||
|
|
||
| /** | ||
| * Only non-capturing groups and lookahead are portable; named groups, lookbehind, atomic groups | ||
| * and inline flags are not. A plain capturing group is always fine. | ||
| */ | ||
| private static boolean isPortableGroup(String regex, int index) { | ||
| if (!isFollowedBy(regex, index, '?')) { | ||
| return true; | ||
| } | ||
| if (index + 2 >= regex.length()) { | ||
| return false; | ||
| } | ||
| char kind = regex.charAt(index + 2); | ||
| return kind == ':' || kind == '=' || kind == '!'; | ||
| } | ||
|
|
||
| private static boolean isFollowedBy(String regex, int index, char expected) { | ||
| return index + 1 < regex.length() && regex.charAt(index + 1) == expected; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
core/src/main/java/org/apache/struts2/components/HtmlConstraintProvider.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one | ||
| * or more contributor license agreements. See the NOTICE file | ||
| * distributed with this work for additional information | ||
| * regarding copyright ownership. The ASF licenses this file | ||
| * to you under the Apache License, Version 2.0 (the | ||
| * "License"); you may not use this file except in compliance | ||
| * with the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, | ||
| * software distributed under the License is distributed on an | ||
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| * KIND, either express or implied. See the License for the | ||
| * specific language governing permissions and limitations | ||
| * under the License. | ||
| */ | ||
| package org.apache.struts2.components; | ||
|
|
||
| import org.apache.struts2.validator.Validator; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Maps a field's validators onto the HTML attributes a theme should render for it. | ||
| * <p> | ||
| * The default implementation is deliberately conservative — see {@link StrutsHtmlConstraintProvider}. | ||
| * Applications wanting a best-effort mapping (an {@code email} validator becoming | ||
| * {@code type="email"}, say) should register their own implementation instead. | ||
| * | ||
| * @since 7.4.0 | ||
| */ | ||
| public interface HtmlConstraintProvider { | ||
|
|
||
| /** | ||
| * @param validators the field's validators; may be null or empty | ||
| * @param control the kind of control being rendered | ||
| * @param action the action instance, used to resolve i18n validator messages; may be null | ||
| * @return attribute name to value; never null, possibly empty | ||
| */ | ||
| Map<String, String> constraintsFor(List<Validator> validators, HtmlControlType control, Object action); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.