View Javadoc
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         request.setCharset(enc);
340 
341         // forms are ignoring the rel='noreferrer'
342         if (browser.hasFeature(FORM_IGNORE_REL_NOREFERRER)
343                 || !relContainsNoreferrer()) {
344             request.setRefererHeader(htmlPage.getUrl());
345         }
346 
347         if (HttpMethod.POST == method) {
348             try {
349                 request.setAdditionalHeader(HttpHeader.ORIGIN,
350                         UrlUtils.getUrlWithProtocolAndAuthority(htmlPage.getUrl()).toExternalForm());
351             }
352             catch (final MalformedURLException e) {
353                 if (LOG.isInfoEnabled()) {
354                     LOG.info("Invalid origin url '" + htmlPage.getUrl() + "'");
355                 }
356             }
357         }
358         if (HttpMethod.POST == method) {
359             if (browser.hasFeature(FORM_SUBMISSION_HEADER_CACHE_CONTROL_MAX_AGE)) {
360                 request.setAdditionalHeader(HttpHeader.CACHE_CONTROL, "max-age=0");
361             }
362         }
363 
364         return request;
365     }
366 
367     private boolean relContainsNoreferrer() {
368         String rel = getRelAttribute();
369         if (rel != null) {
370             rel = rel.toLowerCase(Locale.ROOT);
371             return ArrayUtils.contains(StringUtils.splitAtBlank(rel), "noreferrer");
372         }
373         return false;
374     }
375 
376     /**
377      * Returns the charset to use for the form submission. This is the first one
378      * from the list provided in {@link #getAcceptCharsetAttribute()} if any
379      * or the page's charset else
380      * @return the charset to use for the form submission
381      */
382     private Charset getSubmitCharset() {
383         String charset = getAcceptCharsetAttribute();
384         if (!charset.isEmpty()) {
385             charset = charset.trim();
386             return EncodingSniffer.toCharset(
387                     SUBMIT_CHARSET_PATTERN.matcher(charset).replaceAll("").toUpperCase(Locale.ROOT));
388         }
389         return getPage().getCharset();
390     }
391 
392     /**
393      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
394      *
395      * Returns a list of {@link NameValuePair}s that represent the data that will be
396      * sent to the server when this form is submitted. This is primarily intended to aid
397      * debugging.
398      *
399      * @param submitElement the element used to submit the form, or {@code null} if the
400      *        form was submitted by JavaScript
401      * @return the list of {@link NameValuePair}s that represent that data that will be sent
402      *         to the server when this form is submitted
403      */
404     public List<NameValuePair> getParameterListForSubmit(final SubmittableElement submitElement) {
405         final Collection<SubmittableElement> submittableElements = getSubmittableElements(submitElement);
406 
407         final List<NameValuePair> parameterList = new ArrayList<>(submittableElements.size());
408         for (final SubmittableElement element : submittableElements) {
409             parameterList.addAll(Arrays.asList(element.getSubmitNameValuePairs()));
410         }
411 
412         return parameterList;
413     }
414 
415     /**
416      * Resets this form to its initial values, returning the page contained by this form's window after the
417      * reset. Note that the returned page may or may not be the same as the original page, based on JavaScript
418      * event handlers, etc.
419      *
420      * @return the page contained by this form's window after the reset
421      */
422     public Page reset() {
423         final SgmlPage sgmlPage = getPage();
424         final ScriptResult scriptResult = fireEvent(Event.TYPE_RESET);
425         if (ScriptResult.isFalse(scriptResult)) {
426             return sgmlPage.getWebClient().getCurrentWindow().getEnclosedPage();
427         }
428 
429         for (final HtmlElement next : getHtmlElementDescendants()) {
430             if (next instanceof SubmittableElement element) {
431                 element.reset();
432             }
433         }
434 
435         return sgmlPage;
436     }
437 
438     /**
439      * {@inheritDoc}
440      */
441     @Override
442     public boolean isValid() {
443         for (final HtmlElement element : getFormElements()) {
444             if (!element.isValid()) {
445                 return false;
446             }
447         }
448         return super.isValid();
449     }
450 
451     /**
452      * Returns a collection of elements that represent all the "submittable" elements in this form,
453      * assuming that the specified element is used to submit the form.
454      *
455      * @param submitElement the element used to submit the form, or {@code null} if the
456      *        form is submitted by JavaScript
457      * @return a collection of elements that represent all the "submittable" elements in this form
458      */
459     Collection<SubmittableElement> getSubmittableElements(final SubmittableElement submitElement) {
460         final List<SubmittableElement> submittableElements = new ArrayList<>();
461 
462         for (final HtmlElement element : getElements(htmlElement -> isSubmittable(htmlElement, submitElement))) {
463             submittableElements.add((SubmittableElement) element);
464         }
465 
466         return submittableElements;
467     }
468 
469     private static boolean isValidForSubmission(final HtmlElement element, final SubmittableElement submitElement) {
470         final String tagName = element.getTagName();
471         if (!SUBMITTABLE_TAG_NAMES.contains(tagName)) {
472             return false;
473         }
474         if (element.isDisabledElementAndDisabled()) {
475             return false;
476         }
477         // clicked input type="image" is submitted even if it hasn't a name
478         if (element == submitElement && element instanceof HtmlImageInput) {
479             return true;
480         }
481 
482         if (!element.hasAttribute(NAME_ATTRIBUTE)) {
483             return false;
484         }
485 
486         if (StringUtils.isEmptyString(element.getAttributeDirect(NAME_ATTRIBUTE))) {
487             return false;
488         }
489 
490         if (element instanceof HtmlInput input) {
491             if (input.isCheckable()) {
492                 return input.isChecked();
493             }
494         }
495         if (element instanceof HtmlSelect select) {
496             return select.isValidForSubmission();
497         }
498         return true;
499     }
500 
501     /**
502      * Returns {@code true} if the specified element gets submitted when this form is submitted,
503      * assuming that the form is submitted using the specified submit element.
504      *
505      * @param element the element to check
506      * @param submitElement the element used to submit the form, or {@code null} if the form is
507      *        submitted by JavaScript
508      * @return {@code true} if the specified element gets submitted when this form is submitted
509      */
510     private static boolean isSubmittable(final HtmlElement element, final SubmittableElement submitElement) {
511         if (!isValidForSubmission(element, submitElement)) {
512             return false;
513         }
514 
515         // The one submit button that was clicked can be submitted but no other ones
516         if (element == submitElement) {
517             return true;
518         }
519         if (element instanceof HtmlInput input) {
520             if (!input.isSubmitable()) {
521                 return false;
522             }
523         }
524 
525         return !HtmlButton.TAG_NAME.equals(element.getTagName());
526     }
527 
528     /**
529      * Returns all input elements which are members of this form and have the specified name.
530      *
531      * @param name the input name to search for
532      * @return all input elements which are members of this form and have the specified name
533      */
534     public List<HtmlInput> getInputsByName(final String name) {
535         return getFormElementsByAttribute(HtmlInput.TAG_NAME, NAME_ATTRIBUTE, name);
536     }
537 
538     /**
539      * Same as {@link #getElementsByAttribute(String, String, String)} but
540      * ignoring elements that are contained in a nested form.
541      */
542     @SuppressWarnings("unchecked")
543     private <E extends HtmlElement> List<E> getFormElementsByAttribute(
544             final String elementName,
545             final String attributeName,
546             final String attributeValue) {
547 
548         return (List<E>) getElements(htmlElement ->
549                                 htmlElement.getTagName().equals(elementName)
550                                 && htmlElement.getAttribute(attributeName).equals(attributeValue));
551     }
552 
553     /**
554      * @return A List containing all form controls in the form.
555      *         The form controls in the returned collection are in the same order
556      *         in which they appear in the form by following a preorder,
557      *         depth-first traversal of the tree. This is called tree order.
558      *         Only the following elements are returned:
559      *         button, fieldset, input, object, output, select, textarea.
560      */
561     public List<HtmlElement> getFormElements() {
562         return getElements(htmlElement -> {
563             final String tagName = htmlElement.getTagName();
564             return HtmlButton.TAG_NAME.equals(tagName)
565                     || HtmlFieldSet.TAG_NAME.equals(tagName)
566                     || HtmlInput.TAG_NAME.equals(tagName)
567                     || HtmlObject.TAG_NAME.equals(tagName)
568                     || HtmlOutput.TAG_NAME.equals(tagName)
569                     || HtmlSelect.TAG_NAME.equals(tagName)
570                     || HtmlTextArea.TAG_NAME.equals(tagName);
571         });
572     }
573 
574     /**
575      * This is the backend for the getElements() javascript function of the form.
576      * see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
577      *
578      * @return A List containing all non-image controls in the form.
579      *         The form controls in the returned collection are in the same order
580      *         in which they appear in the form by following a preorder,
581      *         depth-first traversal of the tree. This is called tree order.
582      *         Only the following elements are returned:
583      *         button, fieldset,
584      *         input (with the exception that any whose type is "image" are omitted for historical reasons),
585      *         object, output, select, textarea.
586      */
587     public List<HtmlElement> getElementsJS() {
588         return getElements(htmlElement -> {
589             final String tagName = htmlElement.getTagName();
590             if (HtmlInput.TAG_NAME.equals(tagName)) {
591                 return !(htmlElement instanceof HtmlImageInput);
592             }
593 
594             return HtmlButton.TAG_NAME.equals(tagName)
595                     || HtmlFieldSet.TAG_NAME.equals(tagName)
596                     || HtmlObject.TAG_NAME.equals(tagName)
597                     || HtmlOutput.TAG_NAME.equals(tagName)
598                     || HtmlSelect.TAG_NAME.equals(tagName)
599                     || HtmlTextArea.TAG_NAME.equals(tagName);
600         });
601     }
602 
603     /**
604      * @param filter a predicate to filter the element
605      * @return all elements attached to this form and matching the filter predicate
606      */
607     public List<HtmlElement> getElements(final Predicate<HtmlElement> filter) {
608         final List<HtmlElement> elements = new ArrayList<>();
609 
610         if (isAttachedToPage()) {
611             for (final HtmlElement element : getPage().getDocumentElement().getHtmlElementDescendants()) {
612                 if (filter.test(element)
613                         && element.getEnclosingForm() == this) {
614                     elements.add(element);
615                 }
616             }
617         }
618         else {
619             for (final HtmlElement element : getHtmlElementDescendants()) {
620                 if (filter.test(element)) {
621                     elements.add(element);
622                 }
623             }
624         }
625 
626         return elements;
627     }
628 
629     /**
630      * Returns the first input element which is a member of this form and has the specified name.
631      *
632      * @param name the input name to search for
633      * @param <I> the input type
634      * @return the first input element which is a member of this form and has the specified name
635      * @throws ElementNotFoundException if there is no input in this form with the specified name
636      */
637     @SuppressWarnings("unchecked")
638     public final <I extends HtmlInput> I getInputByName(final String name) throws ElementNotFoundException {
639         final List<HtmlInput> inputs = getInputsByName(name);
640 
641         if (inputs.isEmpty()) {
642             throw new ElementNotFoundException(HtmlInput.TAG_NAME, NAME_ATTRIBUTE, name);
643         }
644         return (I) inputs.get(0);
645     }
646 
647     /**
648      * Returns all the {@link HtmlSelect} elements in this form that have the specified name.
649      *
650      * @param name the name to search for
651      * @return all the {@link HtmlSelect} elements in this form that have the specified name
652      */
653     public List<HtmlSelect> getSelectsByName(final String name) {
654         return getFormElementsByAttribute(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
655     }
656 
657     /**
658      * Returns the first {@link HtmlSelect} element in this form that has the specified name.
659      *
660      * @param name the name to search for
661      * @return the first {@link HtmlSelect} element in this form that has the specified name
662      * @throws ElementNotFoundException if this form does not contain a {@link HtmlSelect}
663      *         element with the specified name
664      */
665     public HtmlSelect getSelectByName(final String name) throws ElementNotFoundException {
666         final List<HtmlSelect> list = getSelectsByName(name);
667         if (list.isEmpty()) {
668             throw new ElementNotFoundException(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
669         }
670         return list.get(0);
671     }
672 
673     /**
674      * Returns all the {@link HtmlButton} elements in this form that have the specified name.
675      *
676      * @param name the name to search for
677      * @return all the {@link HtmlButton} elements in this form that have the specified name
678      */
679     public List<HtmlButton> getButtonsByName(final String name) {
680         return getFormElementsByAttribute(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
681     }
682 
683     /**
684      * Returns the first {@link HtmlButton} element in this form that has the specified name.
685      *
686      * @param name the name to search for
687      * @return the first {@link HtmlButton} element in this form that has the specified name
688      * @throws ElementNotFoundException if this form does not contain a {@link HtmlButton}
689      *         element with the specified name
690      */
691     public HtmlButton getButtonByName(final String name) throws ElementNotFoundException {
692         final List<HtmlButton> list = getButtonsByName(name);
693         if (list.isEmpty()) {
694             throw new ElementNotFoundException(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
695         }
696         return list.get(0);
697     }
698 
699     /**
700      * Returns all the {@link HtmlTextArea} elements in this form that have the specified name.
701      *
702      * @param name the name to search for
703      * @return all the {@link HtmlTextArea} elements in this form that have the specified name
704      */
705     public List<HtmlTextArea> getTextAreasByName(final String name) {
706         return getFormElementsByAttribute(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
707     }
708 
709     /**
710      * Returns the first {@link HtmlTextArea} element in this form that has the specified name.
711      *
712      * @param name the name to search for
713      * @return the first {@link HtmlTextArea} element in this form that has the specified name
714      * @throws ElementNotFoundException if this form does not contain a {@link HtmlTextArea}
715      *         element with the specified name
716      */
717     public HtmlTextArea getTextAreaByName(final String name) throws ElementNotFoundException {
718         final List<HtmlTextArea> list = getTextAreasByName(name);
719         if (list.isEmpty()) {
720             throw new ElementNotFoundException(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
721         }
722         return list.get(0);
723     }
724 
725     /**
726      * Returns all the {@link HtmlRadioButtonInput} elements in this form that have the specified name.
727      *
728      * @param name the name to search for
729      * @return all the {@link HtmlRadioButtonInput} elements in this form that have the specified name
730      */
731     public List<HtmlRadioButtonInput> getRadioButtonsByName(final String name) {
732         WebAssert.notNull("name", name);
733 
734         final List<HtmlRadioButtonInput> results = new ArrayList<>();
735 
736         for (final HtmlElement element : getInputsByName(name)) {
737             if (element instanceof HtmlRadioButtonInput input) {
738                 results.add(input);
739             }
740         }
741 
742         return results;
743     }
744 
745     /**
746      * Selects the specified radio button in the form. Only a radio button that is actually contained
747      * in the form can be selected.
748      *
749      * @param radioButtonInput the radio button to select
750      */
751     void setCheckedRadioButton(final HtmlRadioButtonInput radioButtonInput) {
752         if (radioButtonInput.getEnclosingForm() == null) {
753             throw new IllegalArgumentException("HtmlRadioButtonInput is not child of this HtmlForm");
754         }
755         final List<HtmlRadioButtonInput> radios = getRadioButtonsByName(radioButtonInput.getNameAttribute());
756 
757         for (final HtmlRadioButtonInput input : radios) {
758             input.setCheckedInternal(input == radioButtonInput);
759         }
760     }
761 
762     /**
763      * Returns the first checked radio button with the specified name. If none of
764      * the radio buttons by that name are checked, this method returns {@code null}.
765      *
766      * @param name the name of the radio button
767      * @return the first checked radio button with the specified name
768      */
769     public HtmlRadioButtonInput getCheckedRadioButton(final String name) {
770         WebAssert.notNull("name", name);
771 
772         for (final HtmlRadioButtonInput input : getRadioButtonsByName(name)) {
773             if (input.isChecked()) {
774                 return input;
775             }
776         }
777         return null;
778     }
779 
780     /**
781      * Returns the value of the attribute {@code action}. Refer to the <a
782      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
783      * details on the use of this attribute.
784      *
785      * @return the value of the attribute {@code action} or an empty string if that attribute isn't defined
786      */
787     public final String getActionAttribute() {
788         return getAttributeDirect("action");
789     }
790 
791     /**
792      * Sets 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      * @param action the value of the attribute {@code action}
797      */
798     public final void setActionAttribute(final String action) {
799         setAttribute("action", action);
800     }
801 
802     /**
803      * Returns the value of the attribute {@code method}. 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      * @return the value of the attribute {@code method} or an empty string if that attribute isn't defined
808      */
809     public final String getMethodAttribute() {
810         return getAttributeDirect("method");
811     }
812 
813     /**
814      * Sets 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      * @param method the value of the attribute {@code method}
819      */
820     public final void setMethodAttribute(final String method) {
821         setAttribute("method", method);
822     }
823 
824     /**
825      * Returns the value of the attribute {@code name}. 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      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
830      */
831     public final String getNameAttribute() {
832         return getAttributeDirect(NAME_ATTRIBUTE);
833     }
834 
835     /**
836      * Sets 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      * @param name the value of the attribute {@code name}
841      */
842     public final void setNameAttribute(final String name) {
843         setAttribute(NAME_ATTRIBUTE, name);
844     }
845 
846     /**
847      * Returns the value of the attribute {@code enctype}. 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. "Enctype" is the encoding type
850      * used when submitting a form back to the server.
851      *
852      * @return the value of the attribute {@code enctype} or an empty string if that attribute isn't defined
853      */
854     public final String getEnctypeAttribute() {
855         return getAttributeDirect("enctype");
856     }
857 
858     /**
859      * Sets the value of the attribute {@code enctype}. Refer to the <a
860      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
861      * details on the use of this attribute. "Enctype" is the encoding type
862      * used when submitting a form back to the server.
863      *
864      * @param encoding the value of the attribute {@code enctype}
865      */
866     public final void setEnctypeAttribute(final String encoding) {
867         setAttribute("enctype", encoding);
868     }
869 
870     /**
871      * Returns the value of the attribute {@code onsubmit}. Refer to the <a
872      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
873      * details on the use of this attribute.
874      *
875      * @return the value of the attribute {@code onsubmit} or an empty string if that attribute isn't defined
876      */
877     public final String getOnSubmitAttribute() {
878         return getAttributeDirect("onsubmit");
879     }
880 
881     /**
882      * Returns the value of the attribute {@code onreset}. 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 onreset} or an empty string if that attribute isn't defined
887      */
888     public final String getOnResetAttribute() {
889         return getAttributeDirect("onreset");
890     }
891 
892     /**
893      * Returns the value of the attribute {@code accept}. 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 accept} or an empty string if that attribute isn't defined
898      */
899     public final String getAcceptAttribute() {
900         return getAttribute(HttpHeader.ACCEPT_LC);
901     }
902 
903     /**
904      * Returns the value of the attribute {@code accept-charset}. Refer to the <a
905      * href='http://www.w3.org/TR/html401/interact/forms.html#adef-accept-charset'>
906      * HTML 4.01</a> documentation for details on the use of this attribute.
907      *
908      * @return the value of the attribute {@code accept-charset} or an empty string if that attribute isn't defined
909      */
910     public final String getAcceptCharsetAttribute() {
911         return getAttribute("accept-charset");
912     }
913 
914     /**
915      * Returns the value of the attribute {@code target}. Refer to the <a
916      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
917      * details on the use of this attribute.
918      *
919      * @return the value of the attribute {@code target} or an empty string if that attribute isn't defined
920      */
921     public final String getTargetAttribute() {
922         return getAttributeDirect("target");
923     }
924 
925     /**
926      * Sets 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      * @param target the value of the attribute {@code target}
931      */
932     public final void setTargetAttribute(final String target) {
933         setAttribute("target", target);
934     }
935 
936     /**
937      * Returns the value of the attribute {@code rel}. Refer to the
938      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
939      * documentation for details on the use of this attribute.
940      *
941      * @return the value of the attribute {@code rel} or an empty string if that attribute isn't defined
942      */
943     public final String getRelAttribute() {
944         return getAttributeDirect("rel");
945     }
946 
947     /**
948      * Returns the first input in this form with the specified value.
949      * @param value the value to search for
950      * @param <I> the input type
951      * @return the first input in this form with the specified value
952      * @throws ElementNotFoundException if this form does not contain any inputs with the specified value
953      */
954     @SuppressWarnings("unchecked")
955     public <I extends HtmlInput> I getInputByValue(final String value) throws ElementNotFoundException {
956         final List<HtmlInput> list = getInputsByValue(value);
957         if (list.isEmpty()) {
958             throw new ElementNotFoundException(HtmlInput.TAG_NAME, VALUE_ATTRIBUTE, value);
959         }
960         return (I) list.get(0);
961     }
962 
963     /**
964      * Returns all the inputs in this form with the specified value.
965      * @param value the value to search for
966      * @return all the inputs in this form with the specified value
967      */
968     public List<HtmlInput> getInputsByValue(final String value) {
969         final List<HtmlInput> results = new ArrayList<>();
970 
971         for (final HtmlElement element : getElements(htmlElement -> htmlElement instanceof HtmlInput)) {
972             if (Objects.equals(((HtmlInput) element).getValue(), value)) {
973                 results.add((HtmlInput) element);
974             }
975         }
976 
977         return results;
978     }
979 
980     /**
981      * {@inheritDoc}
982      */
983     @Override
984     protected void preventDefault() {
985         isPreventDefault_ = true;
986     }
987 
988     /**
989      * Browsers have problems with self closing form tags.
990      */
991     @Override
992     protected boolean isEmptyXmlTagExpanded() {
993         return true;
994     }
995 
996     /**
997      * @return the value of the attribute {@code novalidate} or an empty string if that attribute isn't defined
998      */
999     public final boolean isNoValidate() {
1000         return hasAttribute(ATTRIBUTE_NOVALIDATE);
1001     }
1002 
1003     /**
1004      * Sets the value of the attribute {@code novalidate}.
1005      *
1006      * @param noValidate the value of the attribute {@code novalidate}
1007      */
1008     public final void setNoValidate(final boolean noValidate) {
1009         if (noValidate) {
1010             setAttribute(ATTRIBUTE_NOVALIDATE, ATTRIBUTE_NOVALIDATE);
1011         }
1012         else {
1013             removeAttribute(ATTRIBUTE_NOVALIDATE);
1014         }
1015     }
1016 
1017     /**
1018      * Register an element to the past names map with the specified name.
1019      * @param name name or id attribute of the element
1020      * @param element the element to register
1021      */
1022     public void registerPastName(final String name, final HtmlElement element) {
1023         if (pastNamesMap_ == null) {
1024             pastNamesMap_ = new HashMap<>();
1025         }
1026         pastNamesMap_.put(name, element);
1027     }
1028 
1029     /**
1030      * Return the element registered in the past names map with the specified name.
1031      * If the element is no longer owned by this form, the entry is removed and null is returned.
1032      * @param name name or id attribute of the element
1033      * @return the element, or null if not found or no longer owned by this form
1034      */
1035     public HtmlElement getNamedElement(final String name) {
1036         if (pastNamesMap_ == null) {
1037             return null;
1038         }
1039         final HtmlElement element = pastNamesMap_.get(name);
1040         if (element != null && element.getEnclosingForm() != this) {
1041             pastNamesMap_.remove(name);
1042             return null;
1043         }
1044         return element;
1045     }
1046 }