1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host;
16
17 import java.net.MalformedURLException;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.Iterator;
21 import java.util.List;
22 import java.util.ListIterator;
23 import java.util.Map;
24
25 import org.apache.commons.lang3.StringUtils;
26 import org.apache.commons.logging.Log;
27 import org.apache.commons.logging.LogFactory;
28 import org.htmlunit.FormEncodingType;
29 import org.htmlunit.WebRequest;
30 import org.htmlunit.corejs.javascript.ClassDescriptor;
31 import org.htmlunit.corejs.javascript.Context;
32 import org.htmlunit.corejs.javascript.ES6Iterator;
33 import org.htmlunit.corejs.javascript.EcmaError;
34 import org.htmlunit.corejs.javascript.Function;
35 import org.htmlunit.corejs.javascript.IteratorLikeIterable;
36 import org.htmlunit.corejs.javascript.NativeObject;
37 import org.htmlunit.corejs.javascript.ScriptRuntime;
38 import org.htmlunit.corejs.javascript.Scriptable;
39 import org.htmlunit.corejs.javascript.SymbolKey;
40 import org.htmlunit.corejs.javascript.TopLevel;
41 import org.htmlunit.corejs.javascript.VarScope;
42 import org.htmlunit.javascript.HtmlUnitScriptable;
43 import org.htmlunit.javascript.JavaScriptEngine;
44 import org.htmlunit.javascript.configuration.JsxClass;
45 import org.htmlunit.javascript.configuration.JsxConstructor;
46 import org.htmlunit.javascript.configuration.JsxFunction;
47 import org.htmlunit.javascript.configuration.JsxGetter;
48 import org.htmlunit.javascript.configuration.JsxSymbol;
49 import org.htmlunit.javascript.host.xml.FormData.FormDataIterator;
50 import org.htmlunit.util.NameValuePair;
51 import org.htmlunit.util.UrlUtils;
52
53
54
55
56
57
58
59
60
61
62 @JsxClass
63 public class URLSearchParams extends HtmlUnitScriptable {
64
65 private static final Log LOG = LogFactory.getLog(URLSearchParams.class);
66
67
68 private static final String URL_SEARCH_PARMS_ITERATOR_TAG = "URLSearchParams Iterator";
69
70 private URL url_;
71
72
73
74
75 public static final class NativeParamsIterator extends ES6Iterator {
76
77 private static final ClassDescriptor DESCRIPTOR =
78 ES6Iterator.makeDescriptor(URL_SEARCH_PARMS_ITERATOR_TAG, URL_SEARCH_PARMS_ITERATOR_TAG);
79
80 enum Type { KEYS, VALUES, BOTH }
81
82 private final Type type_;
83 private final String className_;
84 private final transient Iterator<NameValuePair> iterator_;
85
86
87
88
89
90
91
92
93 public static void init(final Context cx, final TopLevel scope, final String className) {
94 ES6Iterator.initialize(
95 DESCRIPTOR, cx, scope, new FormDataIterator(className), false, URL_SEARCH_PARMS_ITERATOR_TAG);
96 }
97
98
99
100
101
102 public NativeParamsIterator(final String className) {
103 super();
104 iterator_ = Collections.emptyIterator();
105 type_ = Type.BOTH;
106 className_ = className;
107 }
108
109
110
111
112
113
114
115
116 public NativeParamsIterator(final VarScope scope, final String className, final Type type,
117 final Iterator<NameValuePair> iterator) {
118 super(scope, className);
119 iterator_ = iterator;
120 type_ = type;
121 className_ = className;
122 }
123
124 @Override
125 public String getClassName() {
126 return className_;
127 }
128
129 @Override
130 protected boolean isDone(final Context cx, final VarScope scope) {
131 return !iterator_.hasNext();
132 }
133
134 @Override
135 protected Object nextValue(final Context cx, final VarScope scope) {
136 final NameValuePair e = iterator_.next();
137 return switch (type_) {
138 case KEYS -> e.getName();
139 case VALUES -> e.getValue();
140 case BOTH -> cx.newArray(scope, new Object[]{e.getName(), e.getValue()});
141 };
142 }
143 }
144
145
146
147
148 public URLSearchParams() {
149 super();
150 }
151
152
153
154
155
156 URLSearchParams(final URL url) {
157 super();
158 url_ = url;
159 }
160
161
162
163
164
165 @JsxConstructor
166 public void jsConstructor(final Object params) {
167 url_ = new URL();
168 url_.jsConstructor("http://www.htmlunit.org", "");
169
170 if (params == null || JavaScriptEngine.isUndefined(params)) {
171 return;
172 }
173
174 try {
175 url_.setSearch(resolveParams(params));
176 }
177 catch (final EcmaError e) {
178 throw JavaScriptEngine.typeError("Failed to construct 'URLSearchParams': " + e.getErrorMessage());
179 }
180 catch (final MalformedURLException e) {
181 LOG.error(e.getMessage(), e);
182 }
183 }
184
185
186
187
188 private static List<NameValuePair> resolveParams(final Object params) {
189
190 if (params instanceof Scriptable paramsScriptable && hasProperty(paramsScriptable, SymbolKey.ITERATOR)) {
191
192 final Context cx = Context.getCurrentContext();
193
194 final List<NameValuePair> nameValuePairs = new ArrayList<>();
195
196 try (IteratorLikeIterable itr = buildIteratorLikeIterable(cx, paramsScriptable)) {
197 for (final Object nameValue : itr) {
198 if (!(nameValue instanceof Scriptable)) {
199 throw JavaScriptEngine.typeError("The provided value cannot be converted to a sequence.");
200 }
201 if (!hasProperty((Scriptable) nameValue, SymbolKey.ITERATOR)) {
202 throw JavaScriptEngine.typeError("The object must have a callable @@iterator property.");
203 }
204
205 try (IteratorLikeIterable nameValueItr = buildIteratorLikeIterable(cx, (Scriptable) nameValue)) {
206
207 final Iterator<Object> nameValueIterator = nameValueItr.iterator();
208 final Object name =
209 nameValueIterator.hasNext() ? nameValueIterator.next() : NOT_FOUND;
210 final Object value =
211 nameValueIterator.hasNext() ? nameValueIterator.next() : NOT_FOUND;
212
213 if (name == NOT_FOUND
214 || value == NOT_FOUND
215 || nameValueIterator.hasNext()) {
216 throw JavaScriptEngine.typeError("Sequence initializer must only contain pair elements.");
217 }
218
219 nameValuePairs.add(new NameValuePair(
220 JavaScriptEngine.toString(name),
221 JavaScriptEngine.toString(value)));
222 }
223 }
224 }
225
226 return nameValuePairs;
227 }
228
229
230 if (params instanceof NativeObject object) {
231 final List<NameValuePair> nameValuePairs = new ArrayList<>();
232 for (final Map.Entry<Object, Object> keyValuePair : object.entrySet()) {
233 nameValuePairs.add(
234 new NameValuePair(
235 JavaScriptEngine.toString(keyValuePair.getKey()),
236 JavaScriptEngine.toString(keyValuePair.getValue())));
237 }
238 return nameValuePairs;
239 }
240
241
242 return splitQuery(JavaScriptEngine.toString(params));
243 }
244
245 private List<NameValuePair> splitQuery() {
246 return splitQuery(url_.getSearch());
247 }
248
249 private static List<NameValuePair> splitQuery(String params) {
250 final List<NameValuePair> splitted = new ArrayList<>();
251
252 params = StringUtils.stripStart(params, "?");
253 if (org.htmlunit.util.StringUtils.isEmptyOrNull(params)) {
254 return splitted;
255 }
256
257 final String[] parts = StringUtils.split(params, '&');
258 for (final String part : parts) {
259 final NameValuePair pair = splitQueryParameter(part);
260 splitted.add(new NameValuePair(UrlUtils.decode(pair.getName()), UrlUtils.decode(pair.getValue())));
261 }
262 return splitted;
263 }
264
265 private static NameValuePair splitQueryParameter(final String singleParam) {
266 final int idx = singleParam.indexOf('=');
267
268 if (idx > -1) {
269 final String key = singleParam.substring(0, idx);
270 final String value = singleParam.substring(idx + 1);
271 return new NameValuePair(key, value);
272 }
273
274 return new NameValuePair(singleParam, "");
275 }
276
277 private static IteratorLikeIterable buildIteratorLikeIterable(final Context cx, final Scriptable iterable) {
278 final Object iterator = ScriptRuntime.callIterator(iterable, cx, iterable.getParentScope());
279 return new IteratorLikeIterable(cx, iterable.getParentScope(), iterator);
280 }
281
282
283
284
285
286
287
288
289 @JsxFunction
290 public void append(final String name, final String value) {
291 final String search = url_.getSearch();
292
293 final List<NameValuePair> pairs;
294 if (search == null || search.isEmpty()) {
295 pairs = new ArrayList<>(1);
296 }
297 else {
298 pairs = splitQuery(search);
299 }
300
301 pairs.add(new NameValuePair(name, value));
302 try {
303 url_.setSearch(pairs);
304 }
305 catch (final MalformedURLException e) {
306 LOG.error(e.getMessage(), e);
307 }
308 }
309
310
311
312
313
314
315
316 @JsxFunction
317 @Override
318 public void delete(final String name) {
319 final List<NameValuePair> splitted = splitQuery();
320 splitted.removeIf(entry -> entry.getName().equals(name));
321
322 if (splitted.isEmpty()) {
323 try {
324 url_.setSearch((String) null);
325 }
326 catch (final MalformedURLException e) {
327 LOG.error(e.getMessage(), e);
328 }
329 return;
330 }
331
332 try {
333 url_.setSearch(splitted);
334 }
335 catch (final MalformedURLException e) {
336 LOG.error(e.getMessage(), e);
337 }
338 }
339
340
341
342
343
344
345
346
347 @JsxFunction
348 public String get(final String name) {
349 final List<NameValuePair> splitted = splitQuery();
350 for (final NameValuePair param : splitted) {
351 if (param.getName().equals(name)) {
352 return param.getValue();
353 }
354 }
355 return null;
356 }
357
358
359
360
361
362
363
364
365 @JsxFunction
366 public Scriptable getAll(final String name) {
367 final List<NameValuePair> splitted = splitQuery();
368 final List<String> result = new ArrayList<>(splitted.size());
369 for (final NameValuePair param : splitted) {
370 if (param.getName().equals(name)) {
371 result.add(param.getValue());
372 }
373 }
374
375 return JavaScriptEngine.newArray(getParentScope(), result.toArray());
376 }
377
378
379
380
381
382
383
384
385
386
387 @JsxFunction
388 public void set(final String name, final String value) {
389 final List<NameValuePair> splitted = splitQuery();
390
391 boolean change = true;
392 final ListIterator<NameValuePair> iter = splitted.listIterator();
393 while (iter.hasNext()) {
394 final NameValuePair entry = iter.next();
395 if (entry.getName().equals(name)) {
396 if (change) {
397 iter.set(new NameValuePair(name, value));
398 change = false;
399 }
400 else {
401 iter.remove();
402 }
403 }
404 }
405
406 if (change) {
407 splitted.add(new NameValuePair(name, value));
408 }
409
410 try {
411 url_.setSearch(splitted);
412 }
413 catch (final MalformedURLException e) {
414 LOG.error(e.getMessage(), e);
415 }
416 }
417
418
419
420
421
422
423
424
425 @JsxFunction
426 public boolean has(final String name) {
427 final List<NameValuePair> splitted = splitQuery();
428
429 for (final NameValuePair param : splitted) {
430 if (param.getName().equals(name)) {
431 return true;
432 }
433 }
434 return false;
435 }
436
437
438
439
440
441
442 @JsxFunction
443 public void forEach(final Object callback) {
444 if (!(callback instanceof Function fun)) {
445 throw JavaScriptEngine.typeError(
446 "Foreach callback '" + JavaScriptEngine.toString(callback) + "' is not a function");
447 }
448
449 String currentSearch = null;
450 List<NameValuePair> params = null;
451
452 for (int i = 0;; i++) {
453 final String search = url_.getSearch();
454 if (!search.equals(currentSearch)) {
455 params = splitQuery(search);
456 currentSearch = search;
457 }
458 if (i >= params.size()) {
459 break;
460 }
461
462 final NameValuePair param = params.get(i);
463 fun.call(Context.getCurrentContext(), getParentScope(), this,
464 new Object[] {param.getValue(), param.getName(), this});
465 }
466 }
467
468
469
470
471
472
473
474
475 @JsxFunction
476 @JsxSymbol(symbolName = "iterator")
477 public ES6Iterator entries() {
478 final List<NameValuePair> splitted = splitQuery();
479
480 return new NativeParamsIterator(getParentScope(),
481 URL_SEARCH_PARMS_ITERATOR_TAG, NativeParamsIterator.Type.BOTH, splitted.iterator());
482 }
483
484
485
486
487
488
489
490 @JsxFunction
491 public ES6Iterator keys() {
492 final List<NameValuePair> splitted = splitQuery();
493
494 return new NativeParamsIterator(getParentScope(),
495 URL_SEARCH_PARMS_ITERATOR_TAG, NativeParamsIterator.Type.KEYS, splitted.iterator());
496 }
497
498
499
500
501
502
503
504 @JsxFunction
505 public ES6Iterator values() {
506 final List<NameValuePair> splitted = splitQuery();
507
508 return new NativeParamsIterator(getParentScope(),
509 URL_SEARCH_PARMS_ITERATOR_TAG, NativeParamsIterator.Type.VALUES, splitted.iterator());
510 }
511
512
513
514
515 @JsxGetter
516 public int getSize() {
517 final List<NameValuePair> splitted = splitQuery();
518 return splitted.size();
519 }
520
521
522
523
524 @JsxFunction(functionName = "toString")
525 public String jsToString() {
526 final StringBuilder newSearch = new StringBuilder();
527 for (final NameValuePair nameValuePair : splitQuery(url_.getSearch())) {
528 if (newSearch.length() > 0) {
529 newSearch.append('&');
530 }
531 newSearch
532 .append(UrlUtils.encodeQueryPart(nameValuePair.getName()))
533 .append('=')
534 .append(UrlUtils.encodeQueryPart(nameValuePair.getValue()));
535 }
536
537 return newSearch.toString();
538 }
539
540
541
542
543
544
545
546 @Override
547 public Object getDefaultValue(final Class<?> hint) {
548 return jsToString();
549 }
550
551
552
553
554
555 public void fillRequest(final WebRequest webRequest) {
556 webRequest.setRequestBody(null);
557 webRequest.setEncodingType(FormEncodingType.URL_ENCODED);
558
559 final List<NameValuePair> splitted = splitQuery();
560 if (!splitted.isEmpty()) {
561 webRequest.setRequestParameters(new ArrayList<>(splitted));
562 }
563 }
564 }