View Javadoc
1   /*
2    * Copyright (c) 2002-2018 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    * http://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 com.gargoylesoftware.htmlunit.javascript.host.html;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.FORMFIELD_REACHABLE_BY_NEW_NAMES;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.FORMFIELD_REACHABLE_BY_ORIGINAL_NAME;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.FORM_SUBMISSION_DOWNLOWDS_ALSO_IF_ONLY_HASH_CHANGED;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FORM_ACTION_EXPANDURL_IGNORE_EMPTY;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FORM_DISPATCHEVENT_SUBMITS;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FORM_REJECT_INVALID_ENCODING;
23  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FORM_SUBMIT_FORCES_DOWNLOAD;
24  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FORM_USABLE_AS_FUNCTION;
25  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
26  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
27  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
28  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
29  
30  import java.net.MalformedURLException;
31  import java.util.ArrayList;
32  import java.util.Iterator;
33  import java.util.List;
34  
35  import org.apache.commons.lang3.StringUtils;
36  
37  import com.gargoylesoftware.htmlunit.BrowserVersion;
38  import com.gargoylesoftware.htmlunit.FormEncodingType;
39  import com.gargoylesoftware.htmlunit.WebAssert;
40  import com.gargoylesoftware.htmlunit.WebClient;
41  import com.gargoylesoftware.htmlunit.WebRequest;
42  import com.gargoylesoftware.htmlunit.html.DomElement;
43  import com.gargoylesoftware.htmlunit.html.DomNode;
44  import com.gargoylesoftware.htmlunit.html.FormFieldWithNameHistory;
45  import com.gargoylesoftware.htmlunit.html.HtmlAttributeChangeEvent;
46  import com.gargoylesoftware.htmlunit.html.HtmlButton;
47  import com.gargoylesoftware.htmlunit.html.HtmlElement;
48  import com.gargoylesoftware.htmlunit.html.HtmlForm;
49  import com.gargoylesoftware.htmlunit.html.HtmlImage;
50  import com.gargoylesoftware.htmlunit.html.HtmlImageInput;
51  import com.gargoylesoftware.htmlunit.html.HtmlInput;
52  import com.gargoylesoftware.htmlunit.html.HtmlPage;
53  import com.gargoylesoftware.htmlunit.html.HtmlSelect;
54  import com.gargoylesoftware.htmlunit.html.HtmlTextArea;
55  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
56  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
57  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
58  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
59  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
60  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
61  import com.gargoylesoftware.htmlunit.protocol.javascript.JavaScriptURLConnection;
62  
63  import net.sourceforge.htmlunit.corejs.javascript.Context;
64  import net.sourceforge.htmlunit.corejs.javascript.Function;
65  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
66  import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
67  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
68  
69  /**
70   * A JavaScript object for {@code HTMLFormElement}.
71   *
72   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
73   * @author Daniel Gredler
74   * @author Kent Tong
75   * @author Chris Erskine
76   * @author Marc Guillemot
77   * @author Ahmed Ashour
78   * @author Sudhan Moghe
79   * @author Ronald Brill
80   * @author Frank Danek
81   *
82   * @see <a href="http://msdn.microsoft.com/en-us/library/ms535249.aspx">MSDN documentation</a>
83   */
84  @JsxClass(domClass = HtmlForm.class)
85  public class HTMLFormElement extends HTMLElement implements Function {
86  
87      /**
88       * Creates an instance.
89       */
90      @JsxConstructor({CHROME, FF, EDGE})
91      public HTMLFormElement() {
92      }
93  
94      /**
95       * {@inheritDoc}
96       */
97      @Override
98      public void setHtmlElement(final HtmlElement htmlElement) {
99          super.setHtmlElement(htmlElement);
100         final HtmlForm htmlForm = getHtmlForm();
101         htmlForm.setScriptableObject(this);
102     }
103 
104     /**
105      * Returns the value of the property {@code name}.
106      * @return the value of this property
107      */
108     @JsxGetter
109     public String getName() {
110         return getHtmlForm().getNameAttribute();
111     }
112 
113     /**
114      * Sets the value of the property {@code name}.
115      * @param name the new value
116      */
117     @JsxSetter
118     public void setName(final String name) {
119         getHtmlForm().setNameAttribute(name);
120     }
121 
122     /**
123      * Returns the value of the property {@code elements}.
124      * @return the value of this property
125      */
126     @JsxGetter
127     public HTMLCollection getElements() {
128         final HtmlForm htmlForm = getHtmlForm();
129 
130         return new HTMLCollection(htmlForm, false) {
131             private boolean filterChildrenOfNestedForms_;
132 
133             @Override
134             protected List<DomNode> computeElements() {
135                 final List<DomNode> response = super.computeElements();
136                 // it would be more performant to avoid iterating through
137                 // nested forms but as it is a corner case of ill formed HTML
138                 // the needed refactoring would take too much time
139                 // => filter here and not in isMatching as it won't be needed in most
140                 // of the cases
141                 if (filterChildrenOfNestedForms_) {
142                     for (final Iterator<DomNode> iter = response.iterator(); iter.hasNext();) {
143                         final HtmlElement field = (HtmlElement) iter.next();
144                         if (field.getEnclosingForm() != htmlForm) {
145                             iter.remove();
146                         }
147                     }
148                 }
149                 response.addAll(htmlForm.getLostChildren());
150                 return response;
151             }
152 
153             @Override
154             protected Object getWithPreemption(final String name) {
155                 return HTMLFormElement.this.getWithPreemption(name);
156             }
157 
158             @Override
159             public EffectOnCache getEffectOnCache(final HtmlAttributeChangeEvent event) {
160                 return EffectOnCache.NONE;
161             }
162 
163             @Override
164             protected boolean isMatching(final DomNode node) {
165                 if (node instanceof HtmlForm) {
166                     filterChildrenOfNestedForms_ = true;
167                     return false;
168                 }
169 
170                 return node instanceof HtmlInput || node instanceof HtmlButton
171                         || node instanceof HtmlTextArea || node instanceof HtmlSelect;
172             }
173         };
174     }
175 
176     /**
177      * Returns the value of the property {@code length}.
178      * Does not count input {@code type=image} elements
179      * (<a href="http://msdn.microsoft.com/en-us/library/ms534101.aspx">MSDN doc</a>)
180      * @return the value of this property
181      */
182     @JsxGetter
183     public int getLength() {
184         final int all = getElements().getLength();
185         final int images = getHtmlForm().getElementsByAttribute(HtmlInput.TAG_NAME, "type", "image").size();
186         return all - images;
187     }
188 
189     /**
190      * Returns the value of the property {@code action}.
191      * @return the value of this property
192      */
193     @JsxGetter
194     public String getAction() {
195         String action = getHtmlForm().getActionAttribute();
196         final BrowserVersion browser = getBrowserVersion();
197         if (action != DomElement.ATTRIBUTE_NOT_DEFINED) {
198             if (action.length() == 0 && browser.hasFeature(JS_FORM_ACTION_EXPANDURL_IGNORE_EMPTY)) {
199                 return action;
200             }
201 
202             try {
203                 action = ((HtmlPage) getHtmlForm().getPage()).getFullyQualifiedUrl(action).toExternalForm();
204             }
205             catch (final MalformedURLException e) {
206                 // nothing, return action attribute
207             }
208         }
209         return action;
210     }
211 
212     /**
213      * Sets the value of the property {@code action}.
214      * @param action the new value
215      */
216     @JsxSetter
217     public void setAction(final String action) {
218         WebAssert.notNull("action", action);
219         getHtmlForm().setActionAttribute(action);
220     }
221 
222     /**
223      * Returns the value of the property {@code method}.
224      * @return the value of this property
225      */
226     @JsxGetter
227     public String getMethod() {
228         return getHtmlForm().getMethodAttribute();
229     }
230 
231     /**
232      * Sets the value of the property {@code method}.
233      * @param method the new property
234      */
235     @JsxSetter
236     public void setMethod(final String method) {
237         WebAssert.notNull("method", method);
238         getHtmlForm().setMethodAttribute(method);
239     }
240 
241     /**
242      * Returns the value of the property {@code target}.
243      * @return the value of this property
244      */
245     @JsxGetter
246     public String getTarget() {
247         return getHtmlForm().getTargetAttribute();
248     }
249 
250     /**
251      * Sets the value of the property {@code target}.
252      * @param target the new value
253      */
254     @JsxSetter
255     public void setTarget(final String target) {
256         WebAssert.notNull("target", target);
257         getHtmlForm().setTargetAttribute(target);
258     }
259 
260     /**
261      * Returns the value of the property {@code enctype}.
262      * @return the value of this property
263      */
264     @JsxGetter
265     public String getEnctype() {
266         final String encoding = getHtmlForm().getEnctypeAttribute();
267         if (!FormEncodingType.URL_ENCODED.getName().equals(encoding)
268                 && !FormEncodingType.MULTIPART.getName().equals(encoding)
269                 && !"text/plain".equals(encoding)) {
270             return FormEncodingType.URL_ENCODED.getName();
271         }
272         return encoding;
273     }
274 
275     /**
276      * Sets the value of the property {@code enctype}.
277      * @param enctype the new value
278      */
279     @JsxSetter
280     public void setEnctype(final String enctype) {
281         WebAssert.notNull("encoding", enctype);
282         if (getBrowserVersion().hasFeature(JS_FORM_REJECT_INVALID_ENCODING)
283                 && !FormEncodingType.URL_ENCODED.getName().equals(enctype)
284                 && !FormEncodingType.MULTIPART.getName().equals(enctype)) {
285             throw Context.reportRuntimeError("Cannot set the encoding property to invalid value: '" + enctype + "'");
286         }
287         getHtmlForm().setEnctypeAttribute(enctype);
288     }
289 
290     /**
291      * Returns the value of the property {@code encoding}.
292      * @return the value of this property
293      */
294     @JsxGetter
295     public String getEncoding() {
296         return getEnctype();
297     }
298 
299     /**
300      * Sets the value of the property {@code encoding}.
301      * @param encoding the new value
302      */
303     @JsxSetter
304     public void setEncoding(final String encoding) {
305         setEnctype(encoding);
306     }
307 
308     /**
309      * @return the associated HtmlForm
310      */
311     public HtmlForm getHtmlForm() {
312         return (HtmlForm) getDomNodeOrDie();
313     }
314 
315     /**
316      * Submits the form (at the end of the current script execution).
317      */
318     @JsxFunction
319     public void submit() {
320         final HtmlPage page = (HtmlPage) getDomNodeOrDie().getPage();
321         final WebClient webClient = page.getWebClient();
322 
323         final String action = getHtmlForm().getActionAttribute().trim();
324         if (StringUtils.startsWithIgnoreCase(action, JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
325             final String js = action.substring(JavaScriptURLConnection.JAVASCRIPT_PREFIX.length());
326             webClient.getJavaScriptEngine().execute(page, js, "Form action", 0);
327         }
328         else {
329             // download should be done ASAP, response will be loaded into a window later
330             final WebRequest request = getHtmlForm().getWebRequest(null);
331             final String target = page.getResolvedTarget(getTarget());
332             final boolean forceDownload = webClient.getBrowserVersion().hasFeature(JS_FORM_SUBMIT_FORCES_DOWNLOAD);
333             final boolean checkHash =
334                     !webClient.getBrowserVersion().hasFeature(FORM_SUBMISSION_DOWNLOWDS_ALSO_IF_ONLY_HASH_CHANGED);
335             webClient.download(page.getEnclosingWindow(),
336                         target, request, checkHash, forceDownload, "JS form.submit()");
337         }
338     }
339 
340     /**
341      * Retrieves a form object or an object from an elements collection.
342      * @param index Integer or String that specifies the object or collection to retrieve.
343      *              If this parameter is an integer, it is the zero-based index of the object.
344      *              If this parameter is a string, all objects with matching name or id properties are retrieved,
345      *              and a collection is returned if more than one match is made
346      * @param subIndex Optional. Integer that specifies the zero-based index of the object to retrieve
347      *              when a collection is returned
348      * @return an object or a collection of objects if successful, or null otherwise
349      */
350     @JsxFunction(IE)
351     public Object item(final Object index, final Object subIndex) {
352         if (index instanceof Number) {
353             return getElements().item(index);
354         }
355 
356         final String name = Context.toString(index);
357         final Object response = getWithPreemption(name);
358         if (subIndex instanceof Number && response instanceof HTMLCollection) {
359             return ((HTMLCollection) response).item(subIndex);
360         }
361 
362         return response;
363     }
364 
365     /**
366      * Resets this form.
367      */
368     @JsxFunction
369     public void reset() {
370         getHtmlForm().reset();
371     }
372 
373     /**
374      * Overridden to allow the retrieval of certain form elements by ID or name.
375      *
376      * @param name {@inheritDoc}
377      * @return {@inheritDoc}
378      */
379     @Override
380     protected Object getWithPreemption(final String name) {
381         if (getDomNodeOrNull() == null) {
382             return NOT_FOUND;
383         }
384         final List<HtmlElement> elements = findElements(name);
385 
386         if (elements.isEmpty()) {
387             return NOT_FOUND;
388         }
389         if (elements.size() == 1) {
390             return getScriptableFor(elements.get(0));
391         }
392         final List<DomNode> nodes = new ArrayList<>(elements);
393         return new HTMLCollection(getHtmlForm(), nodes) {
394             @Override
395             protected List<DomNode> computeElements() {
396                 return new ArrayList<>(findElements(name));
397             }
398         };
399     }
400 
401     private List<HtmlElement> findElements(final String name) {
402         final List<HtmlElement> elements = new ArrayList<>();
403         addElements(name, getHtmlForm().getHtmlElementDescendants(), elements);
404         addElements(name, getHtmlForm().getLostChildren(), elements);
405 
406         // If no form fields are found, IE and Firefox are able to find img elements by ID or name.
407         if (elements.isEmpty()) {
408             for (final DomNode node : getHtmlForm().getChildren()) {
409                 if (node instanceof HtmlImage) {
410                     final HtmlImage img = (HtmlImage) node;
411                     if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
412                         elements.add(img);
413                     }
414                 }
415             }
416         }
417 
418         return elements;
419     }
420 
421     private void addElements(final String name, final Iterable<HtmlElement> nodes,
422         final List<HtmlElement> addTo) {
423         for (final HtmlElement node : nodes) {
424             if (isAccessibleByIdOrName(node, name)) {
425                 addTo.add(node);
426             }
427         }
428     }
429 
430     /**
431      * Indicates if the element can be reached by id or name in expressions like "myForm.myField".
432      * @param element the element to test
433      * @param name the name used to address the element
434      * @return {@code true} if this element matches the conditions
435      */
436     private boolean isAccessibleByIdOrName(final HtmlElement element, final String name) {
437         if (element instanceof FormFieldWithNameHistory && !(element instanceof HtmlImageInput)) {
438             if (element.getEnclosingForm() != getHtmlForm()) {
439                 return false; // nested forms
440             }
441             if (name.equals(element.getId())) {
442                 return true;
443             }
444             final FormFieldWithNameHistory elementWithNames = (FormFieldWithNameHistory) element;
445             if (getBrowserVersion().hasFeature(FORMFIELD_REACHABLE_BY_ORIGINAL_NAME)) {
446                 if (name.equals(elementWithNames.getOriginalName())) {
447                     return true;
448                 }
449             }
450             else if (name.equals(element.getAttributeDirect("name"))) {
451                 return true;
452             }
453 
454             if (getBrowserVersion().hasFeature(FORMFIELD_REACHABLE_BY_NEW_NAMES)
455                     && elementWithNames.getNewNames().contains(name)) {
456                 return true;
457             }
458         }
459         return false;
460     }
461 
462     /**
463      * Returns the specified indexed property.
464      * @param index the index of the property
465      * @param start the scriptable object that was originally queried for this property
466      * @return the property
467      */
468     @Override
469     public Object get(final int index, final Scriptable start) {
470         if (getDomNodeOrNull() == null) {
471             return NOT_FOUND; // typically for the prototype
472         }
473         return getElements().get(index, ((HTMLFormElement) start).getElements());
474     }
475 
476     /**
477      * {@inheritDoc}
478      */
479     @Override
480     public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
481         if (!getBrowserVersion().hasFeature(JS_FORM_USABLE_AS_FUNCTION)) {
482             throw Context.reportRuntimeError("Not a function.");
483         }
484         if (args.length > 0) {
485             final Object arg = args[0];
486             if (arg instanceof String) {
487                 return ScriptableObject.getProperty(this, (String) arg);
488             }
489             else if (arg instanceof Number) {
490                 return ScriptableObject.getProperty(this, ((Number) arg).intValue());
491             }
492         }
493         return Undefined.instance;
494     }
495 
496     /**
497      * {@inheritDoc}
498      */
499     @Override
500     public Scriptable construct(final Context cx, final Scriptable scope, final Object[] args) {
501         if (!getBrowserVersion().hasFeature(JS_FORM_USABLE_AS_FUNCTION)) {
502             throw Context.reportRuntimeError("Not a function.");
503         }
504         return null;
505     }
506 
507     @Override
508     public boolean dispatchEvent(final Event event) {
509         final boolean result = super.dispatchEvent(event);
510 
511         if (Event.TYPE_SUBMIT.equals(event.getType())
512                 && getBrowserVersion().hasFeature(JS_FORM_DISPATCHEVENT_SUBMITS)) {
513             submit();
514         }
515         return result;
516     }
517 
518     /**
519      * Checks whether the element has any constraints and whether it satisfies them.
520      * @return if the element is valid
521      */
522     @JsxFunction
523     public boolean checkValidity() {
524         return getDomNodeOrDie().isValid();
525     }
526 
527 }