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