View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.JS_FORM_DISPATCHEVENT_SUBMITS;
18  
19  import java.io.Serializable;
20  import java.net.MalformedURLException;
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.function.Supplier;
24  
25  import org.htmlunit.FormEncodingType;
26  import org.htmlunit.WebAssert;
27  import org.htmlunit.corejs.javascript.Context;
28  import org.htmlunit.corejs.javascript.Function;
29  import org.htmlunit.corejs.javascript.Scriptable;
30  import org.htmlunit.corejs.javascript.ScriptableObject;
31  import org.htmlunit.corejs.javascript.VarScope;
32  import org.htmlunit.html.DomElement;
33  import org.htmlunit.html.DomNode;
34  import org.htmlunit.html.HtmlAttributeChangeEvent;
35  import org.htmlunit.html.HtmlElement;
36  import org.htmlunit.html.HtmlForm;
37  import org.htmlunit.html.HtmlImage;
38  import org.htmlunit.html.HtmlPage;
39  import org.htmlunit.html.SubmittableElement;
40  import org.htmlunit.javascript.JavaScriptEngine;
41  import org.htmlunit.javascript.configuration.JsxClass;
42  import org.htmlunit.javascript.configuration.JsxConstructor;
43  import org.htmlunit.javascript.configuration.JsxFunction;
44  import org.htmlunit.javascript.configuration.JsxGetter;
45  import org.htmlunit.javascript.configuration.JsxSetter;
46  import org.htmlunit.javascript.configuration.JsxSymbol;
47  import org.htmlunit.javascript.host.dom.AbstractList.EffectOnCache;
48  import org.htmlunit.javascript.host.dom.DOMTokenList;
49  import org.htmlunit.javascript.host.dom.RadioNodeList;
50  import org.htmlunit.javascript.host.event.Event;
51  import org.htmlunit.util.MimeType;
52  
53  /**
54   * A JavaScript object {@code HTMLFormElement}.
55   *
56   * @author Mike Bowler
57   * @author Daniel Gredler
58   * @author Kent Tong
59   * @author Chris Erskine
60   * @author Marc Guillemot
61   * @author Ahmed Ashour
62   * @author Sudhan Moghe
63   * @author Ronald Brill
64   * @author Frank Danek
65   * @author Lai Quang Duong
66   *
67   * @see <a href="http://msdn.microsoft.com/en-us/library/ms535249.aspx">MSDN documentation</a>
68   */
69  @JsxClass(domClass = HtmlForm.class)
70  public class HTMLFormElement extends HTMLElement implements Function {
71  
72      /**
73       * JavaScript constructor.
74       */
75      @Override
76      @JsxConstructor
77      public void jsConstructor() {
78          super.jsConstructor();
79      }
80  
81      /**
82       * Returns the value of the property {@code name}.
83       * @return the value of this property
84       */
85      @JsxGetter
86      @Override
87      public String getName() {
88          return getHtmlForm().getNameAttribute();
89      }
90  
91      /**
92       * Sets the value of the property {@code name}.
93       * @param name the new value
94       */
95      @JsxSetter
96      @Override
97      public void setName(final String name) {
98          getHtmlForm().setNameAttribute(name);
99      }
100 
101     /**
102      * Returns the value of the property {@code elements}.
103      * @return the value of this property
104      */
105     @JsxGetter
106     public HTMLFormControlsCollection getElements() {
107         final HtmlForm htmlForm = getHtmlForm();
108 
109         final HTMLFormControlsCollection elements = new HTMLFormControlsCollection(htmlForm, false) {
110             @Override
111             protected Object getWithPreemption(final String name) {
112                 final List<HtmlElement> elementsForName = findElements(name);
113                 if (elementsForName.isEmpty()) {
114                     return NOT_FOUND;
115                 }
116                 if (elementsForName.size() == 1) {
117                     return getScriptableFor(elementsForName.get(0));
118                 }
119 
120                 final List<DomNode> nodes = new ArrayList<>(elementsForName);
121                 final RadioNodeList nodeList = new RadioNodeList(getHtmlForm(), nodes);
122                 nodeList.setElementsSupplier(
123                         (Supplier<List<DomNode>> & Serializable) () -> new ArrayList<>(findElements(name)));
124                 return nodeList;
125             }
126         };
127 
128         elements.setElementsSupplier(
129                 (Supplier<List<DomNode>> & Serializable)
130                 () -> {
131                     final DomNode domNode = getDomNodeOrNull();
132                     if (domNode == null) {
133                         return new ArrayList<>();
134                     }
135                     return new ArrayList<>(((HtmlForm) domNode).getElementsJS());
136                 });
137 
138         elements.setEffectOnCacheFunction(
139                 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
140                 event -> EffectOnCache.NONE);
141 
142         return elements;
143     }
144 
145     /**
146      * @return the Iterator symbol
147      */
148     @JsxSymbol
149     public Scriptable iterator() {
150         return getElements().iterator();
151     }
152 
153     /**
154      * Returns the value of the property {@code length}.
155      * Does not count input {@code type=image} elements
156      * (<a href="http://msdn.microsoft.com/en-us/library/ms534101.aspx">MSDN doc</a>)
157      * @return the value of this property
158      */
159     @JsxGetter
160     public int getLength() {
161         return getElements().getLength();
162     }
163 
164     /**
165      * Returns the value of the property {@code action}.
166      * @return the value of this property
167      */
168     @JsxGetter
169     public String getAction() {
170         final String action = getHtmlForm().getActionAttribute();
171 
172         try {
173             return ((HtmlPage) getHtmlForm().getPage()).getFullyQualifiedUrl(action).toExternalForm();
174         }
175         catch (final MalformedURLException ignored) {
176             // nothing, return action attribute
177         }
178         return action;
179     }
180 
181     /**
182      * Sets the value of the property {@code action}.
183      * @param action the new value
184      */
185     @JsxSetter
186     public void setAction(final String action) {
187         WebAssert.notNull("action", action);
188         getHtmlForm().setActionAttribute(action);
189     }
190 
191     /**
192      * Returns the value of the property {@code method}.
193      * @return the value of this property
194      */
195     @JsxGetter
196     public String getMethod() {
197         return getHtmlForm().getMethodAttribute();
198     }
199 
200     /**
201      * Sets the value of the property {@code method}.
202      * @param method the new property
203      */
204     @JsxSetter
205     public void setMethod(final String method) {
206         WebAssert.notNull("method", method);
207         getHtmlForm().setMethodAttribute(method);
208     }
209 
210     /**
211      * Returns the value of the property {@code target}.
212      * @return the value of this property
213      */
214     @JsxGetter
215     public String getTarget() {
216         return getHtmlForm().getTargetAttribute();
217     }
218 
219     /**
220      * Sets the value of the property {@code target}.
221      * @param target the new value
222      */
223     @JsxSetter
224     public void setTarget(final String target) {
225         WebAssert.notNull("target", target);
226         getHtmlForm().setTargetAttribute(target);
227     }
228 
229     /**
230      * Returns the value of the rel property.
231      * @return the rel property
232      */
233     @JsxGetter
234     public String getRel() {
235         return getHtmlForm().getRelAttribute();
236     }
237 
238     /**
239      * Sets the rel property.
240      * @param rel rel attribute value
241      */
242     @JsxSetter
243     public void setRel(final String rel) {
244         getHtmlForm().setAttribute("rel", rel);
245     }
246 
247     /**
248      * Returns the {@code relList} attribute.
249      * @return the {@code relList} attribute
250      */
251     @JsxGetter
252     public DOMTokenList getRelList() {
253         return new DOMTokenList(this, "rel");
254     }
255 
256     /**
257      * Sets the relList property.
258      * @param rel attribute value
259      */
260     @JsxSetter
261     public void setRelList(final Object rel) {
262         if (JavaScriptEngine.isUndefined(rel)) {
263             setRel("undefined");
264             return;
265         }
266         setRel(JavaScriptEngine.toString(rel));
267     }
268 
269     /**
270      * Returns the value of the property {@code enctype}.
271      * @return the value of this property
272      */
273     @JsxGetter
274     public String getEnctype() {
275         final String encoding = getHtmlForm().getEnctypeAttribute();
276         if (!FormEncodingType.URL_ENCODED.getName().equals(encoding)
277                 && !FormEncodingType.MULTIPART.getName().equals(encoding)
278                 && !MimeType.TEXT_PLAIN.equals(encoding)) {
279             return FormEncodingType.URL_ENCODED.getName();
280         }
281         return encoding;
282     }
283 
284     /**
285      * Sets the value of the property {@code enctype}.
286      * @param enctype the new value
287      */
288     @JsxSetter
289     public void setEnctype(final String enctype) {
290         WebAssert.notNull("encoding", enctype);
291         getHtmlForm().setEnctypeAttribute(enctype);
292     }
293 
294     /**
295      * Returns the value of the property {@code encoding}.
296      * @return the value of this property
297      */
298     @JsxGetter
299     public String getEncoding() {
300         return getEnctype();
301     }
302 
303     /**
304      * Sets the value of the property {@code encoding}.
305      * @param encoding the new value
306      */
307     @JsxSetter
308     public void setEncoding(final String encoding) {
309         setEnctype(encoding);
310     }
311 
312     /**
313      * @return the associated HtmlForm
314      */
315     public HtmlForm getHtmlForm() {
316         return (HtmlForm) getDomNodeOrDie();
317     }
318 
319     /**
320      * Submits the form (at the end of the current script execution).
321      */
322     @JsxFunction
323     public void submit() {
324         getHtmlForm().submit(null);
325     }
326 
327     /**
328      * Submits the form by submitted using a specific submit button.
329      * @param submitter The submit button whose attributes describe the method
330      *        by which the form is to be submitted. This may be either
331      *        a &lt;input&gt; or &lt;button&gt; element whose type attribute is submit.
332      *        If you omit the submitter parameter, the form element itself is used as the submitter.
333      */
334     @JsxFunction
335     public void requestSubmit(final Object submitter) {
336         if (JavaScriptEngine.isUndefined(submitter)) {
337             submit();
338             return;
339         }
340 
341         SubmittableElement submittable = null;
342         if (submitter instanceof HTMLElement subHtmlElement) {
343             if (subHtmlElement instanceof HTMLButtonElement element1) {
344                 if ("submit".equals(element1.getType())) {
345                     submittable = (SubmittableElement) subHtmlElement.getDomNodeOrDie();
346                 }
347             }
348             else if (subHtmlElement instanceof HTMLInputElement element) {
349                 if ("submit".equals(element.getType())) {
350                     submittable = (SubmittableElement) subHtmlElement.getDomNodeOrDie();
351                 }
352             }
353 
354             if (submittable != null && subHtmlElement.getForm() != this) {
355                 throw JavaScriptEngine.typeError(
356                         "Failed to execute 'requestSubmit' on 'HTMLFormElement': "
357                         + "The specified element is not owned by this form element.");
358             }
359         }
360 
361         if (submittable == null) {
362             throw JavaScriptEngine.typeError(
363                     "Failed to execute 'requestSubmit' on 'HTMLFormElement': "
364                     + "The specified element is not a submit button.");
365         }
366 
367         this.getHtmlForm().submit(submittable);
368     }
369 
370     /**
371      * Resets this form.
372      */
373     @JsxFunction
374     public void reset() {
375         getHtmlForm().reset();
376     }
377 
378     /**
379      * Overridden to allow the retrieval of certain form elements by ID or name.
380      * @see <a href="https://html.spec.whatwg.org/multipage/forms.html#dom-form-nameditem">
381      *     HTML spec - form named item</a>
382      *
383      * @param name {@inheritDoc}
384      * @return {@inheritDoc}
385      */
386     @Override
387     protected Object getWithPreemption(final String name) {
388         if (getDomNodeOrNull() == null) {
389             return NOT_FOUND;
390         }
391         final List<HtmlElement> elements = findElements(name);
392 
393         if (elements.isEmpty()) {
394             final HtmlElement element = getHtmlForm().getNamedElement(name);
395             return element != null ? getScriptableFor(element) : NOT_FOUND;
396         }
397         if (elements.size() == 1) {
398             final HtmlElement element = elements.get(0);
399             getHtmlForm().registerPastName(name, element);
400             return getScriptableFor(element);
401         }
402         final List<DomNode> nodes = new ArrayList<>(elements);
403 
404         final RadioNodeList nodeList = new RadioNodeList(getHtmlForm(), nodes);
405         nodeList.setElementsSupplier(
406                 (Supplier<List<DomNode>> & Serializable)
407                 () -> new ArrayList<>(findElements(name)));
408         return nodeList;
409     }
410 
411     /**
412      * Overridden to allow the retrieval of certain form elements by ID or name.
413      *
414      * @param name {@inheritDoc}
415      * @param start {@inheritDoc}
416      * @return {@inheritDoc}
417      */
418     @Override
419     public boolean has(final String name, final Scriptable start) {
420         if (super.has(name, start)) {
421             return true;
422         }
423 
424         return findFirstElement(name) != null;
425     }
426 
427     /**
428      * Overridden to allow the retrieval of certain form elements by ID or name.
429      *
430      * @param cx {@inheritDoc}
431      * @param id {@inheritDoc}
432      * @return {@inheritDoc}
433      */
434     @Override
435     protected DescriptorInfo getOwnPropertyDescriptor(final Context cx, final Object id) {
436         final DescriptorInfo descInfo = super.getOwnPropertyDescriptor(cx, id);
437         if (descInfo != null) {
438             return descInfo;
439         }
440 
441         if (id instanceof CharSequence) {
442             final HtmlElement element = findFirstElement(id.toString());
443             if (element != null) {
444                 return ScriptableObject.buildDataDescriptor(element.getScriptableObject(),
445                                             ScriptableObject.READONLY | ScriptableObject.DONTENUM);
446             }
447         }
448 
449         return null;
450     }
451 
452     List<HtmlElement> findElements(final String name) {
453         final List<HtmlElement> elements = new ArrayList<>();
454         final HtmlForm form = (HtmlForm) getDomNodeOrNull();
455         if (form == null) {
456             return elements;
457         }
458 
459         for (final HtmlElement element : form.getElementsJS()) {
460             if (name.equals(element.getId())
461                     || name.equals(element.getAttributeDirect(DomElement.NAME_ATTRIBUTE))) {
462                 elements.add(element);
463             }
464         }
465 
466         // If no form fields are found, browsers are able to find img elements by ID or name.
467         if (elements.isEmpty()) {
468             for (final DomNode node : form.getHtmlElementDescendants()) {
469                 if (node instanceof HtmlImage img) {
470                     if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
471                         elements.add(img);
472                     }
473                 }
474             }
475         }
476 
477         return elements;
478     }
479 
480     private HtmlElement findFirstElement(final String name) {
481         final HtmlForm form = (HtmlForm) getDomNodeOrNull();
482         if (form == null) {
483             return null;
484         }
485 
486         for (final HtmlElement node : form.getElementsJS()) {
487             if (name.equals(node.getId())
488                     || name.equals(node.getAttributeDirect(DomElement.NAME_ATTRIBUTE))) {
489                 return node;
490             }
491         }
492 
493         // If no form fields are found, browsers are able to find img elements by ID or name.
494         for (final DomNode node : form.getHtmlElementDescendants()) {
495             if (node instanceof HtmlImage img) {
496                 if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
497                     return img;
498                 }
499             }
500         }
501 
502         return null;
503     }
504 
505     /**
506      * Returns the specified indexed property.
507      * @param index the index of the property
508      * @param start the scriptable object that was originally queried for this property
509      * @return the property
510      */
511     @Override
512     public Object get(final int index, final Scriptable start) {
513         if (getDomNodeOrNull() == null) {
514             return NOT_FOUND; // typically for the prototype
515         }
516         return getElements().get(index, ((HTMLFormElement) start).getElements());
517     }
518 
519     /**
520      * {@inheritDoc}
521      */
522     @Override
523     public Object call(final Context cx, final VarScope scope, final Scriptable thisObj, final Object[] args) {
524         throw JavaScriptEngine.typeError("Not a function.");
525     }
526 
527     /**
528      * {@inheritDoc}
529      */
530     @Override
531     public Scriptable construct(final Context cx, final VarScope scope, final Object[] args) {
532         throw JavaScriptEngine.typeError("Not a function.");
533     }
534 
535     @Override
536     public boolean dispatchEvent(final Event event) {
537         final boolean result = super.dispatchEvent(event);
538 
539         if (Event.TYPE_SUBMIT.equals(event.getType())
540                 && getBrowserVersion().hasFeature(JS_FORM_DISPATCHEVENT_SUBMITS)) {
541             submit();
542         }
543         return result;
544     }
545 
546     /**
547      * Checks whether the element has any constraints and whether it satisfies them.
548      * @return if the element is valid
549      */
550     @JsxFunction
551     public boolean checkValidity() {
552         return getDomNodeOrDie().isValid();
553     }
554 
555     /**
556      * Returns the value of the property {@code novalidate}.
557      * @return the value of this property
558      */
559     @JsxGetter
560     public boolean isNoValidate() {
561         return getHtmlForm().isNoValidate();
562     }
563 
564     /**
565      * Sets the value of the property {@code novalidate}.
566      * @param value the new value
567      */
568     @JsxSetter
569     public void setNoValidate(final boolean value) {
570         getHtmlForm().setNoValidate(value);
571     }
572 }