View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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             if (!FormEncodingType.URL_ENCODED.getName().equals(enctype)
284                     && !FormEncodingType.MULTIPART.getName().equals(enctype)) {
285                 throw Context.reportRuntimeError("Cannot set the encoding property to invalid value: '"
286                         + enctype + "'");
287             }
288         }
289         getHtmlForm().setEnctypeAttribute(enctype);
290     }
291 
292     /**
293      * Returns the value of the property {@code encoding}.
294      * @return the value of this property
295      */
296     @JsxGetter
297     public String getEncoding() {
298         return getEnctype();
299     }
300 
301     /**
302      * Sets the value of the property {@code encoding}.
303      * @param encoding the new value
304      */
305     @JsxSetter
306     public void setEncoding(final String encoding) {
307         setEnctype(encoding);
308     }
309 
310     /**
311      * @return the associated HtmlForm
312      */
313     public HtmlForm getHtmlForm() {
314         return (HtmlForm) getDomNodeOrDie();
315     }
316 
317     /**
318      * Submits the form (at the end of the current script execution).
319      */
320     @JsxFunction
321     public void submit() {
322         final HtmlPage page = (HtmlPage) getDomNodeOrDie().getPage();
323         final WebClient webClient = page.getWebClient();
324 
325         final String action = getHtmlForm().getActionAttribute().trim();
326         if (StringUtils.startsWithIgnoreCase(action, JavaScriptURLConnection.JAVASCRIPT_PREFIX)) {
327             final String js = action.substring(JavaScriptURLConnection.JAVASCRIPT_PREFIX.length());
328             webClient.getJavaScriptEngine().execute(page, js, "Form action", 0);
329         }
330         else {
331             // download should be done ASAP, response will be loaded into a window later
332             final WebRequest request = getHtmlForm().getWebRequest(null);
333             final String target = page.getResolvedTarget(getTarget());
334             final boolean forceDownload = webClient.getBrowserVersion().hasFeature(JS_FORM_SUBMIT_FORCES_DOWNLOAD);
335             final boolean checkHash =
336                     !webClient.getBrowserVersion().hasFeature(FORM_SUBMISSION_DOWNLOWDS_ALSO_IF_ONLY_HASH_CHANGED);
337             webClient.download(page.getEnclosingWindow(),
338                         target, request, checkHash, forceDownload, "JS form.submit()");
339         }
340     }
341 
342     /**
343      * Retrieves a form object or an object from an elements collection.
344      * @param index Integer or String that specifies the object or collection to retrieve.
345      *              If this parameter is an integer, it is the zero-based index of the object.
346      *              If this parameter is a string, all objects with matching name or id properties are retrieved,
347      *              and a collection is returned if more than one match is made
348      * @param subIndex Optional. Integer that specifies the zero-based index of the object to retrieve
349      *              when a collection is returned
350      * @return an object or a collection of objects if successful, or null otherwise
351      */
352     @JsxFunction(IE)
353     public Object item(final Object index, final Object subIndex) {
354         if (index instanceof Number) {
355             return getElements().item(index);
356         }
357 
358         final String name = Context.toString(index);
359         final Object response = getWithPreemption(name);
360         if (subIndex instanceof Number && response instanceof HTMLCollection) {
361             return ((HTMLCollection) response).item(subIndex);
362         }
363 
364         return response;
365     }
366 
367     /**
368      * Resets this form.
369      */
370     @JsxFunction
371     public void reset() {
372         getHtmlForm().reset();
373     }
374 
375     /**
376      * Overridden to allow the retrieval of certain form elements by ID or name.
377      *
378      * @param name {@inheritDoc}
379      * @return {@inheritDoc}
380      */
381     @Override
382     protected Object getWithPreemption(final String name) {
383         if (getDomNodeOrNull() == null) {
384             return NOT_FOUND;
385         }
386         final List<HtmlElement> elements = findElements(name);
387 
388         if (elements.isEmpty()) {
389             return NOT_FOUND;
390         }
391         if (elements.size() == 1) {
392             return getScriptableFor(elements.get(0));
393         }
394         final List<DomNode> nodes = new ArrayList<>(elements);
395         final HTMLCollection collection = new HTMLCollection(getHtmlForm(), nodes) {
396             @Override
397             protected List<DomNode> computeElements() {
398                 return new ArrayList<>(findElements(name));
399             }
400         };
401         return collection;
402     }
403 
404     private List<HtmlElement> findElements(final String name) {
405         final List<HtmlElement> elements = new ArrayList<>();
406         addElements(name, getHtmlForm().getHtmlElementDescendants(), elements);
407         addElements(name, getHtmlForm().getLostChildren(), elements);
408 
409         // If no form fields are found, IE and Firefox are able to find img elements by ID or name.
410         if (elements.isEmpty()) {
411             for (final DomNode node : getHtmlForm().getChildren()) {
412                 if (node instanceof HtmlImage) {
413                     final HtmlImage img = (HtmlImage) node;
414                     if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
415                         elements.add(img);
416                     }
417                 }
418             }
419         }
420 
421         return elements;
422     }
423 
424     private void addElements(final String name, final Iterable<HtmlElement> nodes,
425         final List<HtmlElement> addTo) {
426         for (final HtmlElement node : nodes) {
427             if (isAccessibleByIdOrName(node, name)) {
428                 addTo.add(node);
429             }
430         }
431     }
432 
433     /**
434      * Indicates if the element can be reached by id or name in expressions like "myForm.myField".
435      * @param element the element to test
436      * @param name the name used to address the element
437      * @return {@code true} if this element matches the conditions
438      */
439     private boolean isAccessibleByIdOrName(final HtmlElement element, final String name) {
440         if (element instanceof FormFieldWithNameHistory && !(element instanceof HtmlImageInput)) {
441             if (element.getEnclosingForm() != getHtmlForm()) {
442                 return false; // nested forms
443             }
444             if (name.equals(element.getId())) {
445                 return true;
446             }
447             final FormFieldWithNameHistory elementWithNames = (FormFieldWithNameHistory) element;
448             if (getBrowserVersion().hasFeature(FORMFIELD_REACHABLE_BY_ORIGINAL_NAME)) {
449                 if (name.equals(elementWithNames.getOriginalName())) {
450                     return true;
451                 }
452             }
453             else if (name.equals(element.getAttribute("name"))) {
454                 return true;
455             }
456 
457             if (getBrowserVersion().hasFeature(FORMFIELD_REACHABLE_BY_NEW_NAMES)) {
458                 if (elementWithNames.getNewNames().contains(name)) {
459                     return true;
460                 }
461             }
462         }
463         return false;
464     }
465 
466     /**
467      * Returns the specified indexed property.
468      * @param index the index of the property
469      * @param start the scriptable object that was originally queried for this property
470      * @return the property
471      */
472     @Override
473     public Object get(final int index, final Scriptable start) {
474         if (getDomNodeOrNull() == null) {
475             return NOT_FOUND; // typically for the prototype
476         }
477         return getElements().get(index, ((HTMLFormElement) start).getElements());
478     }
479 
480     /**
481      * {@inheritDoc}
482      */
483     @Override
484     public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
485         if (!getBrowserVersion().hasFeature(JS_FORM_USABLE_AS_FUNCTION)) {
486             throw Context.reportRuntimeError("Not a function.");
487         }
488         if (args.length > 0) {
489             final Object arg = args[0];
490             if (arg instanceof String) {
491                 return ScriptableObject.getProperty(this, (String) arg);
492             }
493             else if (arg instanceof Number) {
494                 return ScriptableObject.getProperty(this, ((Number) arg).intValue());
495             }
496         }
497         return Undefined.instance;
498     }
499 
500     /**
501      * {@inheritDoc}
502      */
503     @Override
504     public Scriptable construct(final Context cx, final Scriptable scope, final Object[] args) {
505         if (!getBrowserVersion().hasFeature(JS_FORM_USABLE_AS_FUNCTION)) {
506             throw Context.reportRuntimeError("Not a function.");
507         }
508         return null;
509     }
510 
511     @Override
512     public boolean dispatchEvent(final Event event) {
513         final boolean result = super.dispatchEvent(event);
514 
515         if (Event.TYPE_SUBMIT.equals(event.getType())
516                 && getBrowserVersion().hasFeature(JS_FORM_DISPATCHEVENT_SUBMITS)) {
517             submit();
518         }
519         return result;
520     }
521 
522     /**
523      * Checks whether the element has any constraints and whether it satisfies them.
524      * @return if the element is valid
525      */
526     @JsxFunction
527     public boolean checkValidity() {
528         return getDomNodeOrDie().isValid();
529     }
530 
531 }