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      * Returns the form controls contained in this form.
555      *
556      * @return a list containing all form controls in tree order (preorder,
557      *         depth-first traversal). Only the following elements are
558      *         included: {@code button}, {@code fieldset}, {@code input},
559      *         {@code object}, {@code output}, {@code select}, and
560      *         {@code textarea}
561      */
562     public List<HtmlElement> getFormElements() {
563         return getElements(htmlElement -> {
564             final String tagName = htmlElement.getTagName();
565             return HtmlButton.TAG_NAME.equals(tagName)
566                     || HtmlFieldSet.TAG_NAME.equals(tagName)
567                     || HtmlInput.TAG_NAME.equals(tagName)
568                     || HtmlObject.TAG_NAME.equals(tagName)
569                     || HtmlOutput.TAG_NAME.equals(tagName)
570                     || HtmlSelect.TAG_NAME.equals(tagName)
571                     || HtmlTextArea.TAG_NAME.equals(tagName);
572         });
573     }
574 
575     /**
576      * This is the backend for the getElements() javascript function of the form.
577      * see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
578      *
579      * @return A List containing all non-image controls in the form.
580      *         The form controls in the returned collection are in the same order
581      *         in which they appear in the form by following a preorder,
582      *         depth-first traversal of the tree. This is called tree order.
583      *         Only the following elements are returned:
584      *         button, fieldset,
585      *         input (with the exception that any whose type is "image" are omitted for historical reasons),
586      *         object, output, select, textarea.
587      */
588     public List<HtmlElement> getElementsJS() {
589         return getElements(htmlElement -> {
590             final String tagName = htmlElement.getTagName();
591             if (HtmlInput.TAG_NAME.equals(tagName)) {
592                 return !(htmlElement instanceof HtmlImageInput);
593             }
594 
595             return HtmlButton.TAG_NAME.equals(tagName)
596                     || HtmlFieldSet.TAG_NAME.equals(tagName)
597                     || HtmlObject.TAG_NAME.equals(tagName)
598                     || HtmlOutput.TAG_NAME.equals(tagName)
599                     || HtmlSelect.TAG_NAME.equals(tagName)
600                     || HtmlTextArea.TAG_NAME.equals(tagName);
601         });
602     }
603 
604     /**
605      * Returns the form elements that match the specified filter.
606      *
607      * @param filter the predicate used to select elements
608      * @return a list of form elements matching the specified filter
609      */
610     public List<HtmlElement> getElements(final Predicate<HtmlElement> filter) {
611         final List<HtmlElement> elements = new ArrayList<>();
612 
613         if (isAttachedToPage()) {
614             for (final HtmlElement element : getPage().getDocumentElement().getHtmlElementDescendants()) {
615                 if (filter.test(element)
616                         && element.getEnclosingForm() == this) {
617                     elements.add(element);
618                 }
619             }
620         }
621         else {
622             for (final HtmlElement element : getHtmlElementDescendants()) {
623                 if (filter.test(element)) {
624                     elements.add(element);
625                 }
626             }
627         }
628 
629         return elements;
630     }
631 
632     /**
633      * Returns the first input element which is a member of this form and has the specified name.
634      *
635      * @param name the input name to search for
636      * @param <I> the input type
637      * @return the first input element which is a member of this form and has the specified name
638      * @throws ElementNotFoundException if there is no input in this form with the specified name
639      */
640     @SuppressWarnings("unchecked")
641     public final <I extends HtmlInput> I getInputByName(final String name) throws ElementNotFoundException {
642         final List<HtmlInput> inputs = getInputsByName(name);
643 
644         if (inputs.isEmpty()) {
645             throw new ElementNotFoundException(HtmlInput.TAG_NAME, NAME_ATTRIBUTE, name);
646         }
647         return (I) inputs.get(0);
648     }
649 
650     /**
651      * Returns all the {@link HtmlSelect} elements in this form that have the specified name.
652      *
653      * @param name the name to search for
654      * @return all the {@link HtmlSelect} elements in this form that have the specified name
655      */
656     public List<HtmlSelect> getSelectsByName(final String name) {
657         return getFormElementsByAttribute(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
658     }
659 
660     /**
661      * Returns the first {@link HtmlSelect} element in this form that has the specified name.
662      *
663      * @param name the name to search for
664      * @return the first {@link HtmlSelect} element in this form that has the specified name
665      * @throws ElementNotFoundException if this form does not contain a {@link HtmlSelect}
666      *         element with the specified name
667      */
668     public HtmlSelect getSelectByName(final String name) throws ElementNotFoundException {
669         final List<HtmlSelect> list = getSelectsByName(name);
670         if (list.isEmpty()) {
671             throw new ElementNotFoundException(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
672         }
673         return list.get(0);
674     }
675 
676     /**
677      * Returns all the {@link HtmlButton} elements in this form that have the specified name.
678      *
679      * @param name the name to search for
680      * @return all the {@link HtmlButton} elements in this form that have the specified name
681      */
682     public List<HtmlButton> getButtonsByName(final String name) {
683         return getFormElementsByAttribute(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
684     }
685 
686     /**
687      * Returns the first {@link HtmlButton} element in this form that has the specified name.
688      *
689      * @param name the name to search for
690      * @return the first {@link HtmlButton} element in this form that has the specified name
691      * @throws ElementNotFoundException if this form does not contain a {@link HtmlButton}
692      *         element with the specified name
693      */
694     public HtmlButton getButtonByName(final String name) throws ElementNotFoundException {
695         final List<HtmlButton> list = getButtonsByName(name);
696         if (list.isEmpty()) {
697             throw new ElementNotFoundException(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
698         }
699         return list.get(0);
700     }
701 
702     /**
703      * Returns all the {@link HtmlTextArea} elements in this form that have the specified name.
704      *
705      * @param name the name to search for
706      * @return all the {@link HtmlTextArea} elements in this form that have the specified name
707      */
708     public List<HtmlTextArea> getTextAreasByName(final String name) {
709         return getFormElementsByAttribute(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
710     }
711 
712     /**
713      * Returns the first {@link HtmlTextArea} element in this form that has the specified name.
714      *
715      * @param name the name to search for
716      * @return the first {@link HtmlTextArea} element in this form that has the specified name
717      * @throws ElementNotFoundException if this form does not contain a {@link HtmlTextArea}
718      *         element with the specified name
719      */
720     public HtmlTextArea getTextAreaByName(final String name) throws ElementNotFoundException {
721         final List<HtmlTextArea> list = getTextAreasByName(name);
722         if (list.isEmpty()) {
723             throw new ElementNotFoundException(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
724         }
725         return list.get(0);
726     }
727 
728     /**
729      * Returns all the {@link HtmlRadioButtonInput} elements in this form that have the specified name.
730      *
731      * @param name the name to search for
732      * @return all the {@link HtmlRadioButtonInput} elements in this form that have the specified name
733      */
734     public List<HtmlRadioButtonInput> getRadioButtonsByName(final String name) {
735         WebAssert.notNull("name", name);
736 
737         final List<HtmlRadioButtonInput> results = new ArrayList<>();
738 
739         for (final HtmlElement element : getInputsByName(name)) {
740             if (element instanceof HtmlRadioButtonInput input) {
741                 results.add(input);
742             }
743         }
744 
745         return results;
746     }
747 
748     /**
749      * Selects the specified radio button in the form. Only a radio button that is actually contained
750      * in the form can be selected.
751      *
752      * @param radioButtonInput the radio button to select
753      */
754     void setCheckedRadioButton(final HtmlRadioButtonInput radioButtonInput) {
755         if (radioButtonInput.getEnclosingForm() == null) {
756             throw new IllegalArgumentException("HtmlRadioButtonInput is not child of this HtmlForm");
757         }
758         final List<HtmlRadioButtonInput> radios = getRadioButtonsByName(radioButtonInput.getNameAttribute());
759 
760         for (final HtmlRadioButtonInput input : radios) {
761             input.setCheckedInternal(input == radioButtonInput);
762         }
763     }
764 
765     /**
766      * Returns the first checked radio button with the specified name. If none of
767      * the radio buttons by that name are checked, this method returns {@code null}.
768      *
769      * @param name the name of the radio button
770      * @return the first checked radio button with the specified name
771      */
772     public HtmlRadioButtonInput getCheckedRadioButton(final String name) {
773         WebAssert.notNull("name", name);
774 
775         for (final HtmlRadioButtonInput input : getRadioButtonsByName(name)) {
776             if (input.isChecked()) {
777                 return input;
778             }
779         }
780         return null;
781     }
782 
783     /**
784      * Returns the value of the attribute {@code action}. Refer to the <a
785      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
786      * details on the use of this attribute.
787      *
788      * @return the value of the attribute {@code action} or an empty string if that attribute isn't defined
789      */
790     public final String getActionAttribute() {
791         return getAttributeDirect("action");
792     }
793 
794     /**
795      * Sets the value of the attribute {@code action}. Refer to the <a
796      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
797      * details on the use of this attribute.
798      *
799      * @param action the value of the attribute {@code action}
800      */
801     public final void setActionAttribute(final String action) {
802         setAttribute("action", action);
803     }
804 
805     /**
806      * Returns the value of the attribute {@code method}. Refer to the <a
807      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
808      * details on the use of this attribute.
809      *
810      * @return the value of the attribute {@code method} or an empty string if that attribute isn't defined
811      */
812     public final String getMethodAttribute() {
813         return getAttributeDirect("method");
814     }
815 
816     /**
817      * Sets the value of the attribute {@code method}. Refer to the <a
818      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
819      * details on the use of this attribute.
820      *
821      * @param method the value of the attribute {@code method}
822      */
823     public final void setMethodAttribute(final String method) {
824         setAttribute("method", method);
825     }
826 
827     /**
828      * Returns the value of the attribute {@code name}. Refer to the <a
829      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
830      * details on the use of this attribute.
831      *
832      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
833      */
834     public final String getNameAttribute() {
835         return getAttributeDirect(NAME_ATTRIBUTE);
836     }
837 
838     /**
839      * Sets the value of the attribute {@code name}. Refer to the <a
840      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
841      * details on the use of this attribute.
842      *
843      * @param name the value of the attribute {@code name}
844      */
845     public final void setNameAttribute(final String name) {
846         setAttribute(NAME_ATTRIBUTE, name);
847     }
848 
849     /**
850      * Returns the value of the attribute {@code enctype}. Refer to the <a
851      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
852      * details on the use of this attribute. "Enctype" is the encoding type
853      * used when submitting a form back to the server.
854      *
855      * @return the value of the attribute {@code enctype} or an empty string if that attribute isn't defined
856      */
857     public final String getEnctypeAttribute() {
858         return getAttributeDirect("enctype");
859     }
860 
861     /**
862      * Sets the value of the attribute {@code enctype}. Refer to the <a
863      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
864      * details on the use of this attribute. "Enctype" is the encoding type
865      * used when submitting a form back to the server.
866      *
867      * @param encoding the value of the attribute {@code enctype}
868      */
869     public final void setEnctypeAttribute(final String encoding) {
870         setAttribute("enctype", encoding);
871     }
872 
873     /**
874      * Returns the value of the attribute {@code onsubmit}. Refer to the <a
875      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
876      * details on the use of this attribute.
877      *
878      * @return the value of the attribute {@code onsubmit} or an empty string if that attribute isn't defined
879      */
880     public final String getOnSubmitAttribute() {
881         return getAttributeDirect("onsubmit");
882     }
883 
884     /**
885      * Returns the value of the attribute {@code onreset}. Refer to the <a
886      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
887      * details on the use of this attribute.
888      *
889      * @return the value of the attribute {@code onreset} or an empty string if that attribute isn't defined
890      */
891     public final String getOnResetAttribute() {
892         return getAttributeDirect("onreset");
893     }
894 
895     /**
896      * Returns the value of the attribute {@code accept}. Refer to the <a
897      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
898      * details on the use of this attribute.
899      *
900      * @return the value of the attribute {@code accept} or an empty string if that attribute isn't defined
901      */
902     public final String getAcceptAttribute() {
903         return getAttribute(HttpHeader.ACCEPT_LC);
904     }
905 
906     /**
907      * Returns the value of the attribute {@code accept-charset}. Refer to the <a
908      * href='http://www.w3.org/TR/html401/interact/forms.html#adef-accept-charset'>
909      * HTML 4.01</a> documentation for details on the use of this attribute.
910      *
911      * @return the value of the attribute {@code accept-charset} or an empty string if that attribute isn't defined
912      */
913     public final String getAcceptCharsetAttribute() {
914         return getAttribute("accept-charset");
915     }
916 
917     /**
918      * Returns the value of the attribute {@code target}. Refer to the <a
919      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
920      * details on the use of this attribute.
921      *
922      * @return the value of the attribute {@code target} or an empty string if that attribute isn't defined
923      */
924     public final String getTargetAttribute() {
925         return getAttributeDirect("target");
926     }
927 
928     /**
929      * Sets the value of the attribute {@code target}. Refer to the <a
930      * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
931      * details on the use of this attribute.
932      *
933      * @param target the value of the attribute {@code target}
934      */
935     public final void setTargetAttribute(final String target) {
936         setAttribute("target", target);
937     }
938 
939     /**
940      * Returns the value of the attribute {@code rel}. Refer to the
941      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
942      * documentation for details on the use of this attribute.
943      *
944      * @return the value of the attribute {@code rel} or an empty string if that attribute isn't defined
945      */
946     public final String getRelAttribute() {
947         return getAttributeDirect("rel");
948     }
949 
950     /**
951      * Returns the first input in this form with the specified value.
952      * @param value the value to search for
953      * @param <I> the input type
954      * @return the first input in this form with the specified value
955      * @throws ElementNotFoundException if this form does not contain any inputs with the specified value
956      */
957     @SuppressWarnings("unchecked")
958     public <I extends HtmlInput> I getInputByValue(final String value) throws ElementNotFoundException {
959         final List<HtmlInput> list = getInputsByValue(value);
960         if (list.isEmpty()) {
961             throw new ElementNotFoundException(HtmlInput.TAG_NAME, VALUE_ATTRIBUTE, value);
962         }
963         return (I) list.get(0);
964     }
965 
966     /**
967      * Returns all the inputs in this form with the specified value.
968      * @param value the value to search for
969      * @return all the inputs in this form with the specified value
970      */
971     public List<HtmlInput> getInputsByValue(final String value) {
972         final List<HtmlInput> results = new ArrayList<>();
973 
974         for (final HtmlElement element : getElements(htmlElement -> htmlElement instanceof HtmlInput)) {
975             if (Objects.equals(((HtmlInput) element).getValue(), value)) {
976                 results.add((HtmlInput) element);
977             }
978         }
979 
980         return results;
981     }
982 
983     /**
984      * {@inheritDoc}
985      */
986     @Override
987     protected void preventDefault() {
988         isPreventDefault_ = true;
989     }
990 
991     /**
992      * Browsers have problems with self closing form tags.
993      */
994     @Override
995     protected boolean isEmptyXmlTagExpanded() {
996         return true;
997     }
998 
999     /**
1000      * Returns whether form validation is disabled.
1001      *
1002      * @return {@code true} if the {@code novalidate} attribute is present
1003      */
1004     public final boolean isNoValidate() {
1005         return hasAttribute(ATTRIBUTE_NOVALIDATE);
1006     }
1007 
1008     /**
1009      * Sets the value of the attribute {@code novalidate}.
1010      *
1011      * @param noValidate the value of the attribute {@code novalidate}
1012      */
1013     public final void setNoValidate(final boolean noValidate) {
1014         if (noValidate) {
1015             setAttribute(ATTRIBUTE_NOVALIDATE, ATTRIBUTE_NOVALIDATE);
1016         }
1017         else {
1018             removeAttribute(ATTRIBUTE_NOVALIDATE);
1019         }
1020     }
1021 
1022     /**
1023      * Register an element to the past names map with the specified name.
1024      * @param name name or id attribute of the element
1025      * @param element the element to register
1026      */
1027     public void registerPastName(final String name, final HtmlElement element) {
1028         if (pastNamesMap_ == null) {
1029             pastNamesMap_ = new HashMap<>();
1030         }
1031         pastNamesMap_.put(name, element);
1032     }
1033 
1034     /**
1035      * Return the element registered in the past names map with the specified name.
1036      * If the element is no longer owned by this form, the entry is removed and null is returned.
1037      * @param name name or id attribute of the element
1038      * @return the element, or null if not found or no longer owned by this form
1039      */
1040     public HtmlElement getNamedElement(final String name) {
1041         if (pastNamesMap_ == null) {
1042             return null;
1043         }
1044         final HtmlElement element = pastNamesMap_.get(name);
1045         if (element != null && element.getEnclosingForm() != this) {
1046             pastNamesMap_.remove(name);
1047             return null;
1048         }
1049         return element;
1050     }
1051 }