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