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