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