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