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.xml;
16  
17  import java.util.ArrayList;
18  import java.util.Collections;
19  import java.util.Iterator;
20  import java.util.List;
21  
22  import org.apache.commons.lang3.StringUtils;
23  import org.htmlunit.FormEncodingType;
24  import org.htmlunit.WebRequest;
25  import org.htmlunit.corejs.javascript.Context;
26  import org.htmlunit.corejs.javascript.ES6Iterator;
27  import org.htmlunit.corejs.javascript.Function;
28  import org.htmlunit.corejs.javascript.Scriptable;
29  import org.htmlunit.corejs.javascript.ScriptableObject;
30  import org.htmlunit.javascript.HtmlUnitScriptable;
31  import org.htmlunit.javascript.JavaScriptEngine;
32  import org.htmlunit.javascript.configuration.JsxClass;
33  import org.htmlunit.javascript.configuration.JsxConstructor;
34  import org.htmlunit.javascript.configuration.JsxFunction;
35  import org.htmlunit.javascript.configuration.JsxSymbol;
36  import org.htmlunit.javascript.host.file.Blob;
37  import org.htmlunit.javascript.host.file.File;
38  import org.htmlunit.javascript.host.html.HTMLFormElement;
39  import org.htmlunit.util.NameValuePair;
40  
41  /**
42   * A JavaScript object for {@code FormData}.
43   *
44   * @author Ahmed Ashour
45   * @author Ronald Brill
46   * @author Thorsten Wendelmuth
47   */
48  @JsxClass
49  public class FormData extends HtmlUnitScriptable {
50  
51      /** Constant used to register the prototype in the context. */
52      public static final String FORM_DATA_TAG = "FormData";
53  
54      private final List<NameValuePair> requestParameters_ = new ArrayList<>();
55  
56      /**
57       * FormDate iterator support.
58       */
59      public static final class FormDataIterator extends ES6Iterator {
60          enum Type { KEYS, VALUES, BOTH }
61  
62          private final Type type_;
63          private final String className_;
64          private final List<NameValuePair> nameValuePairList_;
65          private int index_;
66  
67          /**
68           * JS initializer.
69           *
70           * @param scope the scope
71           * @param className the class name
72           */
73          public static void init(final ScriptableObject scope, final String className) {
74              ES6Iterator.init(scope, false, new FormDataIterator(className), FORM_DATA_TAG);
75          }
76  
77          /**
78           * Ctor.
79           *
80           * @param className the class name
81           */
82          public FormDataIterator(final String className) {
83              super();
84  
85              type_ = Type.BOTH;
86              index_ = 0;
87              nameValuePairList_ = Collections.emptyList();
88              className_ = className;
89          }
90  
91          /**
92           * Ctor.
93           *
94           * @param scope the scope
95           * @param className the class name
96           * @param type the type
97           * @param nameValuePairList the list of name value pairs
98           */
99          public FormDataIterator(final Scriptable scope, final String className, final Type type,
100                 final List<NameValuePair> nameValuePairList) {
101             super(scope, FORM_DATA_TAG);
102             type_ = type;
103             index_ = 0;
104             nameValuePairList_ = nameValuePairList;
105             className_ = className;
106         }
107 
108         /**
109          * {@inheritDoc}
110          */
111         @Override
112         public String getClassName() {
113             return className_;
114         }
115 
116         /**
117          * {@inheritDoc}
118          */
119         @Override
120         protected boolean isDone(final Context cx, final Scriptable scope) {
121             return index_ >= nameValuePairList_.size();
122         }
123 
124         /**
125          * {@inheritDoc}
126          */
127         @Override
128         protected Object nextValue(final Context cx, final Scriptable scope) {
129             if (isDone(cx, scope)) {
130                 return Context.getUndefinedValue();
131             }
132 
133             final NameValuePair nextNameValuePair = nameValuePairList_.get(index_++);
134             switch (type_) {
135                 case KEYS:
136                     return nextNameValuePair.getName();
137                 case VALUES:
138                     return nextNameValuePair.getValue();
139                 case BOTH:
140                     return cx.newArray(scope, new Object[] {nextNameValuePair.getName(), nextNameValuePair.getValue()});
141                 default:
142                     throw new AssertionError();
143             }
144         }
145     }
146 
147     /**
148      * Constructor.
149      * @param formObj a form
150      */
151     @JsxConstructor
152     public void jsConstructor(final Object formObj) {
153         if (formObj instanceof HTMLFormElement) {
154             final HTMLFormElement form = (HTMLFormElement) formObj;
155             requestParameters_.addAll(form.getHtmlForm().getParameterListForSubmit(null));
156         }
157     }
158 
159     /**
160      * Appends a new value onto an existing key inside a {@code FormData} object,
161      * or adds the key if it does not already exist.
162      * @param name the name of the field whose data is contained in {@code value}
163      * @param value the field's value
164      * @param filename the filename reported to the server (optional)
165      */
166     @JsxFunction
167     public void append(final String name, final Object value, final Object filename) {
168         if (value instanceof Blob) {
169             final Blob blob = (Blob) value;
170             String fileName = "blob";
171             if (value instanceof File) {
172                 fileName = null;
173             }
174             if (filename instanceof String) {
175                 fileName = (String) filename;
176             }
177             requestParameters_.add(blob.getKeyDataPair(name, fileName));
178             return;
179         }
180         requestParameters_.add(new NameValuePair(name, JavaScriptEngine.toString(value)));
181     }
182 
183     /**
184      * Removes the entry (if exists).
185      * @param name the name of the field to remove
186      */
187     @JsxFunction(functionName = "delete")
188     public void delete_js(final String name) {
189         if (StringUtils.isEmpty(name)) {
190             return;
191         }
192 
193         requestParameters_.removeIf(pair -> name.equals(pair.getName()));
194     }
195 
196     /**
197      * @param name the name of the field to check
198      * @return the first value found for the give name
199      */
200     @JsxFunction
201     public String get(final String name) {
202         if (StringUtils.isEmpty(name)) {
203             return null;
204         }
205 
206         for (final NameValuePair pair : requestParameters_) {
207             if (name.equals(pair.getName())) {
208                 return pair.getValue();
209             }
210         }
211         return null;
212     }
213 
214     /**
215      * @param name the name of the field to check
216      * @return the values found for the give name
217      */
218     @JsxFunction
219     public Scriptable getAll(final String name) {
220         if (StringUtils.isEmpty(name)) {
221             return JavaScriptEngine.newArray(this, 0);
222         }
223 
224         final List<Object> values = new ArrayList<>();
225         for (final NameValuePair pair : requestParameters_) {
226             if (name.equals(pair.getName())) {
227                 values.add(pair.getValue());
228             }
229         }
230 
231         final Object[] stringValues = values.toArray(new Object[0]);
232         return JavaScriptEngine.newArray(this, stringValues);
233     }
234 
235     /**
236      * @param name the name of the field to check
237      * @return true if the name exists
238      */
239     @JsxFunction
240     public boolean has(final String name) {
241         if (StringUtils.isEmpty(name)) {
242             return false;
243         }
244 
245         for (final NameValuePair pair : requestParameters_) {
246             if (name.equals(pair.getName())) {
247                 return true;
248             }
249         }
250         return false;
251     }
252 
253     /**
254      * Sets a new value for an existing key inside a {@code FormData} object,
255      * or adds the key if it does not already exist.
256      * @param name the name of the field whose data is contained in {@code value}
257      * @param value the field's value
258      * @param filename the filename reported to the server (optional)
259      */
260     @JsxFunction
261     public void set(final String name, final Object value, final Object filename) {
262         if (StringUtils.isEmpty(name)) {
263             return;
264         }
265 
266         int pos = -1;
267 
268         final Iterator<NameValuePair> iter = requestParameters_.iterator();
269         int idx = 0;
270         while (iter.hasNext()) {
271             final NameValuePair pair = iter.next();
272             if (name.equals(pair.getName())) {
273                 iter.remove();
274                 if (pos < 0) {
275                     pos = idx;
276                 }
277             }
278             idx++;
279         }
280 
281         if (pos < 0) {
282             pos = requestParameters_.size();
283         }
284 
285         if (value instanceof Blob) {
286             final Blob blob = (Blob) value;
287             String fileName = "blob";
288             if (value instanceof File) {
289                 fileName = null;
290             }
291             if (filename instanceof String) {
292                 fileName = (String) filename;
293             }
294             requestParameters_.add(pos, blob.getKeyDataPair(name, fileName));
295         }
296         else {
297             requestParameters_.add(pos, new NameValuePair(name, JavaScriptEngine.toString(value)));
298         }
299     }
300 
301     /**
302      * @return An Iterator that contains all the requestParameters name[0] and value[1]
303      */
304     @JsxFunction
305     @JsxSymbol(symbolName = "iterator")
306     public Scriptable entries() {
307         return new FormDataIterator(this, "FormData Iterator", FormDataIterator.Type.BOTH, requestParameters_);
308     }
309 
310     /**
311      * Sets the specified request with the parameters in this {@code FormData}.
312      * @param webRequest the web request to fill
313      */
314     public void fillRequest(final WebRequest webRequest) {
315         webRequest.setEncodingType(FormEncodingType.MULTIPART);
316         webRequest.setRequestParameters(requestParameters_);
317     }
318 
319     /**
320      * The FormData.forEach() method allows iteration through
321      * all key/value pairs contained in this object via a callback function.
322      * @param callback Function to execute on each key/value pairs
323      */
324     @JsxFunction
325     public void forEach(final Object callback) {
326         if (!(callback instanceof Function)) {
327             throw JavaScriptEngine.typeError(
328                     "Foreach callback '" + JavaScriptEngine.toString(callback) + "' is not a function");
329         }
330 
331         final Function fun = (Function) callback;
332 
333         // This must be indexes instead of iterator() for correct behavior when of list changes while iterating
334         for (int i = 0;; i++) {
335             if (i >= requestParameters_.size()) {
336                 break;
337             }
338 
339             final NameValuePair param = requestParameters_.get(i);
340             fun.call(Context.getCurrentContext(), getParentScope(), this,
341                         new Object[] {param.getValue(), param.getName(), this});
342         }
343     }
344 
345     /**
346      * The FormData.keys() method returns an iterator allowing to go through
347      * all keys contained in this object. The keys are USVString objects.
348      *
349      * @return an iterator.
350      */
351     @JsxFunction
352     public FormDataIterator keys() {
353         return new FormDataIterator(getParentScope(),
354                 "FormData Iterator", FormDataIterator.Type.KEYS, requestParameters_);
355     }
356 
357     /**
358      * The URLSearchParams.values() method returns an iterator allowing to go through
359      * all values contained in this object. The values are USVString objects.
360      *
361      * @return an iterator.
362      */
363     @JsxFunction
364     public FormDataIterator values() {
365         return new FormDataIterator(getParentScope(),
366                 "FormData Iterator", FormDataIterator.Type.VALUES, requestParameters_);
367     }
368 }