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