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