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