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