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