1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit.html;
16
17 import static org.htmlunit.BrowserVersionFeatures.FORM_IGNORE_REL_NOREFERRER;
18 import static org.htmlunit.BrowserVersionFeatures.FORM_SUBMISSION_HEADER_CACHE_CONTROL_MAX_AGE;
19
20 import java.net.MalformedURLException;
21 import java.net.URL;
22 import java.nio.charset.Charset;
23 import java.nio.charset.StandardCharsets;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.Objects;
33 import java.util.function.Predicate;
34 import java.util.regex.Pattern;
35
36 import org.apache.commons.logging.Log;
37 import org.apache.commons.logging.LogFactory;
38 import org.htmlunit.BrowserVersion;
39 import org.htmlunit.ElementNotFoundException;
40 import org.htmlunit.FormEncodingType;
41 import org.htmlunit.HttpHeader;
42 import org.htmlunit.HttpMethod;
43 import org.htmlunit.Page;
44 import org.htmlunit.ScriptResult;
45 import org.htmlunit.SgmlPage;
46 import org.htmlunit.WebAssert;
47 import org.htmlunit.WebClient;
48 import org.htmlunit.WebRequest;
49 import org.htmlunit.WebWindow;
50 import org.htmlunit.http.HttpUtils;
51 import org.htmlunit.javascript.host.event.Event;
52 import org.htmlunit.javascript.host.event.SubmitEvent;
53 import org.htmlunit.protocol.javascript.JavaScriptURLConnection;
54 import org.htmlunit.util.ArrayUtils;
55 import org.htmlunit.util.EncodingSniffer;
56 import org.htmlunit.util.NameValuePair;
57 import org.htmlunit.util.StringUtils;
58 import org.htmlunit.util.UrlUtils;
59
60 /**
61 * Wrapper for the HTML element "form".
62 *
63 * @author Mike Bowler
64 * @author David K. Taylor
65 * @author Brad Clarke
66 * @author Christian Sell
67 * @author Marc Guillemot
68 * @author George Murnock
69 * @author Kent Tong
70 * @author Ahmed Ashour
71 * @author Philip Graf
72 * @author Ronald Brill
73 * @author Frank Danek
74 * @author Anton Demydenko
75 * @author Lai Quang Duong
76 */
77 public class HtmlForm extends HtmlElement {
78 private static final Log LOG = LogFactory.getLog(HtmlForm.class);
79
80 /** The HTML tag represented by this element. */
81 public static final String TAG_NAME = "form";
82
83 /** The "novalidate" attribute name. */
84 private static final String ATTRIBUTE_NOVALIDATE = "novalidate";
85
86 /** The "formnovalidate" attribute name. */
87 public static final String ATTRIBUTE_FORMNOVALIDATE = "formnovalidate";
88
89 private static final HashSet<String> SUBMITTABLE_TAG_NAMES = new HashSet<>(Arrays.asList(HtmlInput.TAG_NAME,
90 HtmlButton.TAG_NAME, HtmlSelect.TAG_NAME, HtmlTextArea.TAG_NAME));
91
92 private static final Pattern SUBMIT_CHARSET_PATTERN = Pattern.compile("[ ,].*");
93
94 private boolean isPreventDefault_;
95
96 /**
97 * A map that holds past names (name or id attribute) to elements belonging to this form.
98 * @see <a href="https://html.spec.whatwg.org/multipage/forms.html#the-form-element:the-form-element-10">
99 * HTML spec - past names map</a>
100 */
101 private Map<String, HtmlElement> pastNamesMap_;
102
103 /**
104 * Creates an instance.
105 *
106 * @param qualifiedName the qualified name of the element type to instantiate
107 * @param htmlPage the page that contains this element
108 * @param attributes the initial attributes
109 */
110 HtmlForm(final String qualifiedName, final SgmlPage htmlPage,
111 final Map<String, DomAttr> attributes) {
112 super(qualifiedName, htmlPage, attributes);
113 }
114
115 /**
116 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
117 *
118 * <p>Submits this form to the server. If <code>submitElement</code> is {@code null}, then
119 * the submission is treated as if it was triggered by JavaScript, and the <code>onsubmit</code>
120 * handler will not be executed.</p>
121 *
122 * <p><b>IMPORTANT:</b> Using this method directly is not the preferred way of submitting forms.
123 * Most consumers should emulate the user's actions instead, probably by using something like
124 * {@link HtmlElement#click()} or {@link HtmlElement#dblClick()}.</p>
125 *
126 * @param submitElement the element that caused the submit to occur
127 */
128 public void submit(final SubmittableElement submitElement) {
129 final HtmlPage htmlPage = (HtmlPage) getPage();
130 final WebClient webClient = htmlPage.getWebClient();
131
132 if (webClient.isJavaScriptEnabled()) {
133 if (submitElement != null) {
134 isPreventDefault_ = false;
135
136 boolean validate = true;
137 if (submitElement instanceof HtmlSubmitInput input
138 && input.isFormNoValidate()) {
139 validate = false;
140 }
141 else if (submitElement instanceof HtmlButton htmlButton) {
142 if ("submit".equalsIgnoreCase(htmlButton.getType())
143 && htmlButton.isFormNoValidate()) {
144 validate = false;
145 }
146 }
147
148 if (validate
149 && getAttributeDirect(ATTRIBUTE_NOVALIDATE) != ATTRIBUTE_NOT_DEFINED) {
150 validate = false;
151 }
152
153 if (validate && !areChildrenValid()) {
154 return;
155 }
156 final ScriptResult scriptResult = fireEvent(new SubmitEvent(this,
157 ((HtmlElement) submitElement).getScriptableObject()));
158 if (isPreventDefault_) {
159 // null means 'nothing executed'
160 if (scriptResult == null) {
161 return;
162 }
163 return;
164 }
165 }
166
167 final String action = getActionAttribute().trim();
168 if (StringUtils.startsWithIgnoreCase(action, JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
169 htmlPage.executeJavaScript(action, "Form action", getStartLineNumber());
170 return;
171 }
172 }
173 else {
174 if (StringUtils.startsWithIgnoreCase(getActionAttribute(), JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
175 // The action is JavaScript but JavaScript isn't enabled.
176 return;
177 }
178 }
179
180 // html5 attribute's support
181 if (submitElement != null) {
182 updateHtml5Attributes(submitElement);
183 }
184
185 // dialog support
186 final String methodAttribute = getMethodAttribute();
187 if ("dialog".equalsIgnoreCase(methodAttribute)) {
188 // find parent dialog
189 final HtmlElement dialog = getEnclosingElement("dialog");
190 if (dialog != null) {
191 ((HtmlDialog) dialog).close("");
192 }
193 return;
194 }
195
196 final WebRequest request = getWebRequest(submitElement);
197 final String target = htmlPage.getResolvedTarget(getTargetAttribute());
198
199 final WebWindow webWindow = htmlPage.getEnclosingWindow();
200 // Calling form.submit() twice forces double download.
201 webClient.download(webWindow, target, request, false, null, "JS form.submit()");
202 }
203
204 /**
205 * Check if element which cause submit contains new html5 attributes
206 * (formaction, formmethod, formtarget, formenctype)
207 * and override existing values.
208 * @param submitElement the element to update
209 */
210 private void updateHtml5Attributes(final SubmittableElement submitElement) {
211 if (submitElement instanceof HtmlElement element) {
212
213 final String type = element.getAttributeDirect(TYPE_ATTRIBUTE);
214 boolean typeImage = false;
215 final boolean isInput = HtmlInput.TAG_NAME.equals(element.getTagName());
216 if (isInput) {
217 typeImage = "image".equalsIgnoreCase(type);
218 }
219
220 // could be excessive validation but support of html5 fromxxx
221 // attributes available for:
222 // - input with 'submit' and 'image' types
223 // - button with 'submit' or without type
224 final boolean typeSubmit = "submit".equalsIgnoreCase(type);
225 if (isInput && !typeSubmit && !typeImage) {
226 return;
227 }
228 else if (HtmlButton.TAG_NAME.equals(element.getTagName())
229 && !"submit".equals(((HtmlButton) element).getType())) {
230 return;
231 }
232
233 final String formaction = element.getAttributeDirect("formaction");
234 if (ATTRIBUTE_NOT_DEFINED != formaction) {
235 setActionAttribute(formaction);
236 }
237 final String formmethod = element.getAttributeDirect("formmethod");
238 if (ATTRIBUTE_NOT_DEFINED != formmethod) {
239 setMethodAttribute(formmethod);
240 }
241 final String formtarget = element.getAttributeDirect("formtarget");
242 if (ATTRIBUTE_NOT_DEFINED != formtarget) {
243 setTargetAttribute(formtarget);
244 }
245 final String formenctype = element.getAttributeDirect("formenctype");
246 if (ATTRIBUTE_NOT_DEFINED != formenctype) {
247 setEnctypeAttribute(formenctype);
248 }
249 }
250 }
251
252 private boolean areChildrenValid() {
253 boolean valid = true;
254 for (final HtmlElement element : getElements(htmlElement -> htmlElement instanceof HtmlInput)) {
255 if (!element.isValid()) {
256 if (LOG.isInfoEnabled()) {
257 LOG.info("Form validation failed; element '" + element + "' was not valid. Submit cancelled.");
258 }
259 valid = false;
260 break;
261 }
262 }
263 return valid;
264 }
265
266 /**
267 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
268 *
269 * Gets the request for a submission of this form with the specified SubmittableElement.
270 * @param submitElement the element that caused the submit to occur
271 * @return the request
272 */
273 public WebRequest getWebRequest(final SubmittableElement submitElement) {
274 final HttpMethod method;
275 final String methodAttribute = getMethodAttribute();
276 if ("post".equalsIgnoreCase(methodAttribute)) {
277 method = HttpMethod.POST;
278 }
279 else {
280 if (!"get".equalsIgnoreCase(methodAttribute) && StringUtils.isNotBlank(methodAttribute)) {
281 notifyIncorrectness("Incorrect submit method >" + getMethodAttribute() + "<. Using >GET<.");
282 }
283 method = HttpMethod.GET;
284 }
285
286 String actionUrl = getActionAttribute();
287 String anchor = null;
288 String queryFormFields = "";
289 Charset enc = getSubmitCharset();
290 if (StandardCharsets.UTF_16 == enc
291 || StandardCharsets.UTF_16BE == enc
292 || StandardCharsets.UTF_16LE == enc) {
293 enc = StandardCharsets.UTF_8;
294 }
295
296 final List<NameValuePair> parameters = getParameterListForSubmit(submitElement);
297 if (HttpMethod.GET == method) {
298 if (actionUrl.contains("#")) {
299 anchor = StringUtils.substringAfter(actionUrl, "#");
300 }
301 queryFormFields = HttpUtils.toQueryFormFields(parameters, enc);
302
303 // action may already contain some query parameters: they have to be removed
304 actionUrl = StringUtils.substringBefore(actionUrl, "#");
305 actionUrl = StringUtils.substringBefore(actionUrl, "?");
306 parameters.clear(); // parameters have been added to query
307 }
308
309 final HtmlPage htmlPage = (HtmlPage) getPage();
310 URL url;
311 try {
312 if (actionUrl.isEmpty()) {
313 url = WebClient.expandUrl(htmlPage.getUrl(), actionUrl);
314 }
315 else {
316 url = htmlPage.getFullyQualifiedUrl(actionUrl);
317 }
318
319 if (!queryFormFields.isEmpty()) {
320 url = UrlUtils.getUrlWithNewQuery(url, queryFormFields);
321 }
322
323 if (anchor != null && UrlUtils.URL_ABOUT_BLANK != url) {
324 url = UrlUtils.getUrlWithNewRef(url, anchor);
325 }
326 }
327 catch (final MalformedURLException e) {
328 throw new IllegalArgumentException("Not a valid url: " + actionUrl, e);
329 }
330
331 final BrowserVersion browser = htmlPage.getWebClient().getBrowserVersion();
332 final WebRequest request = new WebRequest(url, browser.getHtmlAcceptHeader(),
333 browser.getAcceptEncodingHeader());
334 request.setHttpMethod(method);
335 request.setRequestParameters(parameters);
336 if (HttpMethod.POST == method) {
337 request.setEncodingType(FormEncodingType.getInstance(getEnctypeAttribute()));
338
339 if (browser.hasFeature(FORM_SUBMISSION_HEADER_CACHE_CONTROL_MAX_AGE)) {
340 request.setAdditionalHeader(HttpHeader.CACHE_CONTROL, "max-age=0");
341 }
342
343 try {
344 request.setAdditionalHeader(HttpHeader.ORIGIN,
345 UrlUtils.getUrlWithProtocolAndAuthority(htmlPage.getUrl()).toExternalForm());
346 }
347 catch (final MalformedURLException e) {
348 if (LOG.isInfoEnabled()) {
349 LOG.info("Invalid origin url '" + htmlPage.getUrl() + "'");
350 }
351 }
352 }
353 request.setCharset(enc);
354
355 // Sec-Fetch-* support (https://www.w3.org/TR/fetch-metadata/):
356 // a form submission is a top-level navigation, initiated by the page
357 // containing the form. The initiator must be set regardless of
358 // "noreferrer", since Sec-Fetch-Site reflects the true relationship
359 // between initiator and target even when the Referer header itself is
360 // suppressed. Unlike anchor clicks, this method actually knows whether
361 // the submission was caused by a real submit control (submitElement != null,
362 // e.g. a click on a submit button/input) or triggered purely by script
363 // (submitElement == null, e.g. form.submit()) - see this method's javadoc -
364 // so Sec-Fetch-User can be set correctly rather than hardcoded.
365 request.markAsNavigation(htmlPage.getUrl(), submitElement != null);
366
367 // forms are ignoring the rel='noreferrer'
368 if (browser.hasFeature(FORM_IGNORE_REL_NOREFERRER) || !relContainsNoreferrer()) {
369 request.setRefererHeader(htmlPage.getUrl());
370 }
371
372 return request;
373 }
374
375 private boolean relContainsNoreferrer() {
376 String rel = getRelAttribute();
377 if (rel != null) {
378 rel = rel.toLowerCase(Locale.ROOT);
379 return ArrayUtils.contains(StringUtils.splitAtBlank(rel), "noreferrer");
380 }
381 return false;
382 }
383
384 /**
385 * Returns the charset to use for the form submission. This is the first one
386 * from the list provided in {@link #getAcceptCharsetAttribute()} if any
387 * or the page's charset else
388 * @return the charset to use for the form submission
389 */
390 private Charset getSubmitCharset() {
391 String charset = getAcceptCharsetAttribute();
392 if (!charset.isEmpty()) {
393 charset = charset.trim();
394 return EncodingSniffer.toCharset(
395 SUBMIT_CHARSET_PATTERN.matcher(charset).replaceAll("").toUpperCase(Locale.ROOT));
396 }
397 return getPage().getCharset();
398 }
399
400 /**
401 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
402 *
403 * Returns a list of {@link NameValuePair}s that represent the data that will be
404 * sent to the server when this form is submitted. This is primarily intended to aid
405 * debugging.
406 *
407 * @param submitElement the element used to submit the form, or {@code null} if the
408 * form was submitted by JavaScript
409 * @return the list of {@link NameValuePair}s that represent that data that will be sent
410 * to the server when this form is submitted
411 */
412 public List<NameValuePair> getParameterListForSubmit(final SubmittableElement submitElement) {
413 final Collection<SubmittableElement> submittableElements = getSubmittableElements(submitElement);
414
415 final List<NameValuePair> parameterList = new ArrayList<>(submittableElements.size());
416 for (final SubmittableElement element : submittableElements) {
417 parameterList.addAll(Arrays.asList(element.getSubmitNameValuePairs()));
418 }
419
420 return parameterList;
421 }
422
423 /**
424 * Resets this form to its initial values, returning the page contained by this form's window after the
425 * reset. Note that the returned page may or may not be the same as the original page, based on JavaScript
426 * event handlers, etc.
427 *
428 * @return the page contained by this form's window after the reset
429 */
430 public Page reset() {
431 final SgmlPage sgmlPage = getPage();
432 final ScriptResult scriptResult = fireEvent(Event.TYPE_RESET);
433 if (ScriptResult.isFalse(scriptResult)) {
434 return sgmlPage.getWebClient().getCurrentWindow().getEnclosedPage();
435 }
436
437 for (final HtmlElement next : getHtmlElementDescendants()) {
438 if (next instanceof SubmittableElement element) {
439 element.reset();
440 }
441 }
442
443 return sgmlPage;
444 }
445
446 /**
447 * {@inheritDoc}
448 */
449 @Override
450 public boolean isValid() {
451 for (final HtmlElement element : getFormElements()) {
452 if (!element.isValid()) {
453 return false;
454 }
455 }
456 return super.isValid();
457 }
458
459 /**
460 * Returns a collection of elements that represent all the "submittable" elements in this form,
461 * assuming that the specified element is used to submit the form.
462 *
463 * @param submitElement the element used to submit the form, or {@code null} if the
464 * form is submitted by JavaScript
465 * @return a collection of elements that represent all the "submittable" elements in this form
466 */
467 Collection<SubmittableElement> getSubmittableElements(final SubmittableElement submitElement) {
468 final List<SubmittableElement> submittableElements = new ArrayList<>();
469
470 for (final HtmlElement element : getElements(htmlElement -> isSubmittable(htmlElement, submitElement))) {
471 submittableElements.add((SubmittableElement) element);
472 }
473
474 return submittableElements;
475 }
476
477 private static boolean isValidForSubmission(final HtmlElement element, final SubmittableElement submitElement) {
478 final String tagName = element.getTagName();
479 if (!SUBMITTABLE_TAG_NAMES.contains(tagName)) {
480 return false;
481 }
482 if (element.isDisabledElementAndDisabled()) {
483 return false;
484 }
485 // clicked input type="image" is submitted even if it hasn't a name
486 if (element == submitElement && element instanceof HtmlImageInput) {
487 return true;
488 }
489
490 if (!element.hasAttribute(NAME_ATTRIBUTE)) {
491 return false;
492 }
493
494 if (StringUtils.isEmptyString(element.getAttributeDirect(NAME_ATTRIBUTE))) {
495 return false;
496 }
497
498 if (element instanceof HtmlInput input) {
499 if (input.isCheckable()) {
500 return input.isChecked();
501 }
502 }
503 if (element instanceof HtmlSelect select) {
504 return select.isValidForSubmission();
505 }
506 return true;
507 }
508
509 /**
510 * Returns {@code true} if the specified element gets submitted when this form is submitted,
511 * assuming that the form is submitted using the specified submit element.
512 *
513 * @param element the element to check
514 * @param submitElement the element used to submit the form, or {@code null} if the form is
515 * submitted by JavaScript
516 * @return {@code true} if the specified element gets submitted when this form is submitted
517 */
518 private static boolean isSubmittable(final HtmlElement element, final SubmittableElement submitElement) {
519 if (!isValidForSubmission(element, submitElement)) {
520 return false;
521 }
522
523 // The one submit button that was clicked can be submitted but no other ones
524 if (element == submitElement) {
525 return true;
526 }
527 if (element instanceof HtmlInput input) {
528 if (!input.isSubmitable()) {
529 return false;
530 }
531 }
532
533 return !HtmlButton.TAG_NAME.equals(element.getTagName());
534 }
535
536 /**
537 * Returns all input elements which are members of this form and have the specified name.
538 *
539 * @param name the input name to search for
540 * @return all input elements which are members of this form and have the specified name
541 */
542 public List<HtmlInput> getInputsByName(final String name) {
543 return getFormElementsByAttribute(HtmlInput.TAG_NAME, NAME_ATTRIBUTE, name);
544 }
545
546 /**
547 * Same as {@link #getElementsByAttribute(String, String, String)} but
548 * ignoring elements that are contained in a nested form.
549 */
550 @SuppressWarnings("unchecked")
551 private <E extends HtmlElement> List<E> getFormElementsByAttribute(
552 final String elementName,
553 final String attributeName,
554 final String attributeValue) {
555
556 return (List<E>) getElements(htmlElement ->
557 htmlElement.getTagName().equals(elementName)
558 && htmlElement.getAttribute(attributeName).equals(attributeValue));
559 }
560
561 /**
562 * Returns the form controls contained in this form.
563 *
564 * @return a list containing all form controls in tree order (preorder,
565 * depth-first traversal). Only the following elements are
566 * included: {@code button}, {@code fieldset}, {@code input},
567 * {@code object}, {@code output}, {@code select}, and
568 * {@code textarea}
569 */
570 public List<HtmlElement> getFormElements() {
571 return getElements(htmlElement -> {
572 final String tagName = htmlElement.getTagName();
573 return HtmlButton.TAG_NAME.equals(tagName)
574 || HtmlFieldSet.TAG_NAME.equals(tagName)
575 || HtmlInput.TAG_NAME.equals(tagName)
576 || HtmlObject.TAG_NAME.equals(tagName)
577 || HtmlOutput.TAG_NAME.equals(tagName)
578 || HtmlSelect.TAG_NAME.equals(tagName)
579 || HtmlTextArea.TAG_NAME.equals(tagName);
580 });
581 }
582
583 /**
584 * This is the backend for the getElements() javascript function of the form.
585 * see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
586 *
587 * @return A List containing all non-image controls in the form.
588 * The form controls in the returned collection are in the same order
589 * in which they appear in the form by following a preorder,
590 * depth-first traversal of the tree. This is called tree order.
591 * Only the following elements are returned:
592 * button, fieldset,
593 * input (with the exception that any whose type is "image" are omitted for historical reasons),
594 * object, output, select, textarea.
595 */
596 public List<HtmlElement> getElementsJS() {
597 return getElements(htmlElement -> {
598 final String tagName = htmlElement.getTagName();
599 if (HtmlInput.TAG_NAME.equals(tagName)) {
600 return !(htmlElement instanceof HtmlImageInput);
601 }
602
603 return HtmlButton.TAG_NAME.equals(tagName)
604 || HtmlFieldSet.TAG_NAME.equals(tagName)
605 || HtmlObject.TAG_NAME.equals(tagName)
606 || HtmlOutput.TAG_NAME.equals(tagName)
607 || HtmlSelect.TAG_NAME.equals(tagName)
608 || HtmlTextArea.TAG_NAME.equals(tagName);
609 });
610 }
611
612 /**
613 * Returns the form elements that match the specified filter.
614 *
615 * @param filter the predicate used to select elements
616 * @return a list of form elements matching the specified filter
617 */
618 public List<HtmlElement> getElements(final Predicate<HtmlElement> filter) {
619 final List<HtmlElement> elements = new ArrayList<>();
620
621 if (isAttachedToPage()) {
622 for (final HtmlElement element : getPage().getDocumentElement().getHtmlElementDescendants()) {
623 if (filter.test(element)
624 && element.getEnclosingForm() == this) {
625 elements.add(element);
626 }
627 }
628 }
629 else {
630 for (final HtmlElement element : getHtmlElementDescendants()) {
631 if (filter.test(element)) {
632 elements.add(element);
633 }
634 }
635 }
636
637 return elements;
638 }
639
640 /**
641 * Returns the first input element which is a member of this form and has the specified name.
642 *
643 * @param name the input name to search for
644 * @param <I> the input type
645 * @return the first input element which is a member of this form and has the specified name
646 * @throws ElementNotFoundException if there is no input in this form with the specified name
647 */
648 @SuppressWarnings("unchecked")
649 public final <I extends HtmlInput> I getInputByName(final String name) throws ElementNotFoundException {
650 final List<HtmlInput> inputs = getInputsByName(name);
651
652 if (inputs.isEmpty()) {
653 throw new ElementNotFoundException(HtmlInput.TAG_NAME, NAME_ATTRIBUTE, name);
654 }
655 return (I) inputs.get(0);
656 }
657
658 /**
659 * Returns all the {@link HtmlSelect} elements in this form that have the specified name.
660 *
661 * @param name the name to search for
662 * @return all the {@link HtmlSelect} elements in this form that have the specified name
663 */
664 public List<HtmlSelect> getSelectsByName(final String name) {
665 return getFormElementsByAttribute(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
666 }
667
668 /**
669 * Returns the first {@link HtmlSelect} element in this form that has the specified name.
670 *
671 * @param name the name to search for
672 * @return the first {@link HtmlSelect} element in this form that has the specified name
673 * @throws ElementNotFoundException if this form does not contain a {@link HtmlSelect}
674 * element with the specified name
675 */
676 public HtmlSelect getSelectByName(final String name) throws ElementNotFoundException {
677 final List<HtmlSelect> list = getSelectsByName(name);
678 if (list.isEmpty()) {
679 throw new ElementNotFoundException(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
680 }
681 return list.get(0);
682 }
683
684 /**
685 * Returns all the {@link HtmlButton} elements in this form that have the specified name.
686 *
687 * @param name the name to search for
688 * @return all the {@link HtmlButton} elements in this form that have the specified name
689 */
690 public List<HtmlButton> getButtonsByName(final String name) {
691 return getFormElementsByAttribute(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
692 }
693
694 /**
695 * Returns the first {@link HtmlButton} element in this form that has the specified name.
696 *
697 * @param name the name to search for
698 * @return the first {@link HtmlButton} element in this form that has the specified name
699 * @throws ElementNotFoundException if this form does not contain a {@link HtmlButton}
700 * element with the specified name
701 */
702 public HtmlButton getButtonByName(final String name) throws ElementNotFoundException {
703 final List<HtmlButton> list = getButtonsByName(name);
704 if (list.isEmpty()) {
705 throw new ElementNotFoundException(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
706 }
707 return list.get(0);
708 }
709
710 /**
711 * Returns all the {@link HtmlTextArea} elements in this form that have the specified name.
712 *
713 * @param name the name to search for
714 * @return all the {@link HtmlTextArea} elements in this form that have the specified name
715 */
716 public List<HtmlTextArea> getTextAreasByName(final String name) {
717 return getFormElementsByAttribute(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
718 }
719
720 /**
721 * Returns the first {@link HtmlTextArea} element in this form that has the specified name.
722 *
723 * @param name the name to search for
724 * @return the first {@link HtmlTextArea} element in this form that has the specified name
725 * @throws ElementNotFoundException if this form does not contain a {@link HtmlTextArea}
726 * element with the specified name
727 */
728 public HtmlTextArea getTextAreaByName(final String name) throws ElementNotFoundException {
729 final List<HtmlTextArea> list = getTextAreasByName(name);
730 if (list.isEmpty()) {
731 throw new ElementNotFoundException(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
732 }
733 return list.get(0);
734 }
735
736 /**
737 * Returns all the {@link HtmlRadioButtonInput} elements in this form that have the specified name.
738 *
739 * @param name the name to search for
740 * @return all the {@link HtmlRadioButtonInput} elements in this form that have the specified name
741 */
742 public List<HtmlRadioButtonInput> getRadioButtonsByName(final String name) {
743 WebAssert.notNull("name", name);
744
745 final List<HtmlRadioButtonInput> results = new ArrayList<>();
746
747 for (final HtmlElement element : getInputsByName(name)) {
748 if (element instanceof HtmlRadioButtonInput input) {
749 results.add(input);
750 }
751 }
752
753 return results;
754 }
755
756 /**
757 * Selects the specified radio button in the form. Only a radio button that is actually contained
758 * in the form can be selected.
759 *
760 * @param radioButtonInput the radio button to select
761 */
762 void setCheckedRadioButton(final HtmlRadioButtonInput radioButtonInput) {
763 if (radioButtonInput.getEnclosingForm() == null) {
764 throw new IllegalArgumentException("HtmlRadioButtonInput is not child of this HtmlForm");
765 }
766 final List<HtmlRadioButtonInput> radios = getRadioButtonsByName(radioButtonInput.getNameAttribute());
767
768 for (final HtmlRadioButtonInput input : radios) {
769 input.setCheckedInternal(input == radioButtonInput);
770 }
771 }
772
773 /**
774 * Returns the first checked radio button with the specified name. If none of
775 * the radio buttons by that name are checked, this method returns {@code null}.
776 *
777 * @param name the name of the radio button
778 * @return the first checked radio button with the specified name
779 */
780 public HtmlRadioButtonInput getCheckedRadioButton(final String name) {
781 WebAssert.notNull("name", name);
782
783 for (final HtmlRadioButtonInput input : getRadioButtonsByName(name)) {
784 if (input.isChecked()) {
785 return input;
786 }
787 }
788 return null;
789 }
790
791 /**
792 * Returns the value of the attribute {@code action}. Refer to the <a
793 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
794 * details on the use of this attribute.
795 *
796 * @return the value of the attribute {@code action} or an empty string if that attribute isn't defined
797 */
798 public final String getActionAttribute() {
799 return getAttributeDirect("action");
800 }
801
802 /**
803 * Sets the value of the attribute {@code action}. Refer to the <a
804 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
805 * details on the use of this attribute.
806 *
807 * @param action the value of the attribute {@code action}
808 */
809 public final void setActionAttribute(final String action) {
810 setAttribute("action", action);
811 }
812
813 /**
814 * Returns the value of the attribute {@code method}. Refer to the <a
815 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
816 * details on the use of this attribute.
817 *
818 * @return the value of the attribute {@code method} or an empty string if that attribute isn't defined
819 */
820 public final String getMethodAttribute() {
821 return getAttributeDirect("method");
822 }
823
824 /**
825 * Sets the value of the attribute {@code method}. Refer to the <a
826 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
827 * details on the use of this attribute.
828 *
829 * @param method the value of the attribute {@code method}
830 */
831 public final void setMethodAttribute(final String method) {
832 setAttribute("method", method);
833 }
834
835 /**
836 * Returns the value of the attribute {@code name}. Refer to the <a
837 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
838 * details on the use of this attribute.
839 *
840 * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
841 */
842 public final String getNameAttribute() {
843 return getAttributeDirect(NAME_ATTRIBUTE);
844 }
845
846 /**
847 * Sets the value of the attribute {@code name}. Refer to the <a
848 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
849 * details on the use of this attribute.
850 *
851 * @param name the value of the attribute {@code name}
852 */
853 public final void setNameAttribute(final String name) {
854 setAttribute(NAME_ATTRIBUTE, name);
855 }
856
857 /**
858 * Returns the value of the attribute {@code enctype}. Refer to the <a
859 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
860 * details on the use of this attribute. "Enctype" is the encoding type
861 * used when submitting a form back to the server.
862 *
863 * @return the value of the attribute {@code enctype} or an empty string if that attribute isn't defined
864 */
865 public final String getEnctypeAttribute() {
866 return getAttributeDirect("enctype");
867 }
868
869 /**
870 * Sets the value of the attribute {@code enctype}. Refer to the <a
871 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
872 * details on the use of this attribute. "Enctype" is the encoding type
873 * used when submitting a form back to the server.
874 *
875 * @param encoding the value of the attribute {@code enctype}
876 */
877 public final void setEnctypeAttribute(final String encoding) {
878 setAttribute("enctype", encoding);
879 }
880
881 /**
882 * Returns the value of the attribute {@code onsubmit}. Refer to the <a
883 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
884 * details on the use of this attribute.
885 *
886 * @return the value of the attribute {@code onsubmit} or an empty string if that attribute isn't defined
887 */
888 public final String getOnSubmitAttribute() {
889 return getAttributeDirect("onsubmit");
890 }
891
892 /**
893 * Returns the value of the attribute {@code onreset}. Refer to the <a
894 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
895 * details on the use of this attribute.
896 *
897 * @return the value of the attribute {@code onreset} or an empty string if that attribute isn't defined
898 */
899 public final String getOnResetAttribute() {
900 return getAttributeDirect("onreset");
901 }
902
903 /**
904 * Returns the value of the attribute {@code accept}. Refer to the <a
905 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
906 * details on the use of this attribute.
907 *
908 * @return the value of the attribute {@code accept} or an empty string if that attribute isn't defined
909 */
910 public final String getAcceptAttribute() {
911 return getAttribute(HttpHeader.ACCEPT_LC);
912 }
913
914 /**
915 * Returns the value of the attribute {@code accept-charset}. Refer to the <a
916 * href='http://www.w3.org/TR/html401/interact/forms.html#adef-accept-charset'>
917 * HTML 4.01</a> documentation for details on the use of this attribute.
918 *
919 * @return the value of the attribute {@code accept-charset} or an empty string if that attribute isn't defined
920 */
921 public final String getAcceptCharsetAttribute() {
922 return getAttribute("accept-charset");
923 }
924
925 /**
926 * Returns the value of the attribute {@code target}. Refer to the <a
927 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
928 * details on the use of this attribute.
929 *
930 * @return the value of the attribute {@code target} or an empty string if that attribute isn't defined
931 */
932 public final String getTargetAttribute() {
933 return getAttributeDirect("target");
934 }
935
936 /**
937 * Sets the value of the attribute {@code target}. Refer to the <a
938 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
939 * details on the use of this attribute.
940 *
941 * @param target the value of the attribute {@code target}
942 */
943 public final void setTargetAttribute(final String target) {
944 setAttribute("target", target);
945 }
946
947 /**
948 * Returns the value of the attribute {@code rel}. Refer to the
949 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
950 * documentation for details on the use of this attribute.
951 *
952 * @return the value of the attribute {@code rel} or an empty string if that attribute isn't defined
953 */
954 public final String getRelAttribute() {
955 return getAttributeDirect("rel");
956 }
957
958 /**
959 * Returns the first input in this form with the specified value.
960 * @param value the value to search for
961 * @param <I> the input type
962 * @return the first input in this form with the specified value
963 * @throws ElementNotFoundException if this form does not contain any inputs with the specified value
964 */
965 @SuppressWarnings("unchecked")
966 public <I extends HtmlInput> I getInputByValue(final String value) throws ElementNotFoundException {
967 final List<HtmlInput> list = getInputsByValue(value);
968 if (list.isEmpty()) {
969 throw new ElementNotFoundException(HtmlInput.TAG_NAME, VALUE_ATTRIBUTE, value);
970 }
971 return (I) list.get(0);
972 }
973
974 /**
975 * Returns all the inputs in this form with the specified value.
976 * @param value the value to search for
977 * @return all the inputs in this form with the specified value
978 */
979 public List<HtmlInput> getInputsByValue(final String value) {
980 final List<HtmlInput> results = new ArrayList<>();
981
982 for (final HtmlElement element : getElements(htmlElement -> htmlElement instanceof HtmlInput)) {
983 if (Objects.equals(((HtmlInput) element).getValue(), value)) {
984 results.add((HtmlInput) element);
985 }
986 }
987
988 return results;
989 }
990
991 /**
992 * {@inheritDoc}
993 */
994 @Override
995 protected void preventDefault() {
996 isPreventDefault_ = true;
997 }
998
999 /**
1000 * Browsers have problems with self closing form tags.
1001 */
1002 @Override
1003 protected boolean isEmptyXmlTagExpanded() {
1004 return true;
1005 }
1006
1007 /**
1008 * Returns whether form validation is disabled.
1009 *
1010 * @return {@code true} if the {@code novalidate} attribute is present
1011 */
1012 public final boolean isNoValidate() {
1013 return hasAttribute(ATTRIBUTE_NOVALIDATE);
1014 }
1015
1016 /**
1017 * Sets the value of the attribute {@code novalidate}.
1018 *
1019 * @param noValidate the value of the attribute {@code novalidate}
1020 */
1021 public final void setNoValidate(final boolean noValidate) {
1022 if (noValidate) {
1023 setAttribute(ATTRIBUTE_NOVALIDATE, ATTRIBUTE_NOVALIDATE);
1024 }
1025 else {
1026 removeAttribute(ATTRIBUTE_NOVALIDATE);
1027 }
1028 }
1029
1030 /**
1031 * Register an element to the past names map with the specified name.
1032 * @param name name or id attribute of the element
1033 * @param element the element to register
1034 */
1035 public void registerPastName(final String name, final HtmlElement element) {
1036 if (pastNamesMap_ == null) {
1037 pastNamesMap_ = new HashMap<>();
1038 }
1039 pastNamesMap_.put(name, element);
1040 }
1041
1042 /**
1043 * Return the element registered in the past names map with the specified name.
1044 * If the element is no longer owned by this form, the entry is removed and null is returned.
1045 * @param name name or id attribute of the element
1046 * @return the element, or null if not found or no longer owned by this form
1047 */
1048 public HtmlElement getNamedElement(final String name) {
1049 if (pastNamesMap_ == null) {
1050 return null;
1051 }
1052 final HtmlElement element = pastNamesMap_.get(name);
1053 if (element != null && element.getEnclosingForm() != this) {
1054 pastNamesMap_.remove(name);
1055 return null;
1056 }
1057 return element;
1058 }
1059 }