url: fix definitions of URL/SearchParams methods and accessors

This fixes getter and setter names for the WHATWG URL classes, and fixes
a few other inconsistencies with browsers implementations.

Co-authored-by: Gerhard Stöbich <deb2001-github@yahoo.de>

PR-URL: https://github.com/nodejs/node/pull/36799
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
This commit is contained in:
ExE BossandGerhard Stöbich authored and Antoine du Hamel committed 2021-02-13 17:59:21 +01:00
1 parent dd5b48ab94
commit 0dfc0f5702
2 files changed
+417 -329

No files matched your search

+317 -329
View File
@@ -257,9 +257,7 @@ class URLSearchParams {
}
return `${this.constructor.name} {}`;
}
}
defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
append(name, value) {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
throw new ERR_INVALID_THIS('URLSearchParams');
@@ -272,7 +270,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
value = toUSVString(value);
ArrayPrototypePush(this[searchParams], name, value);
update(this[context], this);
},
}
delete(name) {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
@@ -293,7 +291,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
}
update(this[context], this);
},
}
get(name) {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
@@ -311,7 +309,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
}
return null;
},
}
getAll(name) {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
@@ -330,7 +328,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
}
return values;
},
}
has(name) {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
@@ -348,7 +346,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
}
return false;
},
}
set(name, value) {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
@@ -388,7 +386,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
update(this[context], this);
},
}
sort() {
const a = this[searchParams];
@@ -432,7 +430,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
update(this[context], this);
},
}
// https://heycam.github.io/webidl/#es-iterators
// Define entries here rather than [Symbol.iterator] as the function name
@@ -443,7 +441,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
return createSearchParamsIterator(this, 'key+value');
},
}
forEach(callback, thisArg = undefined) {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
@@ -462,7 +460,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
list = this[searchParams];
i += 2;
}
},
}
// https://heycam.github.io/webidl/#es-iterable
keys() {
@@ -471,7 +469,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
return createSearchParamsIterator(this, 'key');
},
}
values() {
if (!this || !this[searchParams] || this[searchParams][searchParams]) {
@@ -479,7 +477,7 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
}
return createSearchParamsIterator(this, 'value');
},
}
// https://heycam.github.io/webidl/#es-stringifier
// https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior
@@ -490,13 +488,29 @@ defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
return serializeParams(this[searchParams]);
}
});
}
// https://heycam.github.io/webidl/#es-iterable-entries
ObjectDefineProperty(URLSearchParams.prototype, SymbolIterator, {
writable: true,
configurable: true,
value: URLSearchParams.prototype.entries
ObjectDefineProperties(URLSearchParams.prototype, {
append: { enumerable: true },
delete: { enumerable: true },
get: { enumerable: true },
getAll: { enumerable: true },
has: { enumerable: true },
set: { enumerable: true },
sort: { enumerable: true },
entries: { enumerable: true },
forEach: { enumerable: true },
keys: { enumerable: true },
values: { enumerable: true },
toString: { enumerable: true },
[SymbolToStringTag]: { configurable: true, value: 'URLSearchParams' },
// https://heycam.github.io/webidl/#es-iterable-entries
[SymbolIterator]: {
configurable: true,
writable: true,
value: URLSearchParams.prototype.entries,
},
});
function onParseComplete(flags, protocol, username, password,
@@ -648,319 +662,293 @@ class URL {
return `${constructor.name} ${inspect(obj, opts)}`;
}
[kFormat](options) {
if (options)
validateObject(options, 'options');
options = {
fragment: true,
unicode: false,
search: true,
auth: true,
...options
};
const ctx = this[context];
// https://url.spec.whatwg.org/#url-serializing
let ret = ctx.scheme;
if (ctx.host !== null) {
ret += '//';
const has_username = ctx.username !== '';
const has_password = ctx.password !== '';
if (options.auth && (has_username || has_password)) {
if (has_username)
ret += ctx.username;
if (has_password)
ret += `:${ctx.password}`;
ret += '@';
}
ret += options.unicode ?
domainToUnicode(ctx.host) : ctx.host;
if (ctx.port !== null)
ret += `:${ctx.port}`;
}
if (this[cannotBeBase]) {
ret += ctx.path[0];
} else {
if (ctx.host === null && ctx.path.length > 1 && ctx.path[0] === '') {
ret += '/.';
}
if (ctx.path.length) {
ret += '/' + ArrayPrototypeJoin(ctx.path, '/');
}
}
if (options.search && ctx.query !== null)
ret += `?${ctx.query}`;
if (options.fragment && ctx.fragment !== null)
ret += `#${ctx.fragment}`;
return ret;
}
// https://heycam.github.io/webidl/#es-stringifier
toString() {
return this[kFormat]({});
}
get href() {
return this[kFormat]({});
}
set href(input) {
// toUSVString is not needed.
input = `${input}`;
parse(input, -1, undefined, undefined,
FunctionPrototypeBind(onParseComplete, this), onParseError);
}
// readonly
get origin() {
// Refs: https://url.spec.whatwg.org/#concept-url-origin
const ctx = this[context];
switch (ctx.scheme) {
case 'blob:':
if (ctx.path.length > 0) {
try {
return (new URL(ctx.path[0])).origin;
} catch {
// Fall through... do nothing
}
}
return kOpaqueOrigin;
case 'ftp:':
case 'http:':
case 'https:':
case 'ws:':
case 'wss:':
return serializeTupleOrigin(ctx.scheme, ctx.host, ctx.port);
}
return kOpaqueOrigin;
}
get protocol() {
return this[context].scheme;
}
set protocol(scheme) {
// toUSVString is not needed.
scheme = `${scheme}`;
if (scheme.length === 0)
return;
const ctx = this[context];
parse(scheme, kSchemeStart, null, ctx,
FunctionPrototypeBind(onParseProtocolComplete, this));
}
get username() {
return this[context].username;
}
set username(username) {
// toUSVString is not needed.
username = `${username}`;
if (this[cannotHaveUsernamePasswordPort])
return;
const ctx = this[context];
if (username === '') {
ctx.username = '';
ctx.flags &= ~URL_FLAGS_HAS_USERNAME;
return;
}
ctx.username = encodeAuth(username);
ctx.flags |= URL_FLAGS_HAS_USERNAME;
}
get password() {
return this[context].password;
}
set password(password) {
// toUSVString is not needed.
password = `${password}`;
if (this[cannotHaveUsernamePasswordPort])
return;
const ctx = this[context];
if (password === '') {
ctx.password = '';
ctx.flags &= ~URL_FLAGS_HAS_PASSWORD;
return;
}
ctx.password = encodeAuth(password);
ctx.flags |= URL_FLAGS_HAS_PASSWORD;
}
get host() {
const ctx = this[context];
let ret = ctx.host || '';
if (ctx.port !== null)
ret += `:${ctx.port}`;
return ret;
}
set host(host) {
const ctx = this[context];
// toUSVString is not needed.
host = `${host}`;
if (this[cannotBeBase]) {
// Cannot set the host if cannot-be-base is set
return;
}
parse(host, kHost, null, ctx,
FunctionPrototypeBind(onParseHostComplete, this));
}
get hostname() {
return this[context].host || '';
}
set hostname(host) {
const ctx = this[context];
// toUSVString is not needed.
host = `${host}`;
if (this[cannotBeBase]) {
// Cannot set the host if cannot-be-base is set
return;
}
parse(host, kHostname, null, ctx, onParseHostnameComplete.bind(this));
}
get port() {
const port = this[context].port;
return port === null ? '' : String(port);
}
set port(port) {
// toUSVString is not needed.
port = `${port}`;
if (this[cannotHaveUsernamePasswordPort])
return;
const ctx = this[context];
if (port === '') {
ctx.port = null;
return;
}
parse(port, kPort, null, ctx,
FunctionPrototypeBind(onParsePortComplete, this));
}
get pathname() {
const ctx = this[context];
if (this[cannotBeBase])
return ctx.path[0];
if (ctx.path.length === 0)
return '';
return `/${ArrayPrototypeJoin(ctx.path, '/')}`;
}
set pathname(path) {
// toUSVString is not needed.
path = `${path}`;
if (this[cannotBeBase])
return;
parse(path, kPathStart, null, this[context],
onParsePathComplete.bind(this));
}
get search() {
const { query } = this[context];
if (query === null || query === '')
return '';
return `?${query}`;
}
set search(search) {
const ctx = this[context];
search = toUSVString(search);
if (search === '') {
ctx.query = null;
ctx.flags &= ~URL_FLAGS_HAS_QUERY;
} else {
if (search[0] === '?') search = StringPrototypeSlice(search, 1);
ctx.query = '';
ctx.flags |= URL_FLAGS_HAS_QUERY;
if (search) {
parse(search, kQuery, null, ctx,
FunctionPrototypeBind(onParseSearchComplete, this));
}
}
initSearchParams(this[searchParams], search);
}
// readonly
get searchParams() {
return this[searchParams];
}
get hash() {
const { fragment } = this[context];
if (fragment === null || fragment === '')
return '';
return `#${fragment}`;
}
set hash(hash) {
const ctx = this[context];
// toUSVString is not needed.
hash = `${hash}`;
if (!hash) {
ctx.fragment = null;
ctx.flags &= ~URL_FLAGS_HAS_FRAGMENT;
return;
}
if (hash[0] === '#') hash = StringPrototypeSlice(hash, 1);
ctx.fragment = '';
ctx.flags |= URL_FLAGS_HAS_FRAGMENT;
parse(hash, kFragment, null, ctx,
FunctionPrototypeBind(onParseHashComplete, this));
}
toJSON() {
return this[kFormat]({});
}
}
ObjectDefineProperties(URL.prototype, {
[kFormat]: {
enumerable: false,
configurable: false,
// eslint-disable-next-line func-name-matching
value: function format(options) {
if (options)
validateObject(options, 'options');
options = {
fragment: true,
unicode: false,
search: true,
auth: true,
...options
};
const ctx = this[context];
// https://url.spec.whatwg.org/#url-serializing
let ret = ctx.scheme;
if (ctx.host !== null) {
ret += '//';
const has_username = ctx.username !== '';
const has_password = ctx.password !== '';
if (options.auth && (has_username || has_password)) {
if (has_username)
ret += ctx.username;
if (has_password)
ret += `:${ctx.password}`;
ret += '@';
}
ret += options.unicode ?
domainToUnicode(ctx.host) : ctx.host;
if (ctx.port !== null)
ret += `:${ctx.port}`;
}
if (this[cannotBeBase]) {
ret += ctx.path[0];
} else {
if (ctx.host === null && ctx.path.length > 1 && ctx.path[0] === '') {
ret += '/.';
}
if (ctx.path.length) {
ret += '/' + ArrayPrototypeJoin(ctx.path, '/');
}
}
if (options.search && ctx.query !== null)
ret += `?${ctx.query}`;
if (options.fragment && ctx.fragment !== null)
ret += `#${ctx.fragment}`;
return ret;
}
},
[SymbolToStringTag]: {
configurable: true,
value: 'URL'
},
toString: {
// https://heycam.github.io/webidl/#es-stringifier
writable: true,
enumerable: true,
configurable: true,
// eslint-disable-next-line func-name-matching
value: function toString() {
return this[kFormat]({});
}
},
href: {
enumerable: true,
configurable: true,
get() {
return this[kFormat]({});
},
set(input) {
// toUSVString is not needed.
input = `${input}`;
parse(input, -1, undefined, undefined,
FunctionPrototypeBind(onParseComplete, this), onParseError);
}
},
origin: { // readonly
enumerable: true,
configurable: true,
get() {
// Refs: https://url.spec.whatwg.org/#concept-url-origin
const ctx = this[context];
switch (ctx.scheme) {
case 'blob:':
if (ctx.path.length > 0) {
try {
return (new URL(ctx.path[0])).origin;
} catch {
// Fall through... do nothing
}
}
return kOpaqueOrigin;
case 'ftp:':
case 'http:':
case 'https:':
case 'ws:':
case 'wss:':
return serializeTupleOrigin(ctx.scheme, ctx.host, ctx.port);
}
return kOpaqueOrigin;
}
},
protocol: {
enumerable: true,
configurable: true,
get() {
return this[context].scheme;
},
set(scheme) {
// toUSVString is not needed.
scheme = `${scheme}`;
if (scheme.length === 0)
return;
const ctx = this[context];
parse(scheme, kSchemeStart, null, ctx,
FunctionPrototypeBind(onParseProtocolComplete, this));
}
},
username: {
enumerable: true,
configurable: true,
get() {
return this[context].username;
},
set(username) {
// toUSVString is not needed.
username = `${username}`;
if (this[cannotHaveUsernamePasswordPort])
return;
const ctx = this[context];
if (username === '') {
ctx.username = '';
ctx.flags &= ~URL_FLAGS_HAS_USERNAME;
return;
}
ctx.username = encodeAuth(username);
ctx.flags |= URL_FLAGS_HAS_USERNAME;
}
},
password: {
enumerable: true,
configurable: true,
get() {
return this[context].password;
},
set(password) {
// toUSVString is not needed.
password = `${password}`;
if (this[cannotHaveUsernamePasswordPort])
return;
const ctx = this[context];
if (password === '') {
ctx.password = '';
ctx.flags &= ~URL_FLAGS_HAS_PASSWORD;
return;
}
ctx.password = encodeAuth(password);
ctx.flags |= URL_FLAGS_HAS_PASSWORD;
}
},
host: {
enumerable: true,
configurable: true,
get() {
const ctx = this[context];
let ret = ctx.host || '';
if (ctx.port !== null)
ret += `:${ctx.port}`;
return ret;
},
set(host) {
const ctx = this[context];
// toUSVString is not needed.
host = `${host}`;
if (this[cannotBeBase]) {
// Cannot set the host if cannot-be-base is set
return;
}
parse(host, kHost, null, ctx,
FunctionPrototypeBind(onParseHostComplete, this));
}
},
hostname: {
enumerable: true,
configurable: true,
get() {
return this[context].host || '';
},
set(host) {
const ctx = this[context];
// toUSVString is not needed.
host = `${host}`;
if (this[cannotBeBase]) {
// Cannot set the host if cannot-be-base is set
return;
}
parse(host, kHostname, null, ctx, onParseHostnameComplete.bind(this));
}
},
port: {
enumerable: true,
configurable: true,
get() {
const port = this[context].port;
return port === null ? '' : String(port);
},
set(port) {
// toUSVString is not needed.
port = `${port}`;
if (this[cannotHaveUsernamePasswordPort])
return;
const ctx = this[context];
if (port === '') {
ctx.port = null;
return;
}
parse(port, kPort, null, ctx,
FunctionPrototypeBind(onParsePortComplete, this));
}
},
pathname: {
enumerable: true,
configurable: true,
get() {
const ctx = this[context];
if (this[cannotBeBase])
return ctx.path[0];
if (ctx.path.length === 0)
return '';
return `/${ArrayPrototypeJoin(ctx.path, '/')}`;
},
set(path) {
// toUSVString is not needed.
path = `${path}`;
if (this[cannotBeBase])
return;
parse(path, kPathStart, null, this[context],
onParsePathComplete.bind(this));
}
},
search: {
enumerable: true,
configurable: true,
get() {
const { query } = this[context];
if (query === null || query === '')
return '';
return `?${query}`;
},
set(search) {
const ctx = this[context];
search = toUSVString(search);
if (search === '') {
ctx.query = null;
ctx.flags &= ~URL_FLAGS_HAS_QUERY;
} else {
if (search[0] === '?') search = StringPrototypeSlice(search, 1);
ctx.query = '';
ctx.flags |= URL_FLAGS_HAS_QUERY;
if (search) {
parse(search, kQuery, null, ctx,
FunctionPrototypeBind(onParseSearchComplete, this));
}
}
initSearchParams(this[searchParams], search);
}
},
searchParams: { // readonly
enumerable: true,
configurable: true,
get() {
return this[searchParams];
}
},
hash: {
enumerable: true,
configurable: true,
get() {
const { fragment } = this[context];
if (fragment === null || fragment === '')
return '';
return `#${fragment}`;
},
set(hash) {
const ctx = this[context];
// toUSVString is not needed.
hash = `${hash}`;
if (!hash) {
ctx.fragment = null;
ctx.flags &= ~URL_FLAGS_HAS_FRAGMENT;
return;
}
if (hash[0] === '#') hash = StringPrototypeSlice(hash, 1);
ctx.fragment = '';
ctx.flags |= URL_FLAGS_HAS_FRAGMENT;
parse(hash, kFragment, null, ctx,
FunctionPrototypeBind(onParseHashComplete, this));
}
},
toJSON: {
writable: true,
enumerable: true,
configurable: true,
// eslint-disable-next-line func-name-matching
value: function toJSON() {
return this[kFormat]({});
}
}
[kFormat]: { configurable: false, writable: false },
[SymbolToStringTag]: { configurable: true, value: 'URL' },
toString: { enumerable: true },
href: { enumerable: true },
origin: { enumerable: true },
protocol: { enumerable: true },
username: { enumerable: true },
password: { enumerable: true },
host: { enumerable: true },
hostname: { enumerable: true },
port: { enumerable: true },
pathname: { enumerable: true },
search: { enumerable: true },
searchParams: { enumerable: true },
hash: { enumerable: true },
toJSON: { enumerable: true },
});
function update(url, params) {
+100
View File
@@ -0,0 +1,100 @@
'use strict';
require('../common');
const assert = require('assert');
const { URL, URLSearchParams } = require('url');
[
{ name: 'toString' },
{ name: 'toJSON' },
{ name: Symbol.for('nodejs.util.inspect.custom') },
].forEach(({ name }) => {
testMethod(URL.prototype, name);
});
[
{ name: 'href' },
{ name: 'protocol' },
{ name: 'username' },
{ name: 'password' },
{ name: 'host' },
{ name: 'hostname' },
{ name: 'port' },
{ name: 'pathname' },
{ name: 'search' },
{ name: 'hash' },
{ name: 'origin', readonly: true },
{ name: 'searchParams', readonly: true },
].forEach(({ name, readonly = false }) => {
testAccessor(URL.prototype, name, readonly);
});
[
{ name: 'append' },
{ name: 'delete' },
{ name: 'get' },
{ name: 'getAll' },
{ name: 'has' },
{ name: 'set' },
{ name: 'sort' },
{ name: 'entries' },
{ name: 'forEach' },
{ name: 'keys' },
{ name: 'values' },
{ name: 'toString' },
{ name: Symbol.iterator, methodName: 'entries' },
{ name: Symbol.for('nodejs.util.inspect.custom') },
].forEach(({ name, methodName }) => {
testMethod(URLSearchParams.prototype, name, methodName);
});
function stringifyName(name) {
if (typeof name === 'symbol') {
const { description } = name;
if (description === undefined) {
return '';
}
return `[${description}]`;
}
return name;
}
function testMethod(target, name, methodName = stringifyName(name)) {
const desc = Object.getOwnPropertyDescriptor(target, name);
assert.notStrictEqual(desc, undefined);
assert.strictEqual(desc.enumerable, typeof name === 'string');
const { value } = desc;
assert.strictEqual(typeof value, 'function');
assert.strictEqual(value.name, methodName);
assert.strictEqual(
Object.prototype.hasOwnProperty.call(value, 'prototype'),
false,
);
}
function testAccessor(target, name, readonly = false) {
const desc = Object.getOwnPropertyDescriptor(target, name);
assert.notStrictEqual(desc, undefined);
assert.strictEqual(desc.enumerable, typeof name === 'string');
const methodName = stringifyName(name);
const { get, set } = desc;
assert.strictEqual(typeof get, 'function');
assert.strictEqual(get.name, `get ${methodName}`);
assert.strictEqual(
Object.prototype.hasOwnProperty.call(get, 'prototype'),
false,
);
if (readonly) {
assert.strictEqual(set, undefined);
} else {
assert.strictEqual(typeof set, 'function');
assert.strictEqual(set.name, `set ${methodName}`);
assert.strictEqual(
Object.prototype.hasOwnProperty.call(set, 'prototype'),
false,
);
}
}