Skip to content

Commit 61d6293

Browse files
TimothyGuItalo A. Casas
authored andcommitted
url: improve URLSearchParams spec compliance
- Make URLSearchParams constructor spec-compliant - Strip leading `?` in URL#search's setter - Spec-compliant iterable interface - More precise handling of update steps as mandated by the spec - Add class strings to URLSearchParams objects and their prototype - Make sure `this instanceof URLSearchParams` in methods Also included are relevant tests from W3C's Web Platform Tests (https://github.com/w3c/web-platform-tests/tree/master/url). Fixes: #9302 PR-URL: #9484 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 1b25214 commit 61d6293

11 files changed

+797
-43
lines changed

lib/internal/url.js

Lines changed: 240 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ const kHost = Symbol('host');
2020
const kPort = Symbol('port');
2121
const kDomain = Symbol('domain');
2222

23+
// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
24+
const IteratorPrototype = Object.getPrototypeOf(
25+
Object.getPrototypeOf([][Symbol.iterator]())
26+
);
27+
2328
function StorageObject() {}
2429
StorageObject.prototype = Object.create(null);
2530

@@ -101,7 +106,8 @@ class URL {
101106
this[context].query = query;
102107
this[context].fragment = fragment;
103108
this[context].host = host;
104-
this[searchParams] = new URLSearchParams(this);
109+
this[searchParams] = new URLSearchParams(query);
110+
this[searchParams][context] = this;
105111
});
106112
}
107113

@@ -318,8 +324,31 @@ class URL {
318324
}
319325

320326
set search(search) {
321-
update(this, search);
322-
this[searchParams][searchParams] = querystring.parse(this.search);
327+
search = String(search);
328+
if (search[0] === '?') search = search.slice(1);
329+
if (!search) {
330+
this[context].query = null;
331+
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
332+
this[searchParams][searchParams] = {};
333+
return;
334+
}
335+
this[context].query = '';
336+
binding.parse(search,
337+
binding.kQuery,
338+
null,
339+
this[context],
340+
(flags, protocol, username, password,
341+
host, port, path, query, fragment) => {
342+
if (flags & binding.URL_FLAGS_FAILED)
343+
return;
344+
if (query) {
345+
this[context].query = query;
346+
this[context].flags |= binding.URL_FLAGS_HAS_QUERY;
347+
} else {
348+
this[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
349+
}
350+
});
351+
this[searchParams][searchParams] = querystring.parse(search);
323352
}
324353

325354
get hash() {
@@ -493,105 +522,273 @@ function encodeAuth(str) {
493522
return out;
494523
}
495524

496-
function update(url, search) {
497-
search = String(search);
498-
if (!search) {
499-
url[context].query = null;
500-
url[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
525+
function update(url, params) {
526+
if (!url)
501527
return;
528+
529+
url[context].query = params.toString();
530+
}
531+
532+
function getSearchParamPairs(target) {
533+
const obj = target[searchParams];
534+
const keys = Object.keys(obj);
535+
const values = [];
536+
for (var i = 0; i < keys.length; i++) {
537+
const name = keys[i];
538+
const value = obj[name];
539+
if (Array.isArray(value)) {
540+
for (const item of value)
541+
values.push([name, item]);
542+
} else {
543+
values.push([name, value]);
544+
}
502545
}
503-
if (search[0] === '?') search = search.slice(1);
504-
url[context].query = '';
505-
binding.parse(search,
506-
binding.kQuery,
507-
null,
508-
url[context],
509-
(flags, protocol, username, password,
510-
host, port, path, query, fragment) => {
511-
if (flags & binding.URL_FLAGS_FAILED)
512-
return;
513-
if (query) {
514-
url[context].query = query;
515-
url[context].flags |= binding.URL_FLAGS_HAS_QUERY;
516-
} else {
517-
url[context].flags &= ~binding.URL_FLAGS_HAS_QUERY;
518-
}
519-
});
546+
return values;
520547
}
521548

522549
class URLSearchParams {
523-
constructor(url) {
524-
this[context] = url;
525-
this[searchParams] = querystring.parse(url[context].search || '');
550+
constructor(init = '') {
551+
if (init instanceof URLSearchParams) {
552+
const childParams = init[searchParams];
553+
this[searchParams] = Object.assign(Object.create(null), childParams);
554+
} else {
555+
init = String(init);
556+
if (init[0] === '?') init = init.slice(1);
557+
this[searchParams] = querystring.parse(init);
558+
}
559+
560+
// "associated url object"
561+
this[context] = null;
562+
563+
// Class string for an instance of URLSearchParams. This is different from
564+
// the class string of the prototype object (set below).
565+
Object.defineProperty(this, Symbol.toStringTag, {
566+
value: 'URLSearchParams',
567+
writable: false,
568+
enumerable: false,
569+
configurable: true
570+
});
526571
}
527572

528573
append(name, value) {
574+
if (!this || !(this instanceof URLSearchParams)) {
575+
throw new TypeError('Value of `this` is not a URLSearchParams');
576+
}
577+
if (arguments.length < 2) {
578+
throw new TypeError(
579+
'Both `name` and `value` arguments need to be specified');
580+
}
581+
529582
const obj = this[searchParams];
530583
name = String(name);
531584
value = String(value);
532585
var existing = obj[name];
533-
if (!existing) {
586+
if (existing === undefined) {
534587
obj[name] = value;
535588
} else if (Array.isArray(existing)) {
536589
existing.push(value);
537590
} else {
538591
obj[name] = [existing, value];
539592
}
540-
update(this[context], querystring.stringify(obj));
593+
update(this[context], this);
541594
}
542595

543596
delete(name) {
597+
if (!this || !(this instanceof URLSearchParams)) {
598+
throw new TypeError('Value of `this` is not a URLSearchParams');
599+
}
600+
if (arguments.length < 1) {
601+
throw new TypeError('The `name` argument needs to be specified');
602+
}
603+
544604
const obj = this[searchParams];
545605
name = String(name);
546606
delete obj[name];
547-
update(this[context], querystring.stringify(obj));
607+
update(this[context], this);
548608
}
549609

550610
set(name, value) {
611+
if (!this || !(this instanceof URLSearchParams)) {
612+
throw new TypeError('Value of `this` is not a URLSearchParams');
613+
}
614+
if (arguments.length < 2) {
615+
throw new TypeError(
616+
'Both `name` and `value` arguments need to be specified');
617+
}
618+
551619
const obj = this[searchParams];
552620
name = String(name);
553621
value = String(value);
554622
obj[name] = value;
555-
update(this[context], querystring.stringify(obj));
623+
update(this[context], this);
556624
}
557625

558626
get(name) {
627+
if (!this || !(this instanceof URLSearchParams)) {
628+
throw new TypeError('Value of `this` is not a URLSearchParams');
629+
}
630+
if (arguments.length < 1) {
631+
throw new TypeError('The `name` argument needs to be specified');
632+
}
633+
559634
const obj = this[searchParams];
560635
name = String(name);
561636
var value = obj[name];
562-
return Array.isArray(value) ? value[0] : value;
637+
return value === undefined ? null : Array.isArray(value) ? value[0] : value;
563638
}
564639

565640
getAll(name) {
641+
if (!this || !(this instanceof URLSearchParams)) {
642+
throw new TypeError('Value of `this` is not a URLSearchParams');
643+
}
644+
if (arguments.length < 1) {
645+
throw new TypeError('The `name` argument needs to be specified');
646+
}
647+
566648
const obj = this[searchParams];
567649
name = String(name);
568650
var value = obj[name];
569651
return value === undefined ? [] : Array.isArray(value) ? value : [value];
570652
}
571653

572654
has(name) {
655+
if (!this || !(this instanceof URLSearchParams)) {
656+
throw new TypeError('Value of `this` is not a URLSearchParams');
657+
}
658+
if (arguments.length < 1) {
659+
throw new TypeError('The `name` argument needs to be specified');
660+
}
661+
573662
const obj = this[searchParams];
574663
name = String(name);
575664
return name in obj;
576665
}
577666

578-
*[Symbol.iterator]() {
579-
const obj = this[searchParams];
580-
for (const name in obj) {
581-
const value = obj[name];
582-
if (Array.isArray(value)) {
583-
for (const item of value)
584-
yield [name, item];
585-
} else {
586-
yield [name, value];
587-
}
667+
// https://heycam.github.io/webidl/#es-iterators
668+
// Define entries here rather than [Symbol.iterator] as the function name
669+
// must be set to `entries`.
670+
entries() {
671+
if (!this || !(this instanceof URLSearchParams)) {
672+
throw new TypeError('Value of `this` is not a URLSearchParams');
588673
}
674+
675+
return createSearchParamsIterator(this, 'key+value');
589676
}
590677

678+
forEach(callback, thisArg = undefined) {
679+
if (!this || !(this instanceof URLSearchParams)) {
680+
throw new TypeError('Value of `this` is not a URLSearchParams');
681+
}
682+
if (arguments.length < 1) {
683+
throw new TypeError('The `callback` argument needs to be specified');
684+
}
685+
686+
let pairs = getSearchParamPairs(this);
687+
688+
var i = 0;
689+
while (i < pairs.length) {
690+
const [key, value] = pairs[i];
691+
callback.call(thisArg, value, key, this);
692+
pairs = getSearchParamPairs(this);
693+
i++;
694+
}
695+
}
696+
697+
// https://heycam.github.io/webidl/#es-iterable
698+
keys() {
699+
if (!this || !(this instanceof URLSearchParams)) {
700+
throw new TypeError('Value of `this` is not a URLSearchParams');
701+
}
702+
703+
return createSearchParamsIterator(this, 'key');
704+
}
705+
706+
values() {
707+
if (!this || !(this instanceof URLSearchParams)) {
708+
throw new TypeError('Value of `this` is not a URLSearchParams');
709+
}
710+
711+
return createSearchParamsIterator(this, 'value');
712+
}
713+
714+
// https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior
591715
toString() {
716+
if (!this || !(this instanceof URLSearchParams)) {
717+
throw new TypeError('Value of `this` is not a URLSearchParams');
718+
}
719+
592720
return querystring.stringify(this[searchParams]);
593721
}
594722
}
723+
// https://heycam.github.io/webidl/#es-iterable-entries
724+
URLSearchParams.prototype[Symbol.iterator] = URLSearchParams.prototype.entries;
725+
Object.defineProperty(URLSearchParams.prototype, Symbol.toStringTag, {
726+
value: 'URLSearchParamsPrototype',
727+
writable: false,
728+
enumerable: false,
729+
configurable: true
730+
});
731+
732+
// https://heycam.github.io/webidl/#dfn-default-iterator-object
733+
function createSearchParamsIterator(target, kind) {
734+
const iterator = Object.create(URLSearchParamsIteratorPrototype);
735+
iterator[context] = {
736+
target,
737+
kind,
738+
index: 0
739+
};
740+
return iterator;
741+
}
742+
743+
// https://heycam.github.io/webidl/#dfn-iterator-prototype-object
744+
const URLSearchParamsIteratorPrototype = Object.setPrototypeOf({
745+
next() {
746+
if (!this ||
747+
Object.getPrototypeOf(this) !== URLSearchParamsIteratorPrototype) {
748+
throw new TypeError('Value of `this` is not a URLSearchParamsIterator');
749+
}
750+
751+
const {
752+
target,
753+
kind,
754+
index
755+
} = this[context];
756+
const values = getSearchParamPairs(target);
757+
const len = values.length;
758+
if (index >= len) {
759+
return {
760+
value: undefined,
761+
done: true
762+
};
763+
}
764+
765+
const pair = values[index];
766+
this[context].index = index + 1;
767+
768+
let result;
769+
if (kind === 'key') {
770+
result = pair[0];
771+
} else if (kind === 'value') {
772+
result = pair[1];
773+
} else {
774+
result = pair;
775+
}
776+
777+
return {
778+
value: result,
779+
done: false
780+
};
781+
}
782+
}, IteratorPrototype);
783+
784+
// Unlike interface and its prototype object, both default iterator object and
785+
// iterator prototype object of an interface have the same class string.
786+
Object.defineProperty(URLSearchParamsIteratorPrototype, Symbol.toStringTag, {
787+
value: 'URLSearchParamsIterator',
788+
writable: false,
789+
enumerable: false,
790+
configurable: true
791+
});
595792

596793
URL.originFor = function(url) {
597794
if (!(url instanceof URL))

0 commit comments

Comments
 (0)