You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1375 lines
36 KiB

"use strict";
const punycode = require("punycode");
const tr46 = require("tr46");
/*jshint unused: false */
const specialSchemas = {
"ftp": "21",
"file": null,
"gopher": "70",
"http": "80",
"https": "443",
"ws": "80",
"wss": "443"
};
const localSchemas = [
"about",
"blob",
"data",
"filesystem"
];
const bufferReplacement = {
"%2e": ".",
".%2e": "..",
"%2e.": "..",
"%2e%2e": ".."
};
const STATES = {
SCHEME_START: 1,
SCHEME: 2,
NO_SCHEME: 3,
RELATIVE: 4,
SPECIAL_RELATIVE_OR_AUTHORITY: 5,
SPECIAL_AUTHORITY_SLASHES: 6,
NON_RELATIVE_PATH: 7,
QUERY: 8,
FRAGMENT: 9,
SPECIAL_AUTHORITY_IGNORE_SLASHES: 10,
RELATIVE_SLASH: 11,
PATH: 12,
FILE_HOST: 13,
AUTHORITY: 14,
HOST: 15,
PATH_START: 16,
HOST_NAME: 17,
PORT: 18,
PATH_OR_AUTHORITY: 19
};
function countSymbols(str) {
return punycode.ucs2.decode(str).length;
}
function at(input, idx) {
const c = input[idx];
return isNaN(c) ? undefined : String.fromCodePoint(c);
}
function isASCIIDigit(c) {
return c >= 0x30 && c <= 0x39;
}
function isASCIIAlpha(c) {
return (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A);
}
function isASCIIHex(c) {
return isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66);
}
function percentEncode(c) {
let hex = c.toString(16).toUpperCase();
if (hex.length === 1) {
hex = "0" + hex;
}
return "%" + hex;
}
const invalidCodePoint = String.fromCodePoint(65533);
function utf8PercentEncode(c) {
const buf = new Buffer(c);
if (buf.toString() === invalidCodePoint) {
return "";
}
let str = "";
for (let i = 0; i < buf.length; ++i) {
str += percentEncode(buf[i]);
}
return str;
}
function simpleEncode(c) {
const c_str = String.fromCodePoint(c);
if (c < 0x20 || c > 0x7E) {
return utf8PercentEncode(c_str);
} else {
return c_str;
}
}
function defaultEncode(c) {
const c_str = String.fromCodePoint(c);
if (c <= 0x20 || c >= 0x7E || c_str === "\"" || c_str === "#" ||
c_str === "<" || c_str === ">" || c_str === "?" || c_str === "`" ||
c_str === "{" || c_str === "}") {
return utf8PercentEncode(c_str);
} else {
return c_str;
}
}
function passwordEncode(c) {
const c_str = String.fromCodePoint(c);
if (c <= 0x20 || c >= 0x7E || c_str === "\"" || c_str === "#" ||
c_str === "<" || c_str === ">" || c_str === "?" || c_str === "`" ||
c_str === "{" || c_str === "}" ||
c_str === "/" || c_str === "@" || c_str === "\\") {
return utf8PercentEncode(c_str);
} else {
return c_str;
}
}
function usernameEncode(c) {
const c_str = String.fromCodePoint(c);
if (c <= 0x20 || c >= 0x7E || c_str === "\"" || c_str === "#" ||
c_str === "<" || c_str === ">" || c_str === "?" || c_str === "`" ||
c_str === "{" || c_str === "}" ||
c_str === "/" || c_str === "@" || c_str === "\\" || c_str === ":") {
return utf8PercentEncode(c_str);
} else {
return c_str;
}
}
function parseIPv4Number(input) {
let R = 10;
if (input.length >= 2 && input.charAt(0) === "0" && input.charAt(1).toLowerCase() === "x") {
input = input.substring(2);
R = 16;
} else if (input.length >= 2 && input.charAt(0) === "0") {
input = input.substring(1);
R = 8;
}
if (input === "") {
return 0;
}
const regex = R === 10 ? /[^0-9]/ : (R === 16 ? /[^0-9A-Fa-f]/ : /[^0-7]/);
if (regex.test(input)) {
return null;
}
return parseInt(input, R);
}
function parseIPv4(input) {
let parts = input.split(".");
if (parts[parts.length - 1] === "") {
parts.pop();
}
if (parts.length > 4) {
return input;
}
let numbers = [];
for (const part of parts) {
const n = parseIPv4Number(part);
if (n === null) {
return input;
}
numbers.push(n);
}
for (let i = 0; i < numbers.length - 1; ++i) {
if (numbers[i] > 255) {
throw new TypeError("Invalid Host");
}
}
if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) {
throw new TypeError("Invalid Host");
}
let ipv4 = numbers.pop();
let counter = 0;
for (const n of numbers) {
ipv4 += n * Math.pow(256, 3 - counter);
++counter;
}
return ipv4;
}
function serializeIPv4(address) {
let output = "";
let n = address;
for (let i = 0; i < 4; ++i) {
output = String(n % 256) + output;
if (i !== 3) {
output = "." + output;
}
n = Math.floor(n / 256);
}
return output;
}
function parseIPv6(input) {
const ip = [0, 0, 0, 0, 0, 0, 0, 0];
let piecePtr = 0;
let compressPtr = null;
let pointer = 0;
input = punycode.ucs2.decode(input);
if (at(input, pointer) === ":") {
if (at(input, pointer + 1) !== ":") {
throw new TypeError("Invalid Host");
}
pointer += 2;
++piecePtr;
compressPtr = piecePtr;
}
let ipv4 = false;
Main:
while (pointer < input.length) {
if (piecePtr === 8) {
throw new TypeError("Invalid Host");
}
if (at(input, pointer) === ":") {
if (compressPtr !== null) {
throw new TypeError("Invalid Host");
}
++pointer;
++piecePtr;
compressPtr = piecePtr;
continue;
}
let value = 0;
let length = 0;
while (length < 4 && isASCIIHex(input[pointer])) {
value = value * 0x10 + parseInt(at(input, pointer), 16);
++pointer;
++length;
}
switch (at(input, pointer)) {
case ".":
if (length === 0) {
throw new TypeError("Invalid Host");
}
pointer -= length;
ipv4 = true;
break Main;
case ":":
++pointer;
if (input[pointer] === undefined) {
throw new TypeError("Invalid Host");
}
break;
case undefined:
break;
default:
throw new TypeError("Invalid Host");
}
ip[piecePtr] = value;
++piecePtr;
}
if (ipv4 && piecePtr > 6) {
throw new TypeError("Invalid Host");
} else if (input[pointer] !== undefined) {
let dotsSeen = 0;
while (input[pointer] !== undefined) {
let value = null;
if (!isASCIIDigit(input[pointer])) {
throw new TypeError("Invalid Host");
}
while (isASCIIDigit(input[pointer])) {
const number = parseInt(at(input, pointer), 10);
if (value === null) {
value = number;
} else if (value === 0) {
throw new TypeError("Invalid Host");
} else {
value = value * 10 + number;
}
++pointer;
if (value > 255) {
throw new TypeError("Invalid Host");
}
}
if (dotsSeen < 3 && at(input, pointer) !== ".") {
throw new TypeError("Invalid Host");
}
ip[piecePtr] = ip[piecePtr] * 0x100 + value;
if (dotsSeen === 1 || dotsSeen === 3) {
++piecePtr;
}
if (input[pointer] !== undefined) {
++pointer;
}
if (dotsSeen === 3 && input[pointer] !== undefined) {
throw new TypeError("Invalid Host");
}
++dotsSeen;
}
}
if (compressPtr !== null) {
let swaps = piecePtr - compressPtr;
piecePtr = 7;
while (piecePtr !== 0 && swaps > 0) {
const temp = ip[compressPtr + swaps - 1]; // piece
ip[compressPtr + swaps - 1] = ip[piecePtr];
ip[piecePtr] = temp;
--piecePtr;
--swaps;
}
} else if (piecePtr !== 8) {
throw new TypeError("Invalid Host");
}
return ip;
}
function serializeIPv6(address) {
let output = "";
const seqResult = findLongestZeroSequence(address);
const compressPtr = seqResult.idx;
for (var i = 0; i < address.length; ++i) {
if (compressPtr === i) {
if (i === 0) {
output += "::";
} else {
output += ":";
}
i += seqResult.len - 1;
continue;
}
output += address[i].toString(16);
if (i !== address.length - 1) {
output += ":";
}
}
return output;
}
function parseHost(input, isUnicode) {
if (input[0] === "[") {
if (input[input.length - 1] !== "]") {
throw new TypeError("Invalid Host");
}
return parseIPv6(input.substring(1, input.length - 1));
}
let domain;
try {
domain = decodeURIComponent(input);
} catch (e) {
throw new TypeError("Error while decoding host");
}
const asciiDomain = tr46.toASCII(domain, false, tr46.PROCESSING_OPTIONS.TRANSITIONAL, false);
if (asciiDomain === null) {
throw new TypeError("Invalid Host");
}
if (asciiDomain.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/) !== -1) {
throw new TypeError("Invalid Host");
}
let ipv4Host = parseIPv4(asciiDomain);
if (typeof ipv4Host === "number") {
return ipv4Host;
}
return isUnicode ? tr46.toUnicode(asciiDomain, false).domain : asciiDomain;
}
function findLongestZeroSequence(arr) {
let maxIdx = null;
let maxLen = 1; // only find elements > 1
let currStart = null;
let currLen = 0;
for (var i = 0; i < arr.length; ++i) {
if (arr[i] !== 0) {
if (currLen > maxLen) {
maxIdx = currStart;
maxLen = currLen;
}
currStart = null;
currLen = 0;
} else {
if (currStart === null) {
currStart = i;
}
++currLen;
}
}
return {
idx: maxIdx,
len: maxLen
};
}
function serializeHost(host) {
if (typeof host === "number") {
return serializeIPv4(host);
}
// IPv6 serializer
if (host instanceof Array) {
return "[" + serializeIPv6(host) + "]";
}
return host;
}
function trimControlChars(url) {
return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, "");
}
function URLStateMachine(input, base, encoding_override, url, state_override) {
this.pointer = 0;
this.input = input;
this.base = base || null;
this.encoding_override = encoding_override || "utf-8";
this.state_override = state_override;
this.url = url;
if (!this.url) {
this.url = {
scheme: "",
username: "",
password: null,
host: null,
port: "",
path: [],
query: null,
fragment: null,
nonRelative: false
};
this.input = trimControlChars(this.input);
}
this.state = state_override || STATES.SCHEME_START;
this.buffer = "";
this.at_flag = false;
this.arr_flag = false;
this.parse_error = false;
this.input = punycode.ucs2.decode(this.input);
for (; this.pointer <= this.input.length; ++this.pointer) {
const c = this.input[this.pointer];
const c_str = isNaN(c) ? undefined : String.fromCodePoint(c);
// exec state machine
if (this["parse" + this.state](c, c_str) === false) {
break; // terminate algorithm
}
}
}
URLStateMachine.prototype["parse" + STATES.SCHEME_START] =
function parseSchemeStart(c, c_str) {
if (isASCIIAlpha(c)) {
this.buffer += c_str.toLowerCase();
this.state = STATES.SCHEME;
} else if (!this.state_override) {
this.state = STATES.NO_SCHEME;
--this.pointer;
} else {
this.parse_error = true;
return false;
}
};
URLStateMachine.prototype["parse" + STATES.SCHEME] =
function parseScheme(c, c_str) {
if (isASCIIAlpha(c) || c_str === "+" || c_str === "-" || c_str === ".") {
this.buffer += c_str.toLowerCase();
} else if (c_str === ":") {
if (this.state_override) {
// TODO: XOR
if (specialSchemas[this.url.scheme] !== undefined && !specialSchemas[this.buffer]) {
return false;
} else if (specialSchemas[this.url.scheme] === undefined && specialSchemas[this.buffer]) {
return false;
}
}
this.url.scheme = this.buffer;
this.buffer = "";
if (this.state_override) {
return false;
}
if (this.url.scheme === "file") {
this.state = STATES.RELATIVE;
} else if (specialSchemas[this.url.scheme] !== undefined && this.base !== null &&
this.base.scheme === this.url.scheme) {
this.state = STATES.SPECIAL_RELATIVE_OR_AUTHORITY;
} else if (specialSchemas[this.url.scheme] !== undefined) {
this.state = STATES.SPECIAL_AUTHORITY_SLASHES;
} else if (at(this.input, this.pointer + 1) === "/") {
this.state = STATES.PATH_OR_AUTHORITY;
++this.pointer;
} else {
this.url.nonRelative = true;
this.url.path.push("");
this.state = STATES.NON_RELATIVE_PATH;
}
} else if (!this.state_override) {
this.buffer = "";
this.state = STATES.NO_SCHEME;
this.pointer = -1;
} else {
this.parse_error = true;
return false;
}
};
URLStateMachine.prototype["parse" + STATES.NO_SCHEME] =
function parseNoScheme(c, c_str) {
//jshint unused:false
if (this.base === null || (this.base.nonRelative && c_str !== "#")) {
throw new TypeError("Invalid URL");
} else if (this.base.nonRelative && c_str === "#") {
this.url.scheme = this.base.scheme;
this.url.path = this.base.path.slice();
this.url.query = this.base.query;
this.url.fragment = "";
this.url.nonRelative = true;
this.state = STATES.FRAGMENT;
} else {
this.state = STATES.RELATIVE;
--this.pointer;
}
};
URLStateMachine.prototype["parse" + STATES.SPECIAL_RELATIVE_OR_AUTHORITY] =
function parseSpecialRelativeOrAuthority(c, c_str) {
if (c_str === "/" && at(this.input, this.pointer + 1) === "/") {
this.state = STATES.SPECIAL_AUTHORITY_IGNORE_SLASHES;
++this.pointer;
} else {
this.parse_error = true;
this.state = STATES.RELATIVE;
--this.pointer;
}
};
URLStateMachine.prototype["parse" + STATES.PATH_OR_AUTHORITY] =
function parsePathOrAuthority(c, c_str) {
if (c_str === "/") {
this.state = STATES.AUTHORITY;
} else {
this.state = STATES.PATH;
--this.pointer;
}
};
URLStateMachine.prototype["parse" + STATES.RELATIVE] =
function parseRelative(c, c_str) {
if (this.url.scheme !== "file") {
this.url.scheme = this.base.scheme;
}
if (isNaN(c)) {
this.url.username = this.base.username;
this.url.password = this.base.password;
this.url.host = this.base.host;
this.url.port = this.base.port;
this.url.path = this.base.path.slice();
this.url.query = this.base.query;
} else if (c_str === "/") {
this.state = STATES.RELATIVE_SLASH;
} else if (c_str === "?") {
this.url.username = this.base.username;
this.url.password = this.base.password;
this.url.host = this.base.host;
this.url.port = this.base.port;
this.url.path = this.base.path.slice();
this.url.query = "";
this.state = STATES.QUERY;
} else if (c_str === "#") {
this.url.username = this.base.username;
this.url.password = this.base.password;
this.url.host = this.base.host;
this.url.port = this.base.port;
this.url.path = this.base.path.slice();
this.url.query = this.base.query;
this.url.fragment = "";
this.state = STATES.FRAGMENT;
} else if (specialSchemas[this.url.scheme] !== undefined && c_str === "\\") {
this.parse_error = true;
this.state = STATES.RELATIVE_SLASH;
} else {
let nextChar = at(this.input, this.pointer + 1);
let nextNextChar = at(this.input, this.pointer + 2);
if (this.url.scheme !== "file" || !isASCIIAlpha(c) || !(nextChar === ":" || nextChar === "|") ||
this.input.length - this.pointer === 1 || !(nextNextChar === "/" || nextNextChar === "\\" ||
nextNextChar === "?" || nextNextChar === "#")) {
this.url.username = this.base.username;
this.url.password = this.base.password;
this.url.host = this.base.host;
this.url.port = this.base.port;
this.url.path = this.base.path.slice(0, this.base.path.length - 1);
}
this.state = STATES.PATH;
--this.pointer;
}
};
URLStateMachine.prototype["parse" + STATES.RELATIVE_SLASH] =
function parseRelativeSlash(c, c_str) {
if (c_str === "/" || (specialSchemas[this.url.scheme] !== undefined && c_str === "\\")) {
if (c_str === "\\") {
this.parse_error = true;
}
if (this.url.scheme === "file") {
this.state = STATES.FILE_HOST;
} else {
this.state = STATES.SPECIAL_AUTHORITY_IGNORE_SLASHES;
}
} else {
if (this.url.scheme !== "file") {
this.url.username = this.base.username;
this.url.password = this.base.password;
this.url.host = this.base.host;
this.url.port = this.base.port;
}
this.state = STATES.PATH;
--this.pointer;
}
};
URLStateMachine.prototype["parse" + STATES.SPECIAL_AUTHORITY_SLASHES] =
function parseSpecialAuthoritySlashes(c, c_str) {
if (c_str === "/" && at(this.input, this.pointer + 1) === "/") {
this.state = STATES.SPECIAL_AUTHORITY_IGNORE_SLASHES;
++this.pointer;
} else {
this.parse_error = true;
this.state = STATES.SPECIAL_AUTHORITY_IGNORE_SLASHES;
--this.pointer;
}
};
URLStateMachine.prototype["parse" + STATES.SPECIAL_AUTHORITY_IGNORE_SLASHES] =
function parseSpecialAuthorityIgnoreSlashes(c, c_str) {
if (c_str !== "/" && c_str !== "\\") {
this.state = STATES.AUTHORITY;
--this.pointer;
} else {
this.parse_error = true;
}
};
URLStateMachine.prototype["parse" + STATES.AUTHORITY] =
function parseAuthority(c, c_str) {
if (c_str === "@") {
this.parse_error = true;
if (this.at_flag) {
this.buffer = "%40" + this.buffer;
}
this.at_flag = true;
// careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars
const len = countSymbols(this.buffer);
for (let pointer = 0; pointer < len; ++pointer) {
/* jshint -W004 */
const c = this.buffer.codePointAt(pointer);
const c_str = String.fromCodePoint(c);
/* jshint +W004 */
if (c === 0x9 || c === 0xA || c === 0xD) {
continue;
}
if (c_str === ":" && this.url.password === null) {
this.url.password = "";
continue;
}
if (this.url.password !== null) {
this.url.password += passwordEncode(c);
} else {
this.url.username += usernameEncode(c);
}
}
this.buffer = "";
} else if (isNaN(c) || c_str === "/" || c_str === "?" || c_str === "#" ||
(specialSchemas[this.url.scheme] !== undefined && c_str === "\\")) {
this.pointer -= countSymbols(this.buffer) + 1;
this.buffer = "";
this.state = STATES.HOST;
} else {
this.buffer += c_str;
}
};
URLStateMachine.prototype["parse" + STATES.HOST_NAME] =
URLStateMachine.prototype["parse" + STATES.HOST] =
function parseHostName(c, c_str) {
if (c_str === ":" && !this.arr_flag) {
if (specialSchemas[this.url.scheme] !== undefined && this.buffer === "") {
throw new TypeError("Invalid URL");
}
let host = parseHost(this.buffer);
this.url.host = host;
this.buffer = "";
this.state = STATES.PORT;
if (this.state_override === STATES.HOST_NAME) {
return false;
}
} else if (isNaN(c) || c_str === "/" || c_str === "?" || c_str === "#" ||
(specialSchemas[this.url.scheme] !== undefined && c_str === "\\")) {
--this.pointer;
if (specialSchemas[this.url.scheme] !== undefined && this.buffer === "") {
throw new TypeError("Invalid URL");
}
let host = parseHost(this.buffer);
this.url.host = host;
this.buffer = "";
this.state = STATES.PATH_START;
if (this.state_override) {
return false;
}
} else if (c === 0x9 || c === 0xA || c === 0xD) {
this.parse_error = true;
} else {
if (c_str === "[") {
this.arr_flag = true;
} else if (c_str === "]") {
this.arr_flag = false;
}
this.buffer += c_str;
}
};
URLStateMachine.prototype["parse" + STATES.FILE_HOST] =
function parseFileHost(c, c_str) {
if (isNaN(c) || c_str === "/" || c_str === "\\" || c_str === "?" || c_str === "#") {
--this.pointer;
// don't need to count symbols here since we check ASCII values
if (this.buffer.length === 2 &&
isASCIIAlpha(this.buffer.codePointAt(0)) && (this.buffer[1] === ":" || this.buffer[1] === "|")) {
this.state = STATES.PATH;
} else if (this.buffer === "") {
this.state = STATES.PATH_START;
} else {
let host = parseHost(this.buffer);
this.url.host = host;
this.buffer = "";
this.state = STATES.PATH_START;
}
} else if (c === 0x9 || c === 0xA || c === 0xD) {
this.parse_error = true;
} else {
this.buffer += c_str;
}
};
URLStateMachine.prototype["parse" + STATES.PORT] =
function parsePort(c, c_str) {
if (isASCIIDigit(c)) {
this.buffer += c_str;
} else if (isNaN(c) || c_str === "/" || c_str === "?" || c_str === "#" ||
(specialSchemas[this.url.scheme] !== undefined && c_str === "\\")) {
while (this.buffer[0] === "0" && this.buffer.length > 1) {
this.buffer = this.buffer.substr(1);
}
if (this.buffer === specialSchemas[this.url.scheme]) {
this.buffer = "";
}
this.url.port = this.buffer;
if (this.state_override) {
return false;
}
this.buffer = "";
this.state = STATES.PATH_START;
--this.pointer;
} else if (c === 0x9 || c === 0xA || c === 0xD) {
this.parse_error = true;
} else {
this.parse_error = true;
throw new TypeError("Invalid URL");
}
};
URLStateMachine.prototype["parse" + STATES.PATH_START] =
function parsePathStart(c, c_str) {
if (specialSchemas[this.url.scheme] !== undefined && c_str === "\\") {
this.parse_error = true;
}
this.state = STATES.PATH;
if (c_str !== "/" && !(specialSchemas[this.url.scheme] !== undefined && c_str === "\\")) {
--this.pointer;
}
};
URLStateMachine.prototype["parse" + STATES.PATH] =
function parsePath(c, c_str) {
if (isNaN(c) || c_str === "/" || (specialSchemas[this.url.scheme] !== undefined && c_str === "\\") ||
(!this.state_override && (c_str === "?" || c_str === "#"))) {
if (specialSchemas[this.url.scheme] !== undefined && c_str === "\\") {
this.parse_error = true;
}
this.buffer = bufferReplacement[this.buffer.toLowerCase()] || this.buffer;
if (this.buffer === "..") {
this.url.path.pop();
if (c_str !== "/" && !(specialSchemas[this.url.scheme] !== undefined && c_str === "\\")) {
this.url.path.push("");
}
} else if (this.buffer === "." && c_str !== "/" &&
!(specialSchemas[this.url.scheme] !== undefined && c_str === "\\")) {
this.url.path.push("");
} else if (this.buffer !== ".") {
if (this.url.scheme === "file" && this.url.path.length === 0 &&
this.buffer.length === 2 && isASCIIAlpha(this.buffer.codePointAt(0)) && this.buffer[1] === "|") {
this.buffer = this.buffer[0] + ":";
}
this.url.path.push(this.buffer);
}
this.buffer = "";
if (c_str === "?") {
this.url.query = "";
this.state = STATES.QUERY;
}
if (c_str === "#") {
this.url.fragment = "";
this.state = STATES.FRAGMENT;
}
} else if (c === 0x9 || c === 0xA || c === 0xD) {
this.parse_error = true;
} else {
//TODO:If c is not a URL code point and not "%", parse error.
if (c_str === "%" &&
(!isASCIIHex(at(this.input, this.pointer + 1)) ||
!isASCIIHex(at(this.input, this.pointer + 2)))) {
this.parse_error = true;
}
this.buffer += defaultEncode(c);
}
};
URLStateMachine.prototype["parse" + STATES.NON_RELATIVE_PATH] =
function parseNonRelativePath(c, c_str) {
if (c_str === "?") {
this.url.query = "";
this.state = STATES.QUERY;
} else if (c_str === "#") {
this.url.fragment = "";
this.state = STATES.FRAGMENT;
} else {
// TODO: Add: not a URL code point
if (!isNaN(c) && c_str !== "%") {
this.parse_error = true;
}
if (c_str === "%" &&
(!isASCIIHex(at(this.input, this.pointer + 1)) ||
!isASCIIHex(at(this.input, this.pointer + 2)))) {
this.parse_error = true;
}
if (!isNaN(c) && c !== 0x9 && c !== 0xA && c !== 0xD) {
this.url.path[0] = this.url.path[0] + simpleEncode(c);
}
}
};
URLStateMachine.prototype["parse" + STATES.QUERY] =
function parseQuery(c, c_str) {
if (isNaN(c) || (!this.state_override && c_str === "#")) {
if (specialSchemas[this.url.scheme] === undefined || this.url.scheme === "ws" || this.url.scheme === "wss") {
this.encoding_override = "utf-8";
}
const buffer = new Buffer(this.buffer); //TODO: Use encoding override instead
for (let i = 0; i < buffer.length; ++i) {
if (buffer[i] < 0x21 || buffer[i] > 0x7E || buffer[i] === 0x22 || buffer[i] === 0x23 ||
buffer[i] === 0x3C || buffer[i] === 0x3E) {
this.url.query += percentEncode(buffer[i]);
} else {
this.url.query += String.fromCodePoint(buffer[i]);
}
}
this.buffer = "";
if (c_str === "#") {
this.url.fragment = "";
this.state = STATES.FRAGMENT;
}
} else if (c === 0x9 || c === 0xA || c === 0xD) {
this.parse_error = true;
} else {
//TODO: If c is not a URL code point and not "%", parse error.
if (c_str === "%" &&
(!isASCIIHex(at(this.input, this.pointer + 1)) ||
!isASCIIHex(at(this.input, this.pointer + 2)))) {
this.parse_error = true;
}
this.buffer += c_str;
}
};
URLStateMachine.prototype["parse" + STATES.FRAGMENT] =
function parseFragment(c, c_str) {
if (isNaN(c)) { // do nothing
} else if (c === 0x0 || c === 0x9 || c === 0xA || c === 0xD) {
this.parse_error = true;
} else {
//TODO: If c is not a URL code point and not "%", parse error.
if (c_str === "%" &&
(!isASCIIHex(at(this.input, this.pointer + 1)) ||
!isASCIIHex(at(this.input, this.pointer + 2)))) {
this.parse_error = true;
}
this.url.fragment += c_str;
}
};
function serializeURL(url, excludeFragment) {
let output = url.scheme + ":";
if (url.host !== null) {
output += "//" + url.username;
if (url.password !== null) {
output += ":" + url.password;
}
if (url.username !== "" || url.password !== null) {
output += "@";
}
output += serializeHost(url.host);
if (url.port !== "") {
output += ":" + url.port;
}
}
if (url.scheme === "file" && url.host === null) {
output += "//";
}
if (url.nonRelative) {
output += url.path[0];
} else {
output += "/" + url.path.join("/");
}
if (url.query !== null) {
output += "?" + url.query;
}
if (!excludeFragment && url.fragment !== null) {
output += "#" + url.fragment;
}
return output;
}
function serializeOrigin(tuple) {
if (tuple.scheme === undefined || tuple.host === undefined || tuple.port === undefined) {
return "null";
}
let result = tuple.scheme + "://";
result += tr46.toUnicode(tuple.host, false).domain;
if (specialSchemas[tuple.scheme] && tuple.port !== specialSchemas[tuple.scheme]) {
result += ":" + tuple.port;
}
return result;
}
function mixin(src, target) {
const props = Object.getOwnPropertyNames(src);
const descriptors = {};
for (let i = 0; i < props.length; ++i) {
descriptors[props[i]] = Object.getOwnPropertyDescriptor(src, props[i]);
}
Object.defineProperties(target, descriptors);
const symbols = Object.getOwnPropertySymbols(src);
for (var i = 0; i < symbols.length; ++i) {
target[symbols[i]] = src[symbols[i]];
}
}
const inputSymbol = Symbol("input");
const encodingSymbol = Symbol("queryEncoding");
const querySymbol = Symbol("queryObject");
const urlSymbol = Symbol("url");
const baseSymbol = Symbol("base");
const isURLSymbol = Symbol("isURL");
const updateStepsSymbol = Symbol("updateSteps");
function setTheInput(obj, input, url) {
if (url) {
obj[urlSymbol] = url;
obj[inputSymbol] = input;
} else {
obj[urlSymbol] = null;
if (input === null) {
obj[inputSymbol] = "";
} else {
obj[inputSymbol] = input;
try {
if (typeof obj[baseSymbol] === "function") {
obj[urlSymbol] = new URLStateMachine(input, new URLStateMachine(obj[baseSymbol]()).url);
} else {
obj[urlSymbol] = new URLStateMachine(input, obj[baseSymbol]);
}
} catch (e) {}
}
}
const query = obj[urlSymbol] !== null && obj[urlSymbol].url.query !== null ? obj[urlSymbol].url.query : "";
// TODO: Update URLSearchParams
}
const URLUtils = {
get href() {
if (this[urlSymbol] === null) {
return this[inputSymbol];
}
return serializeURL(this[urlSymbol].url);
},
set href(val) {
let input = String(val);
if (this[isURLSymbol]) {
// SPEC: says to use "get the base" algorithm,
// but the base might've already been provided by the constructor.
// Clarify!
// Can't set base symbol to function in URL constructor, so don't need to check this
const parsedURL = new URLStateMachine(input, this[baseSymbol]);
input = "";
setTheInput(this, "", parsedURL);
} else {
setTheInput(this, input);
preUpdateSteps(this, input);
}
},
get origin() {
if (this[urlSymbol] === null) {
return "";
}
const url = this[urlSymbol].url;
switch (url.scheme) {
case "blob":
try {
return module.exports.createURLConstructor()(url.scheme_data).origin;
} catch (e) {
// serializing an opaque identifier returns "null"
return "null";
}
break;
case "ftp":
case "gopher":
case "http":
case "https":
case "ws":
case "wss":
return serializeOrigin({
scheme: url.scheme,
host: serializeHost(url.host),
port: url.port === "" ? specialSchemas[url.scheme] : url.port
});
case "file":
// spec says "exercise to the reader", chrome says "file://"
return "file://";
default:
// serializing an opaque identifier returns "null"
return "null";
}
},
get protocol() {
if (this[urlSymbol] === null) {
return ":";
}
return this[urlSymbol].url.scheme + ":";
},
set protocol(val) {
if (this[urlSymbol] === null) {
return;
}
this[urlSymbol] = new URLStateMachine(val + ":", null, null, this[urlSymbol].url, STATES.SCHEME_START);
preUpdateSteps(this);
},
get username() {
return this[urlSymbol] === null ? "" : this[urlSymbol].url.username;
},
set username(val) {
if (this[urlSymbol] === null || this[urlSymbol].url.host === null || this[urlSymbol].url.nonRelative) {
return;
}
this[urlSymbol].url.username = "";
const decoded = punycode.ucs2.decode(val);
for (let i = 0; i < decoded.length; ++i) {
this[urlSymbol].url.username += usernameEncode(decoded[i]);
}
preUpdateSteps(this);
},
get password() {
return this[urlSymbol] === null || this[urlSymbol].url.password === null ? "" : this[urlSymbol].url.password;
},
set password(val) {
if (this[urlSymbol] === null || this[urlSymbol].url.host === null || this[urlSymbol].url.nonRelative) {
return;
}
this[urlSymbol].url.password = "";
const decoded = punycode.ucs2.decode(val);
for (let i = 0; i < decoded.length; ++i) {
this[urlSymbol].url.password += passwordEncode(decoded[i]);
}
preUpdateSteps(this);
},
get host() {
if (this[urlSymbol] === null || this[urlSymbol].url.host === null) {
return "";
}
return serializeHost(this[urlSymbol].url.host) +
(this[urlSymbol].url.port === "" ? "" : ":" + this[urlSymbol].url.port);
},
set host(val) {
if (this[urlSymbol] === null || this[urlSymbol].url.nonRelative) {
return;
}
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.HOST);
preUpdateSteps(this);
},
get hostname() {
if (this[urlSymbol] === null || this[urlSymbol].url.host === null) {
return "";
}
return serializeHost(this[urlSymbol].url.host);
},
set hostname(val) {
if (this[urlSymbol] === null || this[urlSymbol].url.nonRelative) {
return;
}
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.HOST_NAME);
preUpdateSteps(this);
},
get port() {
if (this[urlSymbol] === null) {
return "";
}
return this[urlSymbol].url.port;
},
set port(val) {
if (this[urlSymbol] === null || this[urlSymbol].url.nonRelative || this[urlSymbol].url.scheme === "file") {
return;
}
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.PORT);
preUpdateSteps(this);
},
get pathname() {
if (this[urlSymbol] === null) {
return "";
}
if (this[urlSymbol].url.nonRelative) {
return this[urlSymbol].url.path[0];
}
return "/" + this[urlSymbol].url.path.join("/");
},
set pathname(val) {
if (this[urlSymbol] === null || this[urlSymbol].url.nonRelative) {
return;
}
this[urlSymbol].url.path = [];
this[urlSymbol] = new URLStateMachine(val, null, null, this[urlSymbol].url, STATES.PATH_START);
preUpdateSteps(this);
},
get search() {
if (this[urlSymbol] === null || !this[urlSymbol].url.query) {
return "";
}
return "?" + this[urlSymbol].url.query;
},
set search(val) {
if (this[urlSymbol] === null) {
return;
}
if (val === "") {
this[urlSymbol].url.query = null;
// TODO: empty query object
preUpdateSteps(this);
return;
}
const input = val[0] === "?" ? val.substr(1) : val;
this[urlSymbol].url.query = "";
// TODO: Add query encoding
this[urlSymbol] = new URLStateMachine(input, null, null, this[urlSymbol].url, STATES.QUERY);
// TODO: Update query object
// Since the query object isn't implemented, call updateSteps manually for now
preUpdateSteps(this);
},
get hash() {
if (this[urlSymbol] === null || !this[urlSymbol].url.fragment) {
return "";
}
return "#" + this[urlSymbol].url.fragment;
},
set hash(val) {
if (this[urlSymbol] === null || this[urlSymbol].url.scheme === "javascript") {
return;
}
if (val === "") {
this[urlSymbol].url.fragment = null;
preUpdateSteps(this);
return;
}
const input = val[0] === "#" ? val.substr(1) : val;
this[urlSymbol].url.fragment = "";
this[urlSymbol] = new URLStateMachine(input, null, null, this[urlSymbol].url, STATES.FRAGMENT);
preUpdateSteps(this);
},
toString() {
return this.href;
}
};
function urlToASCII(domain) {
try {
const asciiDomain = parseHost(domain);
return asciiDomain;
} catch (e) {
return "";
}
}
function urlToUnicode(domain) {
try {
const unicodeDomain = parseHost(domain, true);
return unicodeDomain;
} catch (e) {
return "";
}
}
function init(url, base) {
/*jshint validthis:true */
if (this === undefined) {
throw new TypeError("Failed to construct 'URL': Please use the 'new' operator, " +
"this DOM object constructor cannot be called as a function.");
}
if (arguments.length === 0) {
throw new TypeError("Failed to construct 'URL': 1 argument required, but only 0 present.");
}
let parsedBase = null;
if (base) {
parsedBase = new URLStateMachine(base);
this[baseSymbol] = parsedBase.url;
}
const parsedURL = new URLStateMachine(url, parsedBase ? parsedBase.url : undefined);
setTheInput(this, "", parsedURL);
}
function preUpdateSteps(obj, value) {
if (value === undefined) {
value = serializeURL(obj[urlSymbol].url);
}
obj[updateStepsSymbol].call(obj, value);
}
module.exports.createURLConstructor = function () {
function URL() {
this[isURLSymbol] = true;
this[updateStepsSymbol] = function () {};
init.apply(this, arguments);
}
mixin(URLUtils, URL.prototype);
URL.toASCII = urlToASCII;
URL.toUnicode = urlToUnicode;
return URL;
};
module.exports.mixinURLUtils = function (obj, base, updateSteps) {
obj[isURLSymbol] = false;
if (typeof base === "function") {
obj[baseSymbol] = base;
} else {
obj[baseSymbol] = new URLStateMachine(base).url;
}
obj[updateStepsSymbol] = updateSteps || function () {};
setTheInput(obj, null, null);
mixin(URLUtils, obj);
};
module.exports.setTheInput = function (obj, input) {
setTheInput(obj, input, null);
};
module.exports.reparse = function (obj) {
setTheInput(obj, obj[inputSymbol]);
};