mirror of
https://github.com/nodejs/node.git
synced 2025-05-01 17:03:34 +00:00

Removes the non-standard options on WHATWG URL toString and extends the existing url.format() API to support customizable serialization of the WHATWG URL object. This does not yet include the documentation updates because the documentation for the new WHATWG URL object has not yet landed. Example: ```js const url = require('url'); const URL = url.URL; const myURL = new URL('http://example.org/?a=b#c'); const str = url.format(myURL, {fragment: false, search: false}); console.log(str); // Prints: http://example.org/ ``` PR-URL: https://github.com/nodejs/node/pull/10857 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Timothy Gu <timothygu99@gmail.com> Reviewed-By: Brian White <mscdex@mscdex.net>
1123 lines
30 KiB
JavaScript
1123 lines
30 KiB
JavaScript
'use strict';
|
|
|
|
const util = require('util');
|
|
const binding = process.binding('url');
|
|
const context = Symbol('context');
|
|
const cannotBeBase = Symbol('cannot-be-base');
|
|
const special = Symbol('special');
|
|
const searchParams = Symbol('query');
|
|
const querystring = require('querystring');
|
|
|
|
const kScheme = Symbol('scheme');
|
|
const kHost = Symbol('host');
|
|
const kPort = Symbol('port');
|
|
const kDomain = Symbol('domain');
|
|
const kFormat = Symbol('format');
|
|
|
|
// https://tc39.github.io/ecma262/#sec-%iteratorprototype%-object
|
|
const IteratorPrototype = Object.getPrototypeOf(
|
|
Object.getPrototypeOf([][Symbol.iterator]())
|
|
);
|
|
|
|
function StorageObject() {}
|
|
StorageObject.prototype = Object.create(null);
|
|
|
|
class OpaqueOrigin {
|
|
toString() {
|
|
return 'null';
|
|
}
|
|
|
|
get effectiveDomain() {
|
|
return this;
|
|
}
|
|
}
|
|
|
|
class TupleOrigin {
|
|
constructor(scheme, host, port, domain) {
|
|
this[kScheme] = scheme;
|
|
this[kHost] = host;
|
|
this[kPort] = port;
|
|
this[kDomain] = domain;
|
|
}
|
|
|
|
get scheme() {
|
|
return this[kScheme];
|
|
}
|
|
|
|
get host() {
|
|
return this[kHost];
|
|
}
|
|
|
|
get port() {
|
|
return this[kPort];
|
|
}
|
|
|
|
get domain() {
|
|
return this[kDomain];
|
|
}
|
|
|
|
get effectiveDomain() {
|
|
return this[kDomain] || this[kHost];
|
|
}
|
|
|
|
// https://url.spec.whatwg.org/#dom-url-origin
|
|
toString(unicode = true) {
|
|
var result = this[kScheme];
|
|
result += '://';
|
|
result += unicode ? domainToUnicode(this[kHost]) : this[kHost];
|
|
if (this[kPort] !== undefined && this[kPort] !== null)
|
|
result += `:${this[kPort]}`;
|
|
return result;
|
|
}
|
|
|
|
[util.inspect.custom]() {
|
|
return `TupleOrigin {
|
|
scheme: ${this[kScheme]},
|
|
host: ${this[kHost]},
|
|
port: ${this[kPort]},
|
|
domain: ${this[kDomain]}
|
|
}`;
|
|
}
|
|
}
|
|
|
|
function onParseComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
throw new TypeError('Invalid URL');
|
|
var ctx = this[context];
|
|
ctx.flags = flags;
|
|
ctx.scheme = protocol;
|
|
ctx.username = username;
|
|
ctx.password = password;
|
|
ctx.port = port;
|
|
ctx.path = path;
|
|
ctx.query = query;
|
|
ctx.fragment = fragment;
|
|
ctx.host = host;
|
|
if (this[searchParams]) { // invoked from href setter
|
|
initSearchParams(this[searchParams], query);
|
|
} else {
|
|
this[searchParams] = new URLSearchParams(query);
|
|
}
|
|
this[searchParams][context] = this;
|
|
}
|
|
|
|
// Reused by URL constructor and URL#href setter.
|
|
function parse(url, input, base) {
|
|
input = String(input);
|
|
const base_context = base ? base[context] : undefined;
|
|
url[context] = new StorageObject();
|
|
binding.parse(input.trim(), -1,
|
|
base_context, undefined,
|
|
onParseComplete.bind(url));
|
|
}
|
|
|
|
function onParseProtocolComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
return;
|
|
const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) !== 0;
|
|
const s = this[special];
|
|
const ctx = this[context];
|
|
if ((s && !newIsSpecial) || (!s && newIsSpecial)) {
|
|
return;
|
|
}
|
|
if (newIsSpecial) {
|
|
ctx.flags |= binding.URL_FLAGS_SPECIAL;
|
|
} else {
|
|
ctx.flags &= ~binding.URL_FLAGS_SPECIAL;
|
|
}
|
|
if (protocol) {
|
|
ctx.scheme = protocol;
|
|
ctx.flags |= binding.URL_FLAGS_HAS_SCHEME;
|
|
} else {
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_SCHEME;
|
|
}
|
|
}
|
|
|
|
function onParseHostComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
return;
|
|
const ctx = this[context];
|
|
if (host) {
|
|
ctx.host = host;
|
|
ctx.flags |= binding.URL_FLAGS_HAS_HOST;
|
|
} else {
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
|
|
}
|
|
if (port !== undefined)
|
|
ctx.port = port;
|
|
}
|
|
|
|
function onParseHostnameComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
return;
|
|
const ctx = this[context];
|
|
if (host) {
|
|
ctx.host = host;
|
|
ctx.flags |= binding.URL_FLAGS_HAS_HOST;
|
|
} else {
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
|
|
}
|
|
}
|
|
|
|
function onParsePortComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
return;
|
|
this[context].port = port;
|
|
}
|
|
|
|
function onParsePathComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
return;
|
|
const ctx = this[context];
|
|
if (path) {
|
|
ctx.path = path;
|
|
ctx.flags |= binding.URL_FLAGS_HAS_PATH;
|
|
} else {
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_PATH;
|
|
}
|
|
}
|
|
|
|
function onParseSearchComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
return;
|
|
const ctx = this[context];
|
|
if (query) {
|
|
ctx.query = query;
|
|
ctx.flags |= binding.URL_FLAGS_HAS_QUERY;
|
|
} else {
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY;
|
|
}
|
|
}
|
|
|
|
function onParseHashComplete(flags, protocol, username, password,
|
|
host, port, path, query, fragment) {
|
|
if (flags & binding.URL_FLAGS_FAILED)
|
|
return;
|
|
const ctx = this[context];
|
|
if (fragment) {
|
|
ctx.fragment = fragment;
|
|
ctx.flags |= binding.URL_FLAGS_HAS_FRAGMENT;
|
|
} else {
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
|
|
}
|
|
}
|
|
|
|
class URL {
|
|
constructor(input, base) {
|
|
if (base !== undefined && !(base instanceof URL))
|
|
base = new URL(String(base));
|
|
parse(this, input, base);
|
|
}
|
|
|
|
get [special]() {
|
|
return (this[context].flags & binding.URL_FLAGS_SPECIAL) !== 0;
|
|
}
|
|
|
|
get [cannotBeBase]() {
|
|
return (this[context].flags & binding.URL_FLAGS_CANNOT_BE_BASE) !== 0;
|
|
}
|
|
|
|
[util.inspect.custom](depth, opts) {
|
|
const ctx = this[context];
|
|
var ret = 'URL {\n';
|
|
ret += ` href: ${this.href}\n`;
|
|
if (ctx.scheme !== undefined)
|
|
ret += ` protocol: ${this.protocol}\n`;
|
|
if (ctx.username !== undefined)
|
|
ret += ` username: ${this.username}\n`;
|
|
if (ctx.password !== undefined) {
|
|
const pwd = opts.showHidden ? ctx.password : '--------';
|
|
ret += ` password: ${pwd}\n`;
|
|
}
|
|
if (ctx.host !== undefined)
|
|
ret += ` hostname: ${this.hostname}\n`;
|
|
if (ctx.port !== undefined)
|
|
ret += ` port: ${this.port}\n`;
|
|
if (ctx.path !== undefined)
|
|
ret += ` pathname: ${this.pathname}\n`;
|
|
if (ctx.query !== undefined)
|
|
ret += ` search: ${this.search}\n`;
|
|
if (ctx.fragment !== undefined)
|
|
ret += ` hash: ${this.hash}\n`;
|
|
if (opts.showHidden) {
|
|
ret += ` cannot-be-base: ${this[cannotBeBase]}\n`;
|
|
ret += ` special: ${this[special]}\n`;
|
|
}
|
|
ret += '}';
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
Object.defineProperties(URL.prototype, {
|
|
[kFormat]: {
|
|
enumerable: false,
|
|
configurable: false,
|
|
// eslint-disable-next-line func-name-matching
|
|
value: function format(options) {
|
|
if (options && typeof options !== 'object')
|
|
throw new TypeError('options must be an object');
|
|
options = Object.assign({
|
|
fragment: true,
|
|
unicode: false,
|
|
search: true,
|
|
auth: true
|
|
}, options);
|
|
const ctx = this[context];
|
|
var ret;
|
|
if (this.protocol)
|
|
ret = this.protocol;
|
|
if (ctx.host !== undefined) {
|
|
ret += '//';
|
|
const has_username = typeof ctx.username === 'string';
|
|
const has_password = typeof ctx.password === 'string' &&
|
|
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(this.host) : this.host;
|
|
} else if (ctx.scheme === 'file:') {
|
|
ret += '//';
|
|
}
|
|
if (this.pathname)
|
|
ret += this.pathname;
|
|
if (options.search && typeof ctx.query === 'string')
|
|
ret += `?${ctx.query}`;
|
|
if (options.fragment && typeof ctx.fragment === 'string')
|
|
ret += `#${ctx.fragment}`;
|
|
return ret;
|
|
}
|
|
},
|
|
[Symbol.toStringTag]: {
|
|
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) {
|
|
parse(this, input);
|
|
}
|
|
},
|
|
origin: { // readonly
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
return originFor(this).toString();
|
|
}
|
|
},
|
|
protocol: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
return this[context].scheme;
|
|
},
|
|
set(scheme) {
|
|
scheme = String(scheme);
|
|
if (scheme.length === 0)
|
|
return;
|
|
binding.parse(scheme, binding.kSchemeStart, null, this[context],
|
|
onParseProtocolComplete.bind(this));
|
|
}
|
|
},
|
|
username: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
return this[context].username || '';
|
|
},
|
|
set(username) {
|
|
username = String(username);
|
|
if (!this.hostname)
|
|
return;
|
|
const ctx = this[context];
|
|
if (!username) {
|
|
ctx.username = null;
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_USERNAME;
|
|
return;
|
|
}
|
|
ctx.username = binding.encodeAuth(username);
|
|
ctx.flags |= binding.URL_FLAGS_HAS_USERNAME;
|
|
}
|
|
},
|
|
password: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
return this[context].password || '';
|
|
},
|
|
set(password) {
|
|
password = String(password);
|
|
if (!this.hostname)
|
|
return;
|
|
const ctx = this[context];
|
|
if (!password) {
|
|
ctx.password = null;
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_PASSWORD;
|
|
return;
|
|
}
|
|
ctx.password = binding.encodeAuth(password);
|
|
ctx.flags |= binding.URL_FLAGS_HAS_PASSWORD;
|
|
}
|
|
},
|
|
host: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
const ctx = this[context];
|
|
var ret = ctx.host || '';
|
|
if (ctx.port !== undefined)
|
|
ret += `:${ctx.port}`;
|
|
return ret;
|
|
},
|
|
set(host) {
|
|
const ctx = this[context];
|
|
host = String(host);
|
|
if (this[cannotBeBase] ||
|
|
(this[special] && host.length === 0)) {
|
|
// Cannot set the host if cannot-be-base is set or
|
|
// scheme is special and host length is zero
|
|
return;
|
|
}
|
|
if (!host) {
|
|
ctx.host = null;
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
|
|
return;
|
|
}
|
|
binding.parse(host, binding.kHost, null, ctx,
|
|
onParseHostComplete.bind(this));
|
|
}
|
|
},
|
|
hostname: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
return this[context].host || '';
|
|
},
|
|
set(host) {
|
|
const ctx = this[context];
|
|
host = String(host);
|
|
if (this[cannotBeBase] ||
|
|
(this[special] && host.length === 0)) {
|
|
// Cannot set the host if cannot-be-base is set or
|
|
// scheme is special and host length is zero
|
|
return;
|
|
}
|
|
if (!host) {
|
|
ctx.host = null;
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_HOST;
|
|
return;
|
|
}
|
|
binding.parse(host, binding.kHostname, null, ctx,
|
|
onParseHostnameComplete.bind(this));
|
|
}
|
|
},
|
|
port: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
const port = this[context].port;
|
|
return port === undefined ? '' : String(port);
|
|
},
|
|
set(port) {
|
|
const ctx = this[context];
|
|
if (!ctx.host || this[cannotBeBase] ||
|
|
this.protocol === 'file:')
|
|
return;
|
|
port = String(port);
|
|
if (port === '') {
|
|
ctx.port = undefined;
|
|
return;
|
|
}
|
|
binding.parse(port, binding.kPort, null, ctx,
|
|
onParsePortComplete.bind(this));
|
|
}
|
|
},
|
|
pathname: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
const ctx = this[context];
|
|
if (this[cannotBeBase])
|
|
return ctx.path[0];
|
|
return ctx.path !== undefined ? `/${ctx.path.join('/')}` : '';
|
|
},
|
|
set(path) {
|
|
if (this[cannotBeBase])
|
|
return;
|
|
binding.parse(String(path), binding.kPathStart, null, this[context],
|
|
onParsePathComplete.bind(this));
|
|
}
|
|
},
|
|
search: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
const ctx = this[context];
|
|
return !ctx.query ? '' : `?${ctx.query}`;
|
|
},
|
|
set(search) {
|
|
const ctx = this[context];
|
|
search = String(search);
|
|
if (!search) {
|
|
ctx.query = null;
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY;
|
|
this[searchParams][searchParams] = {};
|
|
return;
|
|
}
|
|
if (search[0] === '?') search = search.slice(1);
|
|
ctx.query = '';
|
|
binding.parse(search, binding.kQuery, null, ctx,
|
|
onParseSearchComplete.bind(this));
|
|
initSearchParams(this[searchParams], search);
|
|
}
|
|
},
|
|
searchParams: { // readonly
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
return this[searchParams];
|
|
}
|
|
},
|
|
hash: {
|
|
enumerable: true,
|
|
configurable: true,
|
|
get() {
|
|
const ctx = this[context];
|
|
return !ctx.fragment ? '' : `#${ctx.fragment}`;
|
|
},
|
|
set(hash) {
|
|
const ctx = this[context];
|
|
hash = String(hash);
|
|
if (this.protocol === 'javascript:')
|
|
return;
|
|
if (!hash) {
|
|
ctx.fragment = null;
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_FRAGMENT;
|
|
return;
|
|
}
|
|
if (hash[0] === '#') hash = hash.slice(1);
|
|
ctx.fragment = '';
|
|
binding.parse(hash, binding.kFragment, null, ctx,
|
|
onParseHashComplete.bind(this));
|
|
}
|
|
}
|
|
});
|
|
|
|
const hexTable = new Array(256);
|
|
|
|
for (var i = 0; i < 256; ++i)
|
|
hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase();
|
|
function encodeAuth(str) {
|
|
// faster encodeURIComponent alternative for encoding auth uri components
|
|
var out = '';
|
|
var lastPos = 0;
|
|
for (var i = 0; i < str.length; ++i) {
|
|
var c = str.charCodeAt(i);
|
|
|
|
// These characters do not need escaping:
|
|
// ! - . _ ~
|
|
// ' ( ) * :
|
|
// digits
|
|
// alpha (uppercase)
|
|
// alpha (lowercase)
|
|
if (c === 0x21 || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E ||
|
|
(c >= 0x27 && c <= 0x2A) ||
|
|
(c >= 0x30 && c <= 0x3A) ||
|
|
(c >= 0x41 && c <= 0x5A) ||
|
|
(c >= 0x61 && c <= 0x7A)) {
|
|
continue;
|
|
}
|
|
|
|
if (i - lastPos > 0)
|
|
out += str.slice(lastPos, i);
|
|
|
|
lastPos = i + 1;
|
|
|
|
// Other ASCII characters
|
|
if (c < 0x80) {
|
|
out += hexTable[c];
|
|
continue;
|
|
}
|
|
|
|
// Multi-byte characters ...
|
|
if (c < 0x800) {
|
|
out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)];
|
|
continue;
|
|
}
|
|
if (c < 0xD800 || c >= 0xE000) {
|
|
out += hexTable[0xE0 | (c >> 12)] +
|
|
hexTable[0x80 | ((c >> 6) & 0x3F)] +
|
|
hexTable[0x80 | (c & 0x3F)];
|
|
continue;
|
|
}
|
|
// Surrogate pair
|
|
++i;
|
|
var c2;
|
|
if (i < str.length)
|
|
c2 = str.charCodeAt(i) & 0x3FF;
|
|
else
|
|
c2 = 0;
|
|
c = 0x10000 + (((c & 0x3FF) << 10) | c2);
|
|
out += hexTable[0xF0 | (c >> 18)] +
|
|
hexTable[0x80 | ((c >> 12) & 0x3F)] +
|
|
hexTable[0x80 | ((c >> 6) & 0x3F)] +
|
|
hexTable[0x80 | (c & 0x3F)];
|
|
}
|
|
if (lastPos === 0)
|
|
return str;
|
|
if (lastPos < str.length)
|
|
return out + str.slice(lastPos);
|
|
return out;
|
|
}
|
|
|
|
function update(url, params) {
|
|
if (!url)
|
|
return;
|
|
|
|
const ctx = url[context];
|
|
const serializedParams = params.toString();
|
|
if (serializedParams) {
|
|
ctx.query = serializedParams;
|
|
ctx.flags |= binding.URL_FLAGS_HAS_QUERY;
|
|
} else {
|
|
ctx.query = null;
|
|
ctx.flags &= ~binding.URL_FLAGS_HAS_QUERY;
|
|
}
|
|
}
|
|
|
|
// Reused by the URL parse function invoked by
|
|
// the href setter, and the URLSearchParams constructor
|
|
function initSearchParams(url, init) {
|
|
url[searchParams] = getParamsFromObject(querystring.parse(init));
|
|
}
|
|
|
|
function getParamsFromObject(obj) {
|
|
const keys = Object.keys(obj);
|
|
const values = [];
|
|
for (var i = 0; i < keys.length; i++) {
|
|
const name = keys[i];
|
|
const value = obj[name];
|
|
if (Array.isArray(value)) {
|
|
for (const item of value)
|
|
values.push(name, item);
|
|
} else {
|
|
values.push(name, value);
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function getObjectFromParams(array) {
|
|
const obj = new StorageObject();
|
|
for (var i = 0; i < array.length; i += 2) {
|
|
const name = array[i];
|
|
const value = array[i + 1];
|
|
if (obj[name]) {
|
|
obj[name].push(value);
|
|
} else {
|
|
obj[name] = [value];
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
// Mainly to mitigate func-name-matching ESLint rule
|
|
function defineIDLClass(proto, classStr, obj) {
|
|
// https://heycam.github.io/webidl/#dfn-class-string
|
|
Object.defineProperty(proto, Symbol.toStringTag, {
|
|
writable: false,
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: classStr
|
|
});
|
|
|
|
// https://heycam.github.io/webidl/#es-operations
|
|
for (const key of Object.keys(obj)) {
|
|
Object.defineProperty(proto, key, {
|
|
writable: true,
|
|
enumerable: true,
|
|
configurable: true,
|
|
value: obj[key]
|
|
});
|
|
}
|
|
for (const key of Object.getOwnPropertySymbols(obj)) {
|
|
Object.defineProperty(proto, key, {
|
|
writable: true,
|
|
enumerable: false,
|
|
configurable: true,
|
|
value: obj[key]
|
|
});
|
|
}
|
|
}
|
|
|
|
class URLSearchParams {
|
|
// URL Standard says the default value is '', but as undefined and '' have
|
|
// the same result, undefined is used to prevent unnecessary parsing.
|
|
// Default parameter is necessary to keep URLSearchParams.length === 0 in
|
|
// accordance with Web IDL spec.
|
|
constructor(init = undefined) {
|
|
if (init === null || init === undefined) {
|
|
this[searchParams] = [];
|
|
} else if (typeof init === 'object') {
|
|
const method = init[Symbol.iterator];
|
|
if (method === this[Symbol.iterator]) {
|
|
// While the spec does not have this branch, we can use it as a
|
|
// shortcut to avoid having to go through the costly generic iterator.
|
|
const childParams = init[searchParams];
|
|
this[searchParams] = childParams.slice();
|
|
} else if (method !== null && method !== undefined) {
|
|
if (typeof method !== 'function') {
|
|
throw new TypeError('Query pairs must be iterable');
|
|
}
|
|
|
|
// sequence<sequence<USVString>>
|
|
// Note: per spec we have to first exhaust the lists then process them
|
|
const pairs = [];
|
|
for (const pair of init) {
|
|
if (typeof pair !== 'object' ||
|
|
typeof pair[Symbol.iterator] !== 'function') {
|
|
throw new TypeError('Each query pair must be iterable');
|
|
}
|
|
pairs.push(Array.from(pair));
|
|
}
|
|
|
|
this[searchParams] = [];
|
|
for (const pair of pairs) {
|
|
if (pair.length !== 2) {
|
|
throw new TypeError('Each query pair must be a name/value tuple');
|
|
}
|
|
this[searchParams].push(String(pair[0]), String(pair[1]));
|
|
}
|
|
} else {
|
|
// record<USVString, USVString>
|
|
this[searchParams] = [];
|
|
for (const key of Object.keys(init)) {
|
|
const value = String(init[key]);
|
|
this[searchParams].push(key, value);
|
|
}
|
|
}
|
|
} else {
|
|
// USVString
|
|
init = String(init);
|
|
if (init[0] === '?') init = init.slice(1);
|
|
initSearchParams(this, init);
|
|
}
|
|
|
|
// "associated url object"
|
|
this[context] = null;
|
|
}
|
|
|
|
[util.inspect.custom](recurseTimes, ctx) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
|
|
const separator = ', ';
|
|
const innerOpts = Object.assign({}, ctx);
|
|
if (recurseTimes !== null) {
|
|
innerOpts.depth = recurseTimes - 1;
|
|
}
|
|
const innerInspect = (v) => util.inspect(v, innerOpts);
|
|
|
|
const list = this[searchParams];
|
|
const output = [];
|
|
for (var i = 0; i < list.length; i += 2)
|
|
output.push(`${innerInspect(list[i])} => ${innerInspect(list[i + 1])}`);
|
|
|
|
const colorRe = /\u001b\[\d\d?m/g;
|
|
const length = output.reduce(
|
|
(prev, cur) => prev + cur.replace(colorRe, '').length + separator.length,
|
|
-separator.length
|
|
);
|
|
if (length > ctx.breakLength) {
|
|
return `${this.constructor.name} {\n ${output.join(',\n ')} }`;
|
|
} else if (output.length) {
|
|
return `${this.constructor.name} { ${output.join(separator)} }`;
|
|
} else {
|
|
return `${this.constructor.name} {}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
defineIDLClass(URLSearchParams.prototype, 'URLSearchParams', {
|
|
append(name, value) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
if (arguments.length < 2) {
|
|
throw new TypeError('"name" and "value" arguments must be specified');
|
|
}
|
|
|
|
name = String(name);
|
|
value = String(value);
|
|
this[searchParams].push(name, value);
|
|
update(this[context], this);
|
|
},
|
|
|
|
delete(name) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
if (arguments.length < 1) {
|
|
throw new TypeError('"name" argument must be specified');
|
|
}
|
|
|
|
const list = this[searchParams];
|
|
name = String(name);
|
|
for (var i = 0; i < list.length;) {
|
|
const cur = list[i];
|
|
if (cur === name) {
|
|
list.splice(i, 2);
|
|
} else {
|
|
i += 2;
|
|
}
|
|
}
|
|
update(this[context], this);
|
|
},
|
|
|
|
get(name) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
if (arguments.length < 1) {
|
|
throw new TypeError('"name" argument must be specified');
|
|
}
|
|
|
|
const list = this[searchParams];
|
|
name = String(name);
|
|
for (var i = 0; i < list.length; i += 2) {
|
|
if (list[i] === name) {
|
|
return list[i + 1];
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
getAll(name) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
if (arguments.length < 1) {
|
|
throw new TypeError('"name" argument must be specified');
|
|
}
|
|
|
|
const list = this[searchParams];
|
|
const values = [];
|
|
name = String(name);
|
|
for (var i = 0; i < list.length; i += 2) {
|
|
if (list[i] === name) {
|
|
values.push(list[i + 1]);
|
|
}
|
|
}
|
|
return values;
|
|
},
|
|
|
|
has(name) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
if (arguments.length < 1) {
|
|
throw new TypeError('"name" argument must be specified');
|
|
}
|
|
|
|
const list = this[searchParams];
|
|
name = String(name);
|
|
for (var i = 0; i < list.length; i += 2) {
|
|
if (list[i] === name) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
set(name, value) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
if (arguments.length < 2) {
|
|
throw new TypeError('"name" and "value" arguments must be specified');
|
|
}
|
|
|
|
const list = this[searchParams];
|
|
name = String(name);
|
|
value = String(value);
|
|
|
|
// If there are any name-value pairs whose name is `name`, in `list`, set
|
|
// the value of the first such name-value pair to `value` and remove the
|
|
// others.
|
|
var found = false;
|
|
for (var i = 0; i < list.length;) {
|
|
const cur = list[i];
|
|
if (cur === name) {
|
|
if (!found) {
|
|
list[i + 1] = value;
|
|
found = true;
|
|
i += 2;
|
|
} else {
|
|
list.splice(i, 2);
|
|
}
|
|
} else {
|
|
i += 2;
|
|
}
|
|
}
|
|
|
|
// Otherwise, append a new name-value pair whose name is `name` and value
|
|
// is `value`, to `list`.
|
|
if (!found) {
|
|
list.push(name, value);
|
|
}
|
|
|
|
update(this[context], this);
|
|
},
|
|
|
|
// https://heycam.github.io/webidl/#es-iterators
|
|
// Define entries here rather than [Symbol.iterator] as the function name
|
|
// must be set to `entries`.
|
|
entries() {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
|
|
return createSearchParamsIterator(this, 'key+value');
|
|
},
|
|
|
|
forEach(callback, thisArg = undefined) {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('"callback" argument must be a function');
|
|
}
|
|
|
|
let list = this[searchParams];
|
|
|
|
var i = 0;
|
|
while (i < list.length) {
|
|
const key = list[i];
|
|
const value = list[i + 1];
|
|
callback.call(thisArg, value, key, this);
|
|
// in case the URL object's `search` is updated
|
|
list = this[searchParams];
|
|
i += 2;
|
|
}
|
|
},
|
|
|
|
// https://heycam.github.io/webidl/#es-iterable
|
|
keys() {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
|
|
return createSearchParamsIterator(this, 'key');
|
|
},
|
|
|
|
values() {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
|
|
return createSearchParamsIterator(this, 'value');
|
|
},
|
|
|
|
// https://heycam.github.io/webidl/#es-stringifier
|
|
// https://url.spec.whatwg.org/#urlsearchparams-stringification-behavior
|
|
toString() {
|
|
if (!this || !(this instanceof URLSearchParams)) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParams');
|
|
}
|
|
|
|
return querystring.stringify(getObjectFromParams(this[searchParams]));
|
|
}
|
|
});
|
|
|
|
// https://heycam.github.io/webidl/#es-iterable-entries
|
|
Object.defineProperty(URLSearchParams.prototype, Symbol.iterator, {
|
|
writable: true,
|
|
configurable: true,
|
|
value: URLSearchParams.prototype.entries
|
|
});
|
|
|
|
// https://heycam.github.io/webidl/#dfn-default-iterator-object
|
|
function createSearchParamsIterator(target, kind) {
|
|
const iterator = Object.create(URLSearchParamsIteratorPrototype);
|
|
iterator[context] = {
|
|
target,
|
|
kind,
|
|
index: 0
|
|
};
|
|
return iterator;
|
|
}
|
|
|
|
// https://heycam.github.io/webidl/#dfn-iterator-prototype-object
|
|
const URLSearchParamsIteratorPrototype = Object.create(IteratorPrototype);
|
|
|
|
defineIDLClass(URLSearchParamsIteratorPrototype, 'URLSearchParamsIterator', {
|
|
next() {
|
|
if (!this ||
|
|
Object.getPrototypeOf(this) !== URLSearchParamsIteratorPrototype) {
|
|
throw new TypeError('Value of `this` is not a URLSearchParamsIterator');
|
|
}
|
|
|
|
const {
|
|
target,
|
|
kind,
|
|
index
|
|
} = this[context];
|
|
const values = target[searchParams];
|
|
const len = values.length;
|
|
if (index >= len) {
|
|
return {
|
|
value: undefined,
|
|
done: true
|
|
};
|
|
}
|
|
|
|
const name = values[index];
|
|
const value = values[index + 1];
|
|
this[context].index = index + 2;
|
|
|
|
let result;
|
|
if (kind === 'key') {
|
|
result = name;
|
|
} else if (kind === 'value') {
|
|
result = value;
|
|
} else {
|
|
result = [name, value];
|
|
}
|
|
|
|
return {
|
|
value: result,
|
|
done: false
|
|
};
|
|
},
|
|
[util.inspect.custom](recurseTimes, ctx) {
|
|
const innerOpts = Object.assign({}, ctx);
|
|
if (recurseTimes !== null) {
|
|
innerOpts.depth = recurseTimes - 1;
|
|
}
|
|
const {
|
|
target,
|
|
kind,
|
|
index
|
|
} = this[context];
|
|
const output = target[searchParams].slice(index).reduce((prev, cur, i) => {
|
|
const key = i % 2 === 0;
|
|
if (kind === 'key' && key) {
|
|
prev.push(cur);
|
|
} else if (kind === 'value' && !key) {
|
|
prev.push(cur);
|
|
} else if (kind === 'key+value' && !key) {
|
|
prev.push([target[searchParams][index + i - 1], cur]);
|
|
}
|
|
return prev;
|
|
}, []);
|
|
const breakLn = util.inspect(output, innerOpts).includes('\n');
|
|
const outputStrs = output.map((p) => util.inspect(p, innerOpts));
|
|
let outputStr;
|
|
if (breakLn) {
|
|
outputStr = `\n ${outputStrs.join(',\n ')}`;
|
|
} else {
|
|
outputStr = ` ${outputStrs.join(', ')}`;
|
|
}
|
|
return `${this[Symbol.toStringTag]} {${outputStr} }`;
|
|
}
|
|
});
|
|
|
|
function originFor(url, base) {
|
|
if (!(url instanceof URL))
|
|
url = new URL(url, base);
|
|
var origin;
|
|
const protocol = url.protocol;
|
|
switch (protocol) {
|
|
case 'blob:':
|
|
if (url[context].path && url[context].path.length > 0) {
|
|
try {
|
|
return (new URL(url[context].path[0])).origin;
|
|
} catch (err) {
|
|
// fall through... do nothing
|
|
}
|
|
}
|
|
origin = new OpaqueOrigin();
|
|
break;
|
|
case 'ftp:':
|
|
case 'gopher:':
|
|
case 'http:':
|
|
case 'https:':
|
|
case 'ws:':
|
|
case 'wss:':
|
|
case 'file':
|
|
origin = new TupleOrigin(protocol.slice(0, -1),
|
|
url[context].host,
|
|
url[context].port,
|
|
null);
|
|
break;
|
|
default:
|
|
origin = new OpaqueOrigin();
|
|
}
|
|
return origin;
|
|
}
|
|
|
|
function domainToASCII(domain) {
|
|
return binding.domainToASCII(String(domain));
|
|
}
|
|
|
|
function domainToUnicode(domain) {
|
|
return binding.domainToUnicode(String(domain));
|
|
}
|
|
|
|
// Utility function that converts a URL object into an ordinary
|
|
// options object as expected by the http.request and https.request
|
|
// APIs.
|
|
function urlToOptions(url) {
|
|
var options = {
|
|
protocol: url.protocol,
|
|
host: url.host,
|
|
hostname: url.hostname,
|
|
hash: url.hash,
|
|
search: url.search,
|
|
pathname: url.pathname,
|
|
path: `${url.pathname}${url.search}`,
|
|
href: url.href
|
|
};
|
|
if (url.port !== '') {
|
|
options.port = Number(url.port);
|
|
}
|
|
if (url.username || url.password) {
|
|
options.auth = `${url.username}:${url.password}`;
|
|
}
|
|
return options;
|
|
}
|
|
|
|
exports.URL = URL;
|
|
exports.URLSearchParams = URLSearchParams;
|
|
exports.domainToASCII = domainToASCII;
|
|
exports.domainToUnicode = domainToUnicode;
|
|
exports.encodeAuth = encodeAuth;
|
|
exports.urlToOptions = urlToOptions;
|
|
exports.formatSymbol = kFormat;
|