View Javadoc
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.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.html.DomElement;
32  import org.htmlunit.html.DomNode;
33  import org.htmlunit.html.FormFieldWithNameHistory;
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 <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
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,
110                 false) {
111             @Override
112             protected Object getWithPreemption(final String name) {
113                 return HTMLFormElement.this.getWithPreemption(name);
114             }
115         };
116 
117         elements.setElementsSupplier(
118                 (Supplier<List<DomNode>> & Serializable)
119                 () -> {
120                     final DomNode domNode = getDomNodeOrNull();
121                     if (domNode == null) {
122                         return new ArrayList<>();
123                     }
124                     return new ArrayList<>(((HtmlForm) domNode).getElementsJS());
125                 });
126 
127         elements.setEffectOnCacheFunction(
128                 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
129                 event -> EffectOnCache.NONE);
130 
131         return elements;
132     }
133 
134     /**
135      * @return the Iterator symbol
136      */
137     @JsxSymbol
138     public Scriptable iterator() {
139         return getElements().iterator();
140     }
141 
142     /**
143      * Returns the value of the property {@code length}.
144      * Does not count input {@code type=image} elements
145      * (<a href="http://msdn.microsoft.com/en-us/library/ms534101.aspx">MSDN doc</a>)
146      * @return the value of this property
147      */
148     @JsxGetter
149     public int getLength() {
150         return getElements().getLength();
151     }
152 
153     /**
154      * Returns the value of the property {@code action}.
155      * @return the value of this property
156      */
157     @JsxGetter
158     public String getAction() {
159         final String action = getHtmlForm().getActionAttribute();
160 
161         try {
162             return ((HtmlPage) getHtmlForm().getPage()).getFullyQualifiedUrl(action).toExternalForm();
163         }
164         catch (final MalformedURLException ignored) {
165             // nothing, return action attribute
166         }
167         return action;
168     }
169 
170     /**
171      * Sets the value of the property {@code action}.
172      * @param action the new value
173      */
174     @JsxSetter
175     public void setAction(final String action) {
176         WebAssert.notNull("action", action);
177         getHtmlForm().setActionAttribute(action);
178     }
179 
180     /**
181      * Returns the value of the property {@code method}.
182      * @return the value of this property
183      */
184     @JsxGetter
185     public String getMethod() {
186         return getHtmlForm().getMethodAttribute();
187     }
188 
189     /**
190      * Sets the value of the property {@code method}.
191      * @param method the new property
192      */
193     @JsxSetter
194     public void setMethod(final String method) {
195         WebAssert.notNull("method", method);
196         getHtmlForm().setMethodAttribute(method);
197     }
198 
199     /**
200      * Returns the value of the property {@code target}.
201      * @return the value of this property
202      */
203     @JsxGetter
204     public String getTarget() {
205         return getHtmlForm().getTargetAttribute();
206     }
207 
208     /**
209      * Sets the value of the property {@code target}.
210      * @param target the new value
211      */
212     @JsxSetter
213     public void setTarget(final String target) {
214         WebAssert.notNull("target", target);
215         getHtmlForm().setTargetAttribute(target);
216     }
217 
218     /**
219      * Returns the value of the rel property.
220      * @return the rel property
221      */
222     @JsxGetter
223     public String getRel() {
224         return getHtmlForm().getRelAttribute();
225     }
226 
227     /**
228      * Sets the rel property.
229      * @param rel rel attribute value
230      */
231     @JsxSetter
232     public void setRel(final String rel) {
233         getHtmlForm().setAttribute("rel", rel);
234     }
235 
236     /**
237      * Returns the {@code relList} attribute.
238      * @return the {@code relList} attribute
239      */
240     @JsxGetter
241     public DOMTokenList getRelList() {
242         return new DOMTokenList(this, "rel");
243     }
244 
245     /**
246      * Sets the relList property.
247      * @param rel attribute value
248      */
249     @JsxSetter
250     public void setRelList(final Object rel) {
251         if (JavaScriptEngine.isUndefined(rel)) {
252             setRel("undefined");
253             return;
254         }
255         setRel(JavaScriptEngine.toString(rel));
256     }
257 
258     /**
259      * Returns the value of the property {@code enctype}.
260      * @return the value of this property
261      */
262     @JsxGetter
263     public String getEnctype() {
264         final String encoding = getHtmlForm().getEnctypeAttribute();
265         if (!FormEncodingType.URL_ENCODED.getName().equals(encoding)
266                 && !FormEncodingType.MULTIPART.getName().equals(encoding)
267                 && !MimeType.TEXT_PLAIN.equals(encoding)) {
268             return FormEncodingType.URL_ENCODED.getName();
269         }
270         return encoding;
271     }
272 
273     /**
274      * Sets the value of the property {@code enctype}.
275      * @param enctype the new value
276      */
277     @JsxSetter
278     public void setEnctype(final String enctype) {
279         WebAssert.notNull("encoding", enctype);
280         getHtmlForm().setEnctypeAttribute(enctype);
281     }
282 
283     /**
284      * Returns the value of the property {@code encoding}.
285      * @return the value of this property
286      */
287     @JsxGetter
288     public String getEncoding() {
289         return getEnctype();
290     }
291 
292     /**
293      * Sets the value of the property {@code encoding}.
294      * @param encoding the new value
295      */
296     @JsxSetter
297     public void setEncoding(final String encoding) {
298         setEnctype(encoding);
299     }
300 
301     /**
302      * @return the associated HtmlForm
303      */
304     public HtmlForm getHtmlForm() {
305         return (HtmlForm) getDomNodeOrDie();
306     }
307 
308     /**
309      * Submits the form (at the end of the current script execution).
310      */
311     @JsxFunction
312     public void submit() {
313         getHtmlForm().submit(null);
314     }
315 
316     /**
317      * Submits the form by submitted using a specific submit button.
318      * @param submitter The submit button whose attributes describe the method
319      *        by which the form is to be submitted. This may be either
320      *        an &lt;input&gt; or &lt;button&gt; element whose type attribute is submit.
321      *        If you omit the submitter parameter, the form element itself is used as the submitter.
322      */
323     @JsxFunction
324     public void requestSubmit(final Object submitter) {
325         if (JavaScriptEngine.isUndefined(submitter)) {
326             submit();
327             return;
328         }
329 
330         SubmittableElement submittable = null;
331         if (submitter instanceof HTMLElement) {
332             final HTMLElement subHtmlElement = (HTMLElement) submitter;
333             if (subHtmlElement instanceof HTMLButtonElement) {
334                 if ("submit".equals(((HTMLButtonElement) subHtmlElement).getType())) {
335                     submittable = (SubmittableElement) subHtmlElement.getDomNodeOrDie();
336                 }
337             }
338             else if (subHtmlElement instanceof HTMLInputElement) {
339                 if ("submit".equals(((HTMLInputElement) subHtmlElement).getType())) {
340                     submittable = (SubmittableElement) subHtmlElement.getDomNodeOrDie();
341                 }
342             }
343 
344             if (submittable != null && subHtmlElement.getForm() != this) {
345                 throw JavaScriptEngine.typeError(
346                         "Failed to execute 'requestSubmit' on 'HTMLFormElement': "
347                         + "The specified element is not owned by this form element.");
348             }
349         }
350 
351         if (submittable == null) {
352             throw JavaScriptEngine.typeError(
353                     "Failed to execute 'requestSubmit' on 'HTMLFormElement': "
354                     + "The specified element is not a submit button.");
355         }
356 
357         this.getHtmlForm().submit(submittable);
358     }
359 
360     /**
361      * Resets this form.
362      */
363     @JsxFunction
364     public void reset() {
365         getHtmlForm().reset();
366     }
367 
368     /**
369      * Overridden to allow the retrieval of certain form elements by ID or name.
370      *
371      * @param name {@inheritDoc}
372      * @return {@inheritDoc}
373      */
374     @Override
375     protected Object getWithPreemption(final String name) {
376         if (getDomNodeOrNull() == null) {
377             return NOT_FOUND;
378         }
379         final List<HtmlElement> elements = findElements(name);
380 
381         if (elements.isEmpty()) {
382             return NOT_FOUND;
383         }
384         if (elements.size() == 1) {
385             return getScriptableFor(elements.get(0));
386         }
387         final List<DomNode> nodes = new ArrayList<>(elements);
388 
389         final RadioNodeList nodeList = new RadioNodeList(getHtmlForm(), nodes);
390         nodeList.setElementsSupplier(
391                 (Supplier<List<DomNode>> & Serializable)
392                 () -> new ArrayList<>(findElements(name)));
393         return nodeList;
394     }
395 
396     /**
397      * Overridden to allow the retrieval of certain form elements by ID or name.
398      *
399      * @param name {@inheritDoc}
400      * @param start {@inheritDoc}
401      * @return {@inheritDoc}
402      */
403     @Override
404     public boolean has(final String name, final Scriptable start) {
405         if (super.has(name, start)) {
406             return true;
407         }
408 
409         return findFirstElement(name) != null;
410     }
411 
412     /**
413      * Overridden to allow the retrieval of certain form elements by ID or name.
414      *
415      * @param cx {@inheritDoc}
416      * @param id {@inheritDoc}
417      * @return {@inheritDoc}
418      */
419     @Override
420     protected ScriptableObject getOwnPropertyDescriptor(final Context cx, final Object id) {
421         final ScriptableObject desc = super.getOwnPropertyDescriptor(cx, id);
422         if (desc != null) {
423             return desc;
424         }
425 
426         if (id instanceof CharSequence) {
427             final HtmlElement element = findFirstElement(id.toString());
428             if (element != null) {
429                 return ScriptableObject.buildDataDescriptor(this, element.getScriptableObject(),
430                                             ScriptableObject.READONLY | ScriptableObject.DONTENUM);
431             }
432         }
433 
434         return null;
435     }
436 
437     List<HtmlElement> findElements(final String name) {
438         final List<HtmlElement> elements = new ArrayList<>();
439         final HtmlForm form = (HtmlForm) getDomNodeOrNull();
440         if (form == null) {
441             return elements;
442         }
443 
444         for (final HtmlElement element : form.getElementsJS()) {
445             if (isAccessibleByIdOrName(element, name)) {
446                 elements.add(element);
447             }
448         }
449 
450         // If no form fields are found, browsers are able to find img elements by ID or name.
451         if (elements.isEmpty()) {
452             for (final DomNode node : form.getHtmlElementDescendants()) {
453                 if (node instanceof HtmlImage) {
454                     final HtmlImage img = (HtmlImage) node;
455                     if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
456                         elements.add(img);
457                     }
458                 }
459             }
460         }
461 
462         return elements;
463     }
464 
465     private HtmlElement findFirstElement(final String name) {
466         final HtmlForm form = (HtmlForm) getDomNodeOrNull();
467         if (form == null) {
468             return null;
469         }
470 
471         for (final HtmlElement node : form.getElementsJS()) {
472             if (isAccessibleByIdOrName(node, name)) {
473                 return node;
474             }
475         }
476 
477         // If no form fields are found, browsers are able to find img elements by ID or name.
478         for (final DomNode node : form.getHtmlElementDescendants()) {
479             if (node instanceof HtmlImage) {
480                 final HtmlImage img = (HtmlImage) node;
481                 if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
482                     return img;
483                 }
484             }
485         }
486 
487         return null;
488     }
489 
490     /**
491      * Indicates if the element can be reached by id or name in expressions like "myForm.myField".
492      * @param element the element to test
493      * @param name the name used to address the element
494      * @return {@code true} if this element matches the conditions
495      */
496     private static boolean isAccessibleByIdOrName(final HtmlElement element, final String name) {
497         if (name.equals(element.getId())) {
498             return true;
499         }
500 
501         if (name.equals(element.getAttributeDirect(DomElement.NAME_ATTRIBUTE))) {
502             return true;
503         }
504 
505         if (element instanceof FormFieldWithNameHistory) {
506             final FormFieldWithNameHistory elementWithNames = (FormFieldWithNameHistory) element;
507 
508             if (name.equals(elementWithNames.getOriginalName())) {
509                 return true;
510             }
511 
512             if (elementWithNames.getNewNames().contains(name)) {
513                 return true;
514             }
515         }
516 
517         return false;
518     }
519 
520     /**
521      * Returns the specified indexed property.
522      * @param index the index of the property
523      * @param start the scriptable object that was originally queried for this property
524      * @return the property
525      */
526     @Override
527     public Object get(final int index, final Scriptable start) {
528         if (getDomNodeOrNull() == null) {
529             return NOT_FOUND; // typically for the prototype
530         }
531         return getElements().get(index, ((HTMLFormElement) start).getElements());
532     }
533 
534     /**
535      * {@inheritDoc}
536      */
537     @Override
538     public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
539         throw JavaScriptEngine.typeError("Not a function.");
540     }
541 
542     /**
543      * {@inheritDoc}
544      */
545     @Override
546     public Scriptable construct(final Context cx, final Scriptable scope, final Object[] args) {
547         throw JavaScriptEngine.typeError("Not a function.");
548     }
549 
550     @Override
551     public boolean dispatchEvent(final Event event) {
552         final boolean result = super.dispatchEvent(event);
553 
554         if (Event.TYPE_SUBMIT.equals(event.getType())
555                 && getBrowserVersion().hasFeature(JS_FORM_DISPATCHEVENT_SUBMITS)) {
556             submit();
557         }
558         return result;
559     }
560 
561     /**
562      * Checks whether the element has any constraints and whether it satisfies them.
563      * @return if the element is valid
564      */
565     @JsxFunction
566     public boolean checkValidity() {
567         return getDomNodeOrDie().isValid();
568     }
569 
570     /**
571      * Returns the value of the property {@code novalidate}.
572      * @return the value of this property
573      */
574     @JsxGetter
575     public boolean isNoValidate() {
576         return getHtmlForm().isNoValidate();
577     }
578 
579     /**
580      * Sets the value of the property {@code novalidate}.
581      * @param value the new value
582      */
583     @JsxSetter
584     public void setNoValidate(final boolean value) {
585         getHtmlForm().setNoValidate(value);
586     }
587 }