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 request.setCharset(enc);
340
341 // forms are ignoring the rel='noreferrer'
342 if (browser.hasFeature(FORM_IGNORE_REL_NOREFERRER)
343 || !relContainsNoreferrer()) {
344 request.setRefererHeader(htmlPage.getUrl());
345 }
346
347 if (HttpMethod.POST == method) {
348 try {
349 request.setAdditionalHeader(HttpHeader.ORIGIN,
350 UrlUtils.getUrlWithProtocolAndAuthority(htmlPage.getUrl()).toExternalForm());
351 }
352 catch (final MalformedURLException e) {
353 if (LOG.isInfoEnabled()) {
354 LOG.info("Invalid origin url '" + htmlPage.getUrl() + "'");
355 }
356 }
357 }
358 if (HttpMethod.POST == method) {
359 if (browser.hasFeature(FORM_SUBMISSION_HEADER_CACHE_CONTROL_MAX_AGE)) {
360 request.setAdditionalHeader(HttpHeader.CACHE_CONTROL, "max-age=0");
361 }
362 }
363
364 return request;
365 }
366
367 private boolean relContainsNoreferrer() {
368 String rel = getRelAttribute();
369 if (rel != null) {
370 rel = rel.toLowerCase(Locale.ROOT);
371 return ArrayUtils.contains(StringUtils.splitAtBlank(rel), "noreferrer");
372 }
373 return false;
374 }
375
376 /**
377 * Returns the charset to use for the form submission. This is the first one
378 * from the list provided in {@link #getAcceptCharsetAttribute()} if any
379 * or the page's charset else
380 * @return the charset to use for the form submission
381 */
382 private Charset getSubmitCharset() {
383 String charset = getAcceptCharsetAttribute();
384 if (!charset.isEmpty()) {
385 charset = charset.trim();
386 return EncodingSniffer.toCharset(
387 SUBMIT_CHARSET_PATTERN.matcher(charset).replaceAll("").toUpperCase(Locale.ROOT));
388 }
389 return getPage().getCharset();
390 }
391
392 /**
393 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
394 *
395 * Returns a list of {@link NameValuePair}s that represent the data that will be
396 * sent to the server when this form is submitted. This is primarily intended to aid
397 * debugging.
398 *
399 * @param submitElement the element used to submit the form, or {@code null} if the
400 * form was submitted by JavaScript
401 * @return the list of {@link NameValuePair}s that represent that data that will be sent
402 * to the server when this form is submitted
403 */
404 public List<NameValuePair> getParameterListForSubmit(final SubmittableElement submitElement) {
405 final Collection<SubmittableElement> submittableElements = getSubmittableElements(submitElement);
406
407 final List<NameValuePair> parameterList = new ArrayList<>(submittableElements.size());
408 for (final SubmittableElement element : submittableElements) {
409 parameterList.addAll(Arrays.asList(element.getSubmitNameValuePairs()));
410 }
411
412 return parameterList;
413 }
414
415 /**
416 * Resets this form to its initial values, returning the page contained by this form's window after the
417 * reset. Note that the returned page may or may not be the same as the original page, based on JavaScript
418 * event handlers, etc.
419 *
420 * @return the page contained by this form's window after the reset
421 */
422 public Page reset() {
423 final SgmlPage sgmlPage = getPage();
424 final ScriptResult scriptResult = fireEvent(Event.TYPE_RESET);
425 if (ScriptResult.isFalse(scriptResult)) {
426 return sgmlPage.getWebClient().getCurrentWindow().getEnclosedPage();
427 }
428
429 for (final HtmlElement next : getHtmlElementDescendants()) {
430 if (next instanceof SubmittableElement element) {
431 element.reset();
432 }
433 }
434
435 return sgmlPage;
436 }
437
438 /**
439 * {@inheritDoc}
440 */
441 @Override
442 public boolean isValid() {
443 for (final HtmlElement element : getFormElements()) {
444 if (!element.isValid()) {
445 return false;
446 }
447 }
448 return super.isValid();
449 }
450
451 /**
452 * Returns a collection of elements that represent all the "submittable" elements in this form,
453 * assuming that the specified element is used to submit the form.
454 *
455 * @param submitElement the element used to submit the form, or {@code null} if the
456 * form is submitted by JavaScript
457 * @return a collection of elements that represent all the "submittable" elements in this form
458 */
459 Collection<SubmittableElement> getSubmittableElements(final SubmittableElement submitElement) {
460 final List<SubmittableElement> submittableElements = new ArrayList<>();
461
462 for (final HtmlElement element : getElements(htmlElement -> isSubmittable(htmlElement, submitElement))) {
463 submittableElements.add((SubmittableElement) element);
464 }
465
466 return submittableElements;
467 }
468
469 private static boolean isValidForSubmission(final HtmlElement element, final SubmittableElement submitElement) {
470 final String tagName = element.getTagName();
471 if (!SUBMITTABLE_TAG_NAMES.contains(tagName)) {
472 return false;
473 }
474 if (element.isDisabledElementAndDisabled()) {
475 return false;
476 }
477 // clicked input type="image" is submitted even if it hasn't a name
478 if (element == submitElement && element instanceof HtmlImageInput) {
479 return true;
480 }
481
482 if (!element.hasAttribute(NAME_ATTRIBUTE)) {
483 return false;
484 }
485
486 if (StringUtils.isEmptyString(element.getAttributeDirect(NAME_ATTRIBUTE))) {
487 return false;
488 }
489
490 if (element instanceof HtmlInput input) {
491 if (input.isCheckable()) {
492 return input.isChecked();
493 }
494 }
495 if (element instanceof HtmlSelect select) {
496 return select.isValidForSubmission();
497 }
498 return true;
499 }
500
501 /**
502 * Returns {@code true} if the specified element gets submitted when this form is submitted,
503 * assuming that the form is submitted using the specified submit element.
504 *
505 * @param element the element to check
506 * @param submitElement the element used to submit the form, or {@code null} if the form is
507 * submitted by JavaScript
508 * @return {@code true} if the specified element gets submitted when this form is submitted
509 */
510 private static boolean isSubmittable(final HtmlElement element, final SubmittableElement submitElement) {
511 if (!isValidForSubmission(element, submitElement)) {
512 return false;
513 }
514
515 // The one submit button that was clicked can be submitted but no other ones
516 if (element == submitElement) {
517 return true;
518 }
519 if (element instanceof HtmlInput input) {
520 if (!input.isSubmitable()) {
521 return false;
522 }
523 }
524
525 return !HtmlButton.TAG_NAME.equals(element.getTagName());
526 }
527
528 /**
529 * Returns all input elements which are members of this form and have the specified name.
530 *
531 * @param name the input name to search for
532 * @return all input elements which are members of this form and have the specified name
533 */
534 public List<HtmlInput> getInputsByName(final String name) {
535 return getFormElementsByAttribute(HtmlInput.TAG_NAME, NAME_ATTRIBUTE, name);
536 }
537
538 /**
539 * Same as {@link #getElementsByAttribute(String, String, String)} but
540 * ignoring elements that are contained in a nested form.
541 */
542 @SuppressWarnings("unchecked")
543 private <E extends HtmlElement> List<E> getFormElementsByAttribute(
544 final String elementName,
545 final String attributeName,
546 final String attributeValue) {
547
548 return (List<E>) getElements(htmlElement ->
549 htmlElement.getTagName().equals(elementName)
550 && htmlElement.getAttribute(attributeName).equals(attributeValue));
551 }
552
553 /**
554 * @return A List containing all form controls in the form.
555 * The form controls in the returned collection are in the same order
556 * in which they appear in the form by following a preorder,
557 * depth-first traversal of the tree. This is called tree order.
558 * Only the following elements are returned:
559 * button, fieldset, input, object, output, select, textarea.
560 */
561 public List<HtmlElement> getFormElements() {
562 return getElements(htmlElement -> {
563 final String tagName = htmlElement.getTagName();
564 return HtmlButton.TAG_NAME.equals(tagName)
565 || HtmlFieldSet.TAG_NAME.equals(tagName)
566 || HtmlInput.TAG_NAME.equals(tagName)
567 || HtmlObject.TAG_NAME.equals(tagName)
568 || HtmlOutput.TAG_NAME.equals(tagName)
569 || HtmlSelect.TAG_NAME.equals(tagName)
570 || HtmlTextArea.TAG_NAME.equals(tagName);
571 });
572 }
573
574 /**
575 * This is the backend for the getElements() javascript function of the form.
576 * see https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements
577 *
578 * @return A List containing all non-image controls in the form.
579 * The form controls in the returned collection are in the same order
580 * in which they appear in the form by following a preorder,
581 * depth-first traversal of the tree. This is called tree order.
582 * Only the following elements are returned:
583 * button, fieldset,
584 * input (with the exception that any whose type is "image" are omitted for historical reasons),
585 * object, output, select, textarea.
586 */
587 public List<HtmlElement> getElementsJS() {
588 return getElements(htmlElement -> {
589 final String tagName = htmlElement.getTagName();
590 if (HtmlInput.TAG_NAME.equals(tagName)) {
591 return !(htmlElement instanceof HtmlImageInput);
592 }
593
594 return HtmlButton.TAG_NAME.equals(tagName)
595 || HtmlFieldSet.TAG_NAME.equals(tagName)
596 || HtmlObject.TAG_NAME.equals(tagName)
597 || HtmlOutput.TAG_NAME.equals(tagName)
598 || HtmlSelect.TAG_NAME.equals(tagName)
599 || HtmlTextArea.TAG_NAME.equals(tagName);
600 });
601 }
602
603 /**
604 * @param filter a predicate to filter the element
605 * @return all elements attached to this form and matching the filter predicate
606 */
607 public List<HtmlElement> getElements(final Predicate<HtmlElement> filter) {
608 final List<HtmlElement> elements = new ArrayList<>();
609
610 if (isAttachedToPage()) {
611 for (final HtmlElement element : getPage().getDocumentElement().getHtmlElementDescendants()) {
612 if (filter.test(element)
613 && element.getEnclosingForm() == this) {
614 elements.add(element);
615 }
616 }
617 }
618 else {
619 for (final HtmlElement element : getHtmlElementDescendants()) {
620 if (filter.test(element)) {
621 elements.add(element);
622 }
623 }
624 }
625
626 return elements;
627 }
628
629 /**
630 * Returns the first input element which is a member of this form and has the specified name.
631 *
632 * @param name the input name to search for
633 * @param <I> the input type
634 * @return the first input element which is a member of this form and has the specified name
635 * @throws ElementNotFoundException if there is no input in this form with the specified name
636 */
637 @SuppressWarnings("unchecked")
638 public final <I extends HtmlInput> I getInputByName(final String name) throws ElementNotFoundException {
639 final List<HtmlInput> inputs = getInputsByName(name);
640
641 if (inputs.isEmpty()) {
642 throw new ElementNotFoundException(HtmlInput.TAG_NAME, NAME_ATTRIBUTE, name);
643 }
644 return (I) inputs.get(0);
645 }
646
647 /**
648 * Returns all the {@link HtmlSelect} elements in this form that have the specified name.
649 *
650 * @param name the name to search for
651 * @return all the {@link HtmlSelect} elements in this form that have the specified name
652 */
653 public List<HtmlSelect> getSelectsByName(final String name) {
654 return getFormElementsByAttribute(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
655 }
656
657 /**
658 * Returns the first {@link HtmlSelect} element in this form that has the specified name.
659 *
660 * @param name the name to search for
661 * @return the first {@link HtmlSelect} element in this form that has the specified name
662 * @throws ElementNotFoundException if this form does not contain a {@link HtmlSelect}
663 * element with the specified name
664 */
665 public HtmlSelect getSelectByName(final String name) throws ElementNotFoundException {
666 final List<HtmlSelect> list = getSelectsByName(name);
667 if (list.isEmpty()) {
668 throw new ElementNotFoundException(HtmlSelect.TAG_NAME, NAME_ATTRIBUTE, name);
669 }
670 return list.get(0);
671 }
672
673 /**
674 * Returns all the {@link HtmlButton} elements in this form that have the specified name.
675 *
676 * @param name the name to search for
677 * @return all the {@link HtmlButton} elements in this form that have the specified name
678 */
679 public List<HtmlButton> getButtonsByName(final String name) {
680 return getFormElementsByAttribute(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
681 }
682
683 /**
684 * Returns the first {@link HtmlButton} element in this form that has the specified name.
685 *
686 * @param name the name to search for
687 * @return the first {@link HtmlButton} element in this form that has the specified name
688 * @throws ElementNotFoundException if this form does not contain a {@link HtmlButton}
689 * element with the specified name
690 */
691 public HtmlButton getButtonByName(final String name) throws ElementNotFoundException {
692 final List<HtmlButton> list = getButtonsByName(name);
693 if (list.isEmpty()) {
694 throw new ElementNotFoundException(HtmlButton.TAG_NAME, NAME_ATTRIBUTE, name);
695 }
696 return list.get(0);
697 }
698
699 /**
700 * Returns all the {@link HtmlTextArea} elements in this form that have the specified name.
701 *
702 * @param name the name to search for
703 * @return all the {@link HtmlTextArea} elements in this form that have the specified name
704 */
705 public List<HtmlTextArea> getTextAreasByName(final String name) {
706 return getFormElementsByAttribute(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
707 }
708
709 /**
710 * Returns the first {@link HtmlTextArea} element in this form that has the specified name.
711 *
712 * @param name the name to search for
713 * @return the first {@link HtmlTextArea} element in this form that has the specified name
714 * @throws ElementNotFoundException if this form does not contain a {@link HtmlTextArea}
715 * element with the specified name
716 */
717 public HtmlTextArea getTextAreaByName(final String name) throws ElementNotFoundException {
718 final List<HtmlTextArea> list = getTextAreasByName(name);
719 if (list.isEmpty()) {
720 throw new ElementNotFoundException(HtmlTextArea.TAG_NAME, NAME_ATTRIBUTE, name);
721 }
722 return list.get(0);
723 }
724
725 /**
726 * Returns all the {@link HtmlRadioButtonInput} elements in this form that have the specified name.
727 *
728 * @param name the name to search for
729 * @return all the {@link HtmlRadioButtonInput} elements in this form that have the specified name
730 */
731 public List<HtmlRadioButtonInput> getRadioButtonsByName(final String name) {
732 WebAssert.notNull("name", name);
733
734 final List<HtmlRadioButtonInput> results = new ArrayList<>();
735
736 for (final HtmlElement element : getInputsByName(name)) {
737 if (element instanceof HtmlRadioButtonInput input) {
738 results.add(input);
739 }
740 }
741
742 return results;
743 }
744
745 /**
746 * Selects the specified radio button in the form. Only a radio button that is actually contained
747 * in the form can be selected.
748 *
749 * @param radioButtonInput the radio button to select
750 */
751 void setCheckedRadioButton(final HtmlRadioButtonInput radioButtonInput) {
752 if (radioButtonInput.getEnclosingForm() == null) {
753 throw new IllegalArgumentException("HtmlRadioButtonInput is not child of this HtmlForm");
754 }
755 final List<HtmlRadioButtonInput> radios = getRadioButtonsByName(radioButtonInput.getNameAttribute());
756
757 for (final HtmlRadioButtonInput input : radios) {
758 input.setCheckedInternal(input == radioButtonInput);
759 }
760 }
761
762 /**
763 * Returns the first checked radio button with the specified name. If none of
764 * the radio buttons by that name are checked, this method returns {@code null}.
765 *
766 * @param name the name of the radio button
767 * @return the first checked radio button with the specified name
768 */
769 public HtmlRadioButtonInput getCheckedRadioButton(final String name) {
770 WebAssert.notNull("name", name);
771
772 for (final HtmlRadioButtonInput input : getRadioButtonsByName(name)) {
773 if (input.isChecked()) {
774 return input;
775 }
776 }
777 return null;
778 }
779
780 /**
781 * Returns the value of the attribute {@code action}. Refer to the <a
782 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
783 * details on the use of this attribute.
784 *
785 * @return the value of the attribute {@code action} or an empty string if that attribute isn't defined
786 */
787 public final String getActionAttribute() {
788 return getAttributeDirect("action");
789 }
790
791 /**
792 * Sets 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 * @param action the value of the attribute {@code action}
797 */
798 public final void setActionAttribute(final String action) {
799 setAttribute("action", action);
800 }
801
802 /**
803 * Returns the value of the attribute {@code method}. 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 * @return the value of the attribute {@code method} or an empty string if that attribute isn't defined
808 */
809 public final String getMethodAttribute() {
810 return getAttributeDirect("method");
811 }
812
813 /**
814 * Sets 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 * @param method the value of the attribute {@code method}
819 */
820 public final void setMethodAttribute(final String method) {
821 setAttribute("method", method);
822 }
823
824 /**
825 * Returns the value of the attribute {@code name}. 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 * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
830 */
831 public final String getNameAttribute() {
832 return getAttributeDirect(NAME_ATTRIBUTE);
833 }
834
835 /**
836 * Sets 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 * @param name the value of the attribute {@code name}
841 */
842 public final void setNameAttribute(final String name) {
843 setAttribute(NAME_ATTRIBUTE, name);
844 }
845
846 /**
847 * Returns the value of the attribute {@code enctype}. 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. "Enctype" is the encoding type
850 * used when submitting a form back to the server.
851 *
852 * @return the value of the attribute {@code enctype} or an empty string if that attribute isn't defined
853 */
854 public final String getEnctypeAttribute() {
855 return getAttributeDirect("enctype");
856 }
857
858 /**
859 * Sets the value of the attribute {@code enctype}. Refer to the <a
860 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
861 * details on the use of this attribute. "Enctype" is the encoding type
862 * used when submitting a form back to the server.
863 *
864 * @param encoding the value of the attribute {@code enctype}
865 */
866 public final void setEnctypeAttribute(final String encoding) {
867 setAttribute("enctype", encoding);
868 }
869
870 /**
871 * Returns the value of the attribute {@code onsubmit}. Refer to the <a
872 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
873 * details on the use of this attribute.
874 *
875 * @return the value of the attribute {@code onsubmit} or an empty string if that attribute isn't defined
876 */
877 public final String getOnSubmitAttribute() {
878 return getAttributeDirect("onsubmit");
879 }
880
881 /**
882 * Returns the value of the attribute {@code onreset}. 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 onreset} or an empty string if that attribute isn't defined
887 */
888 public final String getOnResetAttribute() {
889 return getAttributeDirect("onreset");
890 }
891
892 /**
893 * Returns the value of the attribute {@code accept}. 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 accept} or an empty string if that attribute isn't defined
898 */
899 public final String getAcceptAttribute() {
900 return getAttribute(HttpHeader.ACCEPT_LC);
901 }
902
903 /**
904 * Returns the value of the attribute {@code accept-charset}. Refer to the <a
905 * href='http://www.w3.org/TR/html401/interact/forms.html#adef-accept-charset'>
906 * HTML 4.01</a> documentation for details on the use of this attribute.
907 *
908 * @return the value of the attribute {@code accept-charset} or an empty string if that attribute isn't defined
909 */
910 public final String getAcceptCharsetAttribute() {
911 return getAttribute("accept-charset");
912 }
913
914 /**
915 * Returns the value of the attribute {@code target}. Refer to the <a
916 * href='http://www.w3.org/TR/html401/'>HTML 4.01</a> documentation for
917 * details on the use of this attribute.
918 *
919 * @return the value of the attribute {@code target} or an empty string if that attribute isn't defined
920 */
921 public final String getTargetAttribute() {
922 return getAttributeDirect("target");
923 }
924
925 /**
926 * Sets 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 * @param target the value of the attribute {@code target}
931 */
932 public final void setTargetAttribute(final String target) {
933 setAttribute("target", target);
934 }
935
936 /**
937 * Returns the value of the attribute {@code rel}. Refer to the
938 * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
939 * documentation for details on the use of this attribute.
940 *
941 * @return the value of the attribute {@code rel} or an empty string if that attribute isn't defined
942 */
943 public final String getRelAttribute() {
944 return getAttributeDirect("rel");
945 }
946
947 /**
948 * Returns the first input in this form with the specified value.
949 * @param value the value to search for
950 * @param <I> the input type
951 * @return the first input in this form with the specified value
952 * @throws ElementNotFoundException if this form does not contain any inputs with the specified value
953 */
954 @SuppressWarnings("unchecked")
955 public <I extends HtmlInput> I getInputByValue(final String value) throws ElementNotFoundException {
956 final List<HtmlInput> list = getInputsByValue(value);
957 if (list.isEmpty()) {
958 throw new ElementNotFoundException(HtmlInput.TAG_NAME, VALUE_ATTRIBUTE, value);
959 }
960 return (I) list.get(0);
961 }
962
963 /**
964 * Returns all the inputs in this form with the specified value.
965 * @param value the value to search for
966 * @return all the inputs in this form with the specified value
967 */
968 public List<HtmlInput> getInputsByValue(final String value) {
969 final List<HtmlInput> results = new ArrayList<>();
970
971 for (final HtmlElement element : getElements(htmlElement -> htmlElement instanceof HtmlInput)) {
972 if (Objects.equals(((HtmlInput) element).getValue(), value)) {
973 results.add((HtmlInput) element);
974 }
975 }
976
977 return results;
978 }
979
980 /**
981 * {@inheritDoc}
982 */
983 @Override
984 protected void preventDefault() {
985 isPreventDefault_ = true;
986 }
987
988 /**
989 * Browsers have problems with self closing form tags.
990 */
991 @Override
992 protected boolean isEmptyXmlTagExpanded() {
993 return true;
994 }
995
996 /**
997 * @return the value of the attribute {@code novalidate} or an empty string if that attribute isn't defined
998 */
999 public final boolean isNoValidate() {
1000 return hasAttribute(ATTRIBUTE_NOVALIDATE);
1001 }
1002
1003 /**
1004 * Sets the value of the attribute {@code novalidate}.
1005 *
1006 * @param noValidate the value of the attribute {@code novalidate}
1007 */
1008 public final void setNoValidate(final boolean noValidate) {
1009 if (noValidate) {
1010 setAttribute(ATTRIBUTE_NOVALIDATE, ATTRIBUTE_NOVALIDATE);
1011 }
1012 else {
1013 removeAttribute(ATTRIBUTE_NOVALIDATE);
1014 }
1015 }
1016
1017 /**
1018 * Register an element to the past names map with the specified name.
1019 * @param name name or id attribute of the element
1020 * @param element the element to register
1021 */
1022 public void registerPastName(final String name, final HtmlElement element) {
1023 if (pastNamesMap_ == null) {
1024 pastNamesMap_ = new HashMap<>();
1025 }
1026 pastNamesMap_.put(name, element);
1027 }
1028
1029 /**
1030 * Return the element registered in the past names map with the specified name.
1031 * If the element is no longer owned by this form, the entry is removed and null is returned.
1032 * @param name name or id attribute of the element
1033 * @return the element, or null if not found or no longer owned by this form
1034 */
1035 public HtmlElement getNamedElement(final String name) {
1036 if (pastNamesMap_ == null) {
1037 return null;
1038 }
1039 final HtmlElement element = pastNamesMap_.get(name);
1040 if (element != null && element.getEnclosingForm() != this) {
1041 pastNamesMap_.remove(name);
1042 return null;
1043 }
1044 return element;
1045 }
1046 }