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