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