mirror of https://github.com/lukechilds/node.git
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.
1254 lines
34 KiB
1254 lines
34 KiB
'use strict';
|
|
|
|
const punycode = require('punycode');
|
|
|
|
exports.parse = urlParse;
|
|
exports.resolve = urlResolve;
|
|
exports.resolveObject = urlResolveObject;
|
|
exports.format = urlFormat;
|
|
|
|
exports.Url = Url;
|
|
|
|
function Url() {
|
|
// For more efficient internal representation and laziness.
|
|
// The non-underscore versions of these properties are accessor functions
|
|
// defined on the prototype.
|
|
this._protocol = null;
|
|
this._port = -1;
|
|
this._query = null;
|
|
this._auth = null;
|
|
this._hostname = null;
|
|
this._host = null;
|
|
this._pathname = null;
|
|
this._hash = null;
|
|
this._search = null;
|
|
this._href = '';
|
|
|
|
this._prependSlash = false;
|
|
this._parsesQueryStrings = false;
|
|
|
|
this.slashes = null;
|
|
}
|
|
|
|
// Reference: RFC 3986, RFC 1808, RFC 2396
|
|
|
|
const _protocolCharacters = makeAsciiTable([
|
|
[0x61, 0x7A] /*a-z*/,
|
|
[0x41, 0x5A] /*A-Z*/,
|
|
0x2E /*'.'*/, 0x2B /*'+'*/, 0x2D /*'-'*/
|
|
]);
|
|
|
|
// RFC 2396: characters reserved for delimiting URLs.
|
|
// We actually just auto-escape these.
|
|
// RFC 2396: characters not allowed for various reasons.
|
|
// Allowed by RFCs, but cause of XSS attacks. Always escape these.
|
|
const _autoEscape = [
|
|
'<', '>', '\'', '`', ' ', '\r', '\n', '\t', '{', '}', '|', '\\', '^', '`', '"'
|
|
];
|
|
|
|
const _autoEscapeMap = new Array(128);
|
|
|
|
for (let i = 0, len = _autoEscapeMap.length; i < len; ++i) {
|
|
_autoEscapeMap[i] = '';
|
|
}
|
|
|
|
for (let i = 0, len = _autoEscape.length; i < len; ++i) {
|
|
let c = _autoEscape[i];
|
|
let esc = encodeURIComponent(c);
|
|
if (esc === c)
|
|
esc = escape(c);
|
|
_autoEscapeMap[c.charCodeAt(0)] = esc;
|
|
}
|
|
|
|
// Same as autoEscapeMap except \ is not escaped but is turned into /.
|
|
const _afterQueryAutoEscapeMap = _autoEscapeMap.slice();
|
|
_autoEscapeMap[0x5C /*'\'*/] = '/';
|
|
|
|
// Protocols that always contain a // bit.
|
|
const _slashProtocols = {
|
|
'http': true,
|
|
'https': true,
|
|
'ftp': true,
|
|
'gopher': true,
|
|
'file': true,
|
|
'http:': true,
|
|
'https:': true,
|
|
'ftp:': true,
|
|
'gopher:': true,
|
|
'file:': true
|
|
};
|
|
|
|
const _autoEscapeCharacters = makeAsciiTable(_autoEscape.map(function(v) {
|
|
return v.charCodeAt(0);
|
|
}));
|
|
|
|
// Characters that are never ever allowed in a hostname.
|
|
// Note that any invalid chars are also handled, but these
|
|
// are the ones that are *expected* to be seen, so we fast-path them.
|
|
const _hostEndingCharacters = makeAsciiTable(
|
|
['#', '?', '/', '\\'].map(function(v) {
|
|
return v.charCodeAt(0);
|
|
}));
|
|
// If these characters end a host name, the path will not be prepended a /.
|
|
const _hostEndingCharactersNoPrependSlash = makeAsciiTable([
|
|
'<', '>', '"', '`', ' ', '\r', '\n', '\t', '{', '}', '|', '^', '`', '\'', '%',
|
|
';'
|
|
].map(function(v) {
|
|
return v.charCodeAt(0);
|
|
}));
|
|
|
|
const querystring = require('querystring');
|
|
|
|
const unserializablePropertyNames = ['_protocol', '_port', '_href', '_query',
|
|
'_prependSlash', '_auth', '_hostname',
|
|
'_pathname', '_hash', '_search',
|
|
'_parsesQueryStrings', '_host'];
|
|
const noSerializePattern = new RegExp('^(?:' +
|
|
unserializablePropertyNames.join('|') +
|
|
')$');
|
|
|
|
function urlParse(url, parseQueryString, slashesDenoteHost) {
|
|
if (url instanceof Url) return url;
|
|
|
|
var u = new Url();
|
|
u.parse(url, parseQueryString, slashesDenoteHost);
|
|
return u;
|
|
}
|
|
|
|
Url.prototype.parse = function(str, parseQueryString, hostDenotesSlash) {
|
|
if (typeof str !== 'string') {
|
|
throw new TypeError(`Parameter 'url' must be a string, not ` +
|
|
typeof str);
|
|
}
|
|
// The field's value must always be an actual boolean.
|
|
this._parsesQueryStrings = !!parseQueryString;
|
|
var start = 0;
|
|
var end = str.length - 1;
|
|
|
|
// Trim leading and trailing ws.
|
|
while (str.charCodeAt(start) <= 0x20 /*' '*/) start++;
|
|
var trimmedStart = start;
|
|
while (str.charCodeAt(end) <= 0x20 /*' '*/) end--;
|
|
|
|
start = this._parseProtocol(str, start, end);
|
|
|
|
// Javascript doesn't have host.
|
|
if (this._protocol !== 'javascript') {
|
|
start = this._parseHost(str, trimmedStart, start, end, hostDenotesSlash);
|
|
var proto = this._protocol;
|
|
if (!this._hostname &&
|
|
(this.slashes || (proto && !_slashProtocols[proto])))
|
|
this._hostname = this._host = '';
|
|
}
|
|
|
|
if (start <= end) {
|
|
var ch = str.charCodeAt(start);
|
|
if (ch === 0x2F /*'/'*/ || ch === 0x5C /*'\'*/)
|
|
this._parsePath(str, start, end);
|
|
else if (ch === 0x3F /*'?'*/)
|
|
this._parseQuery(str, start, end);
|
|
else if (ch === 0x23 /*'#'*/)
|
|
this._parseHash(str, start, end);
|
|
else if (this._protocol !== 'javascript')
|
|
this._parsePath(str, start, end);
|
|
else // For javascript the pathname is just the rest of it.
|
|
this._pathname = str.slice(start, end + 1);
|
|
}
|
|
|
|
if (!this._pathname && this._hostname && _slashProtocols[this._protocol])
|
|
this._pathname = '/';
|
|
|
|
if (parseQueryString) {
|
|
var search = this._search;
|
|
if (search == null)
|
|
search = this._search = '';
|
|
if (search.charCodeAt(0) === 0x3F /*'?'*/)
|
|
search = search.slice(1);
|
|
// This calls a setter function, there is no .query data property.
|
|
this.query = querystring.parse(search);
|
|
}
|
|
};
|
|
|
|
function urlResolve(source, relative) {
|
|
return urlParse(source, false, true).resolve(relative);
|
|
}
|
|
|
|
Url.prototype.resolve = function(relative) {
|
|
return this.resolveObject(urlParse(relative, false, true)).format();
|
|
};
|
|
|
|
// Format a parsed object into a url string.
|
|
function urlFormat(obj) {
|
|
// Ensure it's an object, and not a string url.
|
|
// If it's an obj, this is a no-op.
|
|
// this way, you can call url_format() on strings
|
|
// to clean up potentially wonky urls.
|
|
if (typeof obj === 'string') {
|
|
obj = urlParse(obj);
|
|
} else if (typeof obj !== 'object' || obj === null) {
|
|
throw new TypeError('Parameter \'urlObj\' must be an object, not ' +
|
|
obj === null ? 'null' : typeof obj);
|
|
} else if (!(obj instanceof Url)) {
|
|
return Url.prototype.format.call(obj);
|
|
}
|
|
|
|
return obj.format();
|
|
}
|
|
|
|
Url.prototype.format = function() {
|
|
var auth = this.auth || '';
|
|
|
|
if (auth) {
|
|
auth = encodeURIComponent(auth);
|
|
auth = auth.replace(/%3A/i, ':');
|
|
auth += '@';
|
|
}
|
|
|
|
var protocol = this.protocol || '';
|
|
var pathname = this.pathname || '';
|
|
var hash = this.hash || '';
|
|
var search = this.search || '';
|
|
var query = '';
|
|
var hostname = this.hostname || '';
|
|
var port = this.port || '';
|
|
var host = false;
|
|
var scheme = '';
|
|
|
|
// Cache the result of the getter function.
|
|
var q = this.query;
|
|
if (q !== null && typeof q === 'object') {
|
|
query = querystring.stringify(q);
|
|
}
|
|
|
|
if (!search) {
|
|
search = query ? '?' + query : '';
|
|
}
|
|
|
|
if (protocol && protocol.charCodeAt(protocol.length - 1) !== 0x3A /*':'*/)
|
|
protocol += ':';
|
|
|
|
if (this.host) {
|
|
host = auth + this.host;
|
|
} else if (hostname) {
|
|
host = auth + hostname + (port ? ':' + port : '');
|
|
}
|
|
|
|
var slashes = this.slashes ||
|
|
((!protocol || _slashProtocols[protocol]) && host !== false);
|
|
|
|
if (protocol) scheme = protocol + (slashes ? '//' : '');
|
|
else if (slashes) scheme = '//';
|
|
|
|
if (slashes && pathname && pathname.charCodeAt(0) !== 0x2F /*'/'*/) {
|
|
pathname = '/' + pathname;
|
|
}
|
|
if (search && search.charCodeAt(0) !== 0x3F /*'?'*/)
|
|
search = '?' + search;
|
|
if (hash && hash.charCodeAt(0) !== 0x23 /*'#'*/)
|
|
hash = '#' + hash;
|
|
|
|
pathname = escapePathName(pathname);
|
|
search = escapeSearch(search);
|
|
|
|
return scheme + (host === false ? '' : host) + pathname + search + hash;
|
|
};
|
|
|
|
function urlResolveObject(source, relative) {
|
|
if (!source) return relative;
|
|
return urlParse(source, false, true).resolveObject(relative);
|
|
}
|
|
|
|
Url.prototype.resolveObject = function(relative) {
|
|
if (typeof relative === 'string')
|
|
relative = urlParse(relative, false, true);
|
|
|
|
var result = this._clone();
|
|
|
|
// Hash is always overridden, no matter what.
|
|
// even href='' will remove it.
|
|
result._hash = relative._hash;
|
|
|
|
// If the relative url is empty, then there's nothing left to do here.
|
|
if (!relative.href) {
|
|
result._href = '';
|
|
return result;
|
|
}
|
|
|
|
// Hrefs like //foo/bar always cut to the protocol.
|
|
if (relative.slashes && !relative._protocol) {
|
|
relative._copyPropsTo(result, true);
|
|
|
|
if (_slashProtocols[result._protocol] &&
|
|
result._hostname && !result._pathname) {
|
|
result._pathname = '/';
|
|
}
|
|
result._href = '';
|
|
return result;
|
|
}
|
|
|
|
if (relative._protocol && relative._protocol !== result._protocol) {
|
|
// If it's a known url protocol, then changing
|
|
// the protocol does weird things
|
|
// first, if it's not file:, then we MUST have a host,
|
|
// and if there was a path
|
|
// to begin with, then we MUST have a path.
|
|
// if it is file:, then the host is dropped,
|
|
// because that's known to be hostless.
|
|
// anything else is assumed to be absolute.
|
|
if (!_slashProtocols[relative._protocol]) {
|
|
relative._copyPropsTo(result, false);
|
|
result._href = '';
|
|
return result;
|
|
}
|
|
|
|
result._protocol = relative._protocol;
|
|
if (!relative._host &&
|
|
!/^file:?$/.test(relative._protocol) &&
|
|
relative._protocol !== 'javascript') {
|
|
var relPath = (relative._pathname || '').split('/');
|
|
while (relPath.length && !(relative._host = relPath.shift()));
|
|
if (!relative._host) relative._host = '';
|
|
if (!relative._hostname) relative._hostname = '';
|
|
if (relPath[0] !== '') relPath.unshift('');
|
|
if (relPath.length < 2) relPath.unshift('');
|
|
result._pathname = relPath.join('/');
|
|
} else {
|
|
result._pathname = relative._pathname;
|
|
}
|
|
|
|
result._search = relative._search;
|
|
result._host = relative._host || '';
|
|
result._auth = relative._auth;
|
|
result._hostname = relative._hostname || relative._host;
|
|
result._port = relative._port;
|
|
result.slashes = result.slashes || relative.slashes;
|
|
result._href = '';
|
|
return result;
|
|
}
|
|
|
|
var isSourceAbs =
|
|
(result._pathname && result._pathname.charCodeAt(0) === 0x2F /*'/'*/);
|
|
var isRelAbs = (
|
|
relative._host ||
|
|
(relative._pathname &&
|
|
relative._pathname.charCodeAt(0) === 0x2F /*'/'*/));
|
|
var mustEndAbs = (isRelAbs ||
|
|
isSourceAbs ||
|
|
(result._host && relative._pathname));
|
|
|
|
var removeAllDots = mustEndAbs;
|
|
|
|
var srcPath = result._pathname && result._pathname.split('/') || [];
|
|
var relPath = relative._pathname && relative._pathname.split('/') || [];
|
|
var psychotic = result._protocol && !_slashProtocols[result._protocol];
|
|
|
|
// If the url is a non-slashed url, then relative
|
|
// links like ../.. should be able
|
|
// to crawl up to the hostname, as well. This is strange.
|
|
// result.protocol has already been set by now.
|
|
// Later on, put the first path part into the host field.
|
|
if (psychotic) {
|
|
result._hostname = '';
|
|
result._port = -1;
|
|
if (result._host) {
|
|
if (srcPath[0] === '') srcPath[0] = result._host;
|
|
else srcPath.unshift(result._host);
|
|
}
|
|
result._host = '';
|
|
if (relative._protocol) {
|
|
relative._hostname = '';
|
|
relative._port = -1;
|
|
if (relative._host) {
|
|
if (relPath[0] === '') relPath[0] = relative._host;
|
|
else relPath.unshift(relative._host);
|
|
}
|
|
relative._host = '';
|
|
}
|
|
mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
|
|
}
|
|
|
|
if (isRelAbs) {
|
|
// it's absolute.
|
|
result._host = relative._host ? relative._host : result._host;
|
|
result._hostname =
|
|
relative._hostname ? relative._hostname : result._hostname;
|
|
result._search = relative._search;
|
|
srcPath = relPath;
|
|
// Fall through to the dot-handling below.
|
|
} else if (relPath.length) {
|
|
// It's relative
|
|
// throw away the existing file, and take the new path instead.
|
|
if (!srcPath) srcPath = [];
|
|
srcPath.pop();
|
|
srcPath = srcPath.concat(relPath);
|
|
result._search = relative._search;
|
|
} else if (relative._search) {
|
|
// Just pull out the search.
|
|
// like href='?foo'.
|
|
// Put this after the other two cases because it simplifies the booleans
|
|
if (psychotic) {
|
|
result._hostname = result._host = srcPath.shift();
|
|
// Occasionally the auth can get stuck only in host
|
|
// this especialy happens in cases like
|
|
// url.resolveObject('mailto:local1@domain1', 'local2@domain2').
|
|
var authInHost = result._host && result._host.indexOf('@') > 0 ?
|
|
result._host.split('@') : false;
|
|
if (authInHost) {
|
|
result._auth = authInHost.shift();
|
|
result._host = result._hostname = authInHost.shift();
|
|
}
|
|
}
|
|
result._search = relative._search;
|
|
result._href = '';
|
|
return result;
|
|
}
|
|
|
|
if (!srcPath.length) {
|
|
// No path at all. easy.
|
|
// we've already handled the other stuff above.
|
|
result._pathname = null;
|
|
result._href = '';
|
|
return result;
|
|
}
|
|
|
|
// If a url ENDs in . or .., then it must get a trailing slash.
|
|
// however, if it ends in anything else non-slashy,
|
|
// then it must NOT get a trailing slash.
|
|
var last = srcPath.slice(-1)[0];
|
|
var hasTrailingSlash = (
|
|
(result._host || relative._host || srcPath.length > 1) &&
|
|
(last === '.' || last === '..') || last === '');
|
|
|
|
// Strip single dots, resolve double dots to parent dir
|
|
// if the path tries to go above the root, `up` ends up > 0.
|
|
var up = 0;
|
|
for (var i = srcPath.length; i >= 0; i--) {
|
|
last = srcPath[i];
|
|
if (last === '.') {
|
|
srcPath.splice(i, 1);
|
|
} else if (last === '..') {
|
|
srcPath.splice(i, 1);
|
|
up++;
|
|
} else if (up) {
|
|
srcPath.splice(i, 1);
|
|
up--;
|
|
}
|
|
}
|
|
|
|
// If the path is allowed to go above the root, restore leading ..s.
|
|
if (!mustEndAbs && !removeAllDots) {
|
|
for (; up--; up) {
|
|
srcPath.unshift('..');
|
|
}
|
|
}
|
|
|
|
if (mustEndAbs && srcPath[0] !== '' &&
|
|
(!srcPath[0] || srcPath[0].charCodeAt(0) !== 0x2F /*'/'*/)) {
|
|
srcPath.unshift('');
|
|
}
|
|
|
|
if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
|
|
srcPath.push('');
|
|
}
|
|
|
|
var isAbsolute = srcPath[0] === '' ||
|
|
(srcPath[0] && srcPath[0].charCodeAt(0) === 0x2F /*'/'*/);
|
|
|
|
// put the host back
|
|
if (psychotic) {
|
|
result._hostname = result._host = isAbsolute ? '' :
|
|
srcPath.length ? srcPath.shift() : '';
|
|
// Occasionally the auth can get stuck only in host.
|
|
// This especialy happens in cases like
|
|
// url.resolveObject('mailto:local1@domain1', 'local2@domain2').
|
|
var authInHost = result._host && result._host.indexOf('@') > 0 ?
|
|
result._host.split('@') : false;
|
|
if (authInHost) {
|
|
result._auth = authInHost.shift();
|
|
result._host = result._hostname = authInHost.shift();
|
|
}
|
|
}
|
|
|
|
mustEndAbs = mustEndAbs || (result._host && srcPath.length);
|
|
|
|
if (mustEndAbs && !isAbsolute) {
|
|
srcPath.unshift('');
|
|
}
|
|
|
|
result._pathname = srcPath.length === 0 ? null : srcPath.join('/');
|
|
result._auth = relative._auth || result._auth;
|
|
result.slashes = result.slashes || relative.slashes;
|
|
result._href = '';
|
|
return result;
|
|
};
|
|
|
|
|
|
Url.prototype._parseProtocol = function(str, start, end) {
|
|
var needsLowerCasing = false;
|
|
var protocolCharacters = _protocolCharacters;
|
|
|
|
for (var i = start; i <= end; ++i) {
|
|
var ch = str.charCodeAt(i);
|
|
|
|
if (ch === 0x3A /*':'*/) {
|
|
if (i - start === 0)
|
|
return start;
|
|
var protocol = str.slice(start, i);
|
|
if (needsLowerCasing) protocol = protocol.toLowerCase();
|
|
this._protocol = protocol;
|
|
return i + 1;
|
|
} else if (protocolCharacters[ch] === 1) {
|
|
if (ch < 0x61 /*'a'*/)
|
|
needsLowerCasing = true;
|
|
} else {
|
|
return start;
|
|
}
|
|
|
|
}
|
|
return start;
|
|
};
|
|
|
|
Url.prototype._parseAuth = function(str, start, end, decode) {
|
|
var auth = str.slice(start, end + 1);
|
|
if (decode) {
|
|
auth = decodeURIComponent(auth);
|
|
}
|
|
this._auth = auth;
|
|
};
|
|
|
|
Url.prototype._parsePort = function(str, start, end) {
|
|
// Distinguish between :0 and : (no port number at all).
|
|
var hadChars = false;
|
|
var hadValidPortTerminator = true;
|
|
var checkLeadingZeroes = false;
|
|
|
|
for (var i = start; i <= end; ++i) {
|
|
var ch = str.charCodeAt(i);
|
|
|
|
if (0x30 /*'0'*/ <= ch && ch <= 0x39 /*'9'*/) {
|
|
if (i === start && ch === 0x30 /*'0'*/)
|
|
checkLeadingZeroes = true;
|
|
hadChars = true;
|
|
} else {
|
|
hadValidPortTerminator = false;
|
|
if (_hostEndingCharacters[ch] === 1 ||
|
|
_hostEndingCharactersNoPrependSlash[ch] === 1) {
|
|
hadValidPortTerminator = true;
|
|
} else {
|
|
this._port = -2;
|
|
}
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
if (!hadChars || !hadValidPortTerminator)
|
|
return 0;
|
|
|
|
var portEnd = i;
|
|
var port = str.slice(start, portEnd);
|
|
|
|
if (checkLeadingZeroes) {
|
|
var hadNonZero = false;
|
|
for (var i = 0; i < port.length; ++i) {
|
|
if (port.charCodeAt(i) !== 0x30 /*'0'*/) {
|
|
hadNonZero = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (hadNonZero)
|
|
port = -1;
|
|
else
|
|
port = '0';
|
|
}
|
|
this._port = port;
|
|
return portEnd - start;
|
|
};
|
|
|
|
Url.prototype._hasValidPort = function() {
|
|
return typeof this._port === 'string';
|
|
};
|
|
|
|
Url.prototype._parseHost = function(str,
|
|
trimmedStart,
|
|
start,
|
|
end,
|
|
slashesDenoteHost) {
|
|
var hostEndingCharacters = _hostEndingCharacters;
|
|
var first = str.charCodeAt(start);
|
|
var second = str.charCodeAt(start + 1);
|
|
if ((first === 0x2F /*'/'*/ || first === 0x5C /*'\'*/) &&
|
|
(second === 0x2F /*'/'*/ || second === 0x5C /*'\'*/)) {
|
|
this.slashes = true;
|
|
|
|
// The string starts with // or \\.
|
|
if (start === trimmedStart) {
|
|
// The string is just '//' or '\\'.
|
|
if (end - start === 1) return start;
|
|
// If slashes do not denote host and there is no auth,
|
|
// there is no host when the string starts with // or \\.
|
|
var hasAuth =
|
|
containsCharacter(str,
|
|
0x40 /*'@'*/,
|
|
trimmedStart + 2,
|
|
hostEndingCharacters);
|
|
if (!hasAuth && !slashesDenoteHost) {
|
|
this.slashes = null;
|
|
return start;
|
|
}
|
|
}
|
|
// There is a host that starts after the //.
|
|
start += 2;
|
|
}
|
|
// If there is no slashes, there is no hostname if
|
|
// 1. there was no protocol at all.
|
|
else if (!this._protocol ||
|
|
// 2. there was a protocol that requires slashes
|
|
// e.g. in 'http:asd' 'asd' is not a hostname.
|
|
_slashProtocols[this._protocol]
|
|
) {
|
|
return start;
|
|
}
|
|
|
|
var needsLowerCasing = false;
|
|
var idna = false;
|
|
var hostNameStart = start;
|
|
var hostNameEnd = end;
|
|
var lastCh = -1;
|
|
var portLength = 0;
|
|
var charsAfterDot = 0;
|
|
var authNeedsDecoding = false;
|
|
|
|
var j = -1;
|
|
|
|
// Find the last occurrence of an @-sign until hostending character is met
|
|
// also mark if decoding is needed for the auth portion.
|
|
for (var i = start; i <= end; ++i) {
|
|
var ch = str.charCodeAt(i);
|
|
if (ch === 0x40 /*'@'*/)
|
|
j = i;
|
|
else if (ch === 0x25 /*'%'*/)
|
|
authNeedsDecoding = true;
|
|
else if (hostEndingCharacters[ch] === 1)
|
|
break;
|
|
}
|
|
|
|
// @-sign was found at index j, everything to the left from it
|
|
// is auth part.
|
|
if (j > -1) {
|
|
this._parseAuth(str, start, j - 1, authNeedsDecoding);
|
|
// Hostname starts after the last @-sign-
|
|
start = hostNameStart = j + 1;
|
|
}
|
|
|
|
// Host name is starting with a [.
|
|
if (str.charCodeAt(start) === 0x5B /*'['*/) {
|
|
for (var i = start + 1; i <= end; ++i) {
|
|
var ch = str.charCodeAt(i);
|
|
|
|
// Assume valid IP6 is between the brackets.
|
|
if (ch === 0x5D /*']'*/) {
|
|
if (str.charCodeAt(i + 1) === 0x3A /*':'*/)
|
|
portLength = this._parsePort(str, i + 2, end) + 1;
|
|
|
|
var hostname = '[' + str.slice(start + 1, i).toLowerCase() + ']';
|
|
this._hostname = hostname;
|
|
this._host =
|
|
this._hasValidPort() ? hostname + ':' + this._port : hostname;
|
|
this._pathname = '/';
|
|
return i + portLength + 1;
|
|
}
|
|
}
|
|
// Empty hostname, [ starts a path.
|
|
return start;
|
|
}
|
|
var hostEndingCharactersNoPrependSlash = _hostEndingCharactersNoPrependSlash;
|
|
for (var i = start; i <= end; ++i) {
|
|
if (charsAfterDot > 62) {
|
|
this._hostname = this._host = str.slice(start, i);
|
|
return i;
|
|
}
|
|
var ch = str.charCodeAt(i);
|
|
|
|
if (ch === 0x3A /*':'*/) {
|
|
portLength = this._parsePort(str, i + 1, end) + 1;
|
|
hostNameEnd = i - 1;
|
|
break;
|
|
} else if (ch < 0x61 /*'a'*/) {
|
|
if (ch === 0x2E /*'.'*/) {
|
|
// TODO(petkaantonov) This error is originally ignored:
|
|
// if (lastCh === 0x2E /*'.'*/ || lastCh === -1) {
|
|
// this.hostname = this.host = '';
|
|
// return start;
|
|
// }
|
|
charsAfterDot = -1;
|
|
} else if (0x41 /*'A'*/ <= ch && ch <= 0x5A /*'Z'*/) {
|
|
needsLowerCasing = true;
|
|
}
|
|
// Valid characters other than ASCII letters -, _, +, 0-9.
|
|
else if (!(ch === 0x2D /*'-'*/ ||
|
|
ch === 0x5F /*'_'*/ ||
|
|
ch === 0x2B /*'+'*/ ||
|
|
(0x30 /*'0'*/ <= ch && ch <= 0x39 /*'9'*/))) {
|
|
if (hostEndingCharacters[ch] === 0 &&
|
|
hostEndingCharactersNoPrependSlash[ch] === 0)
|
|
this._prependSlash = true;
|
|
hostNameEnd = i - 1;
|
|
break;
|
|
}
|
|
} else if (ch >= 0x7B /*'{'*/) {
|
|
if (ch <= 0x7E /*'~'*/) {
|
|
if (hostEndingCharactersNoPrependSlash[ch] === 0) {
|
|
this._prependSlash = true;
|
|
}
|
|
hostNameEnd = i - 1;
|
|
break;
|
|
}
|
|
idna = true;
|
|
needsLowerCasing = true;
|
|
}
|
|
lastCh = ch;
|
|
charsAfterDot++;
|
|
}
|
|
|
|
// TODO(petkaantonov) This error is originally ignored:
|
|
// if (lastCh === 0x2E /*'.'*/)
|
|
// hostNameEnd--
|
|
|
|
if (hostNameEnd + 1 !== start &&
|
|
hostNameEnd - hostNameStart <= 256) {
|
|
var hostname = str.slice(hostNameStart, hostNameEnd + 1);
|
|
if (needsLowerCasing)
|
|
hostname = hostname.toLowerCase();
|
|
if (idna)
|
|
hostname = punycode.toASCII(hostname);
|
|
|
|
this._hostname = hostname;
|
|
this._host =
|
|
this._hasValidPort() ? hostname + ':' + this._port : hostname;
|
|
}
|
|
|
|
return hostNameEnd + 1 + portLength;
|
|
|
|
};
|
|
|
|
Url.prototype._copyPropsTo = function(target, noProtocol) {
|
|
if (!noProtocol) {
|
|
target._protocol = this._protocol;
|
|
}
|
|
// Forces getter recalculation.
|
|
target._href = null;
|
|
target._host = this._host;
|
|
target._hostname = this._hostname;
|
|
target._pathname = this._pathname;
|
|
target._search = this._search;
|
|
target._parsesQueryStrings = this._parsesQueryStrings;
|
|
target._prependSlash = this._prependSlash;
|
|
target._port = this._port;
|
|
target._auth = this._auth;
|
|
target.slashes = this.slashes;
|
|
target._query = this._query;
|
|
target._hash = this._hash;
|
|
};
|
|
|
|
Url.prototype._clone = function() {
|
|
var ret = new Url();
|
|
ret._protocol = this._protocol;
|
|
ret._href = this._href;
|
|
ret._port = this._port;
|
|
ret._prependSlash = this._prependSlash;
|
|
ret._auth = this._auth;
|
|
ret._hostname = this._hostname;
|
|
ret._host = this._host;
|
|
ret._hash = this._hash;
|
|
ret._search = this._search;
|
|
ret._pathname = this._pathname;
|
|
ret._parsesQueryStrings = this._parsesQueryStrings;
|
|
ret._query = this._query;
|
|
ret.slashes = this.slashes;
|
|
return ret;
|
|
};
|
|
|
|
Url.prototype.toJSON = function() {
|
|
var ret = {
|
|
href: this.href,
|
|
pathname: this.pathname,
|
|
path: this.path,
|
|
protocol: this.protocol,
|
|
query: this.query,
|
|
port: this.port,
|
|
auth: this.auth,
|
|
hash: this.hash,
|
|
host: this.host,
|
|
hostname: this.hostname,
|
|
search: this.search,
|
|
slashes: this.slashes
|
|
};
|
|
var keys = Object.keys(this);
|
|
for (var i = 0; i < keys.length; ++i) {
|
|
var key = keys[i];
|
|
if (noSerializePattern.test(key)) continue;
|
|
ret[key] = this[key];
|
|
|
|
}
|
|
return ret;
|
|
};
|
|
|
|
Url.prototype._parsePath = function(str, start, end) {
|
|
var pathStart = start;
|
|
var pathEnd = end;
|
|
var escape = false;
|
|
var autoEscapeCharacters = _autoEscapeCharacters;
|
|
var prePath = this._port === -2 ? '/:' : '';
|
|
|
|
for (var i = start; i <= end; ++i) {
|
|
var ch = str.charCodeAt(i);
|
|
|
|
if (ch === 0x23 /*'#'*/) {
|
|
this._parseHash(str, i, end);
|
|
pathEnd = i - 1;
|
|
break;
|
|
} else if (ch === 0x3F /*'?'*/) {
|
|
this._parseQuery(str, i, end);
|
|
pathEnd = i - 1;
|
|
break;
|
|
} else if (!escape && autoEscapeCharacters[ch] === 1) {
|
|
escape = true;
|
|
}
|
|
}
|
|
|
|
if (pathStart > pathEnd) {
|
|
this._pathname = prePath === '' ? '/' : prePath;
|
|
return;
|
|
}
|
|
|
|
var path;
|
|
if (escape) {
|
|
path = getComponentEscaped(str, pathStart, pathEnd, false);
|
|
} else {
|
|
path = str.slice(pathStart, pathEnd + 1);
|
|
}
|
|
|
|
this._pathname = prePath === '' ?
|
|
(this._prependSlash ? '/' + path : path) : prePath + path;
|
|
};
|
|
|
|
Url.prototype._parseQuery = function(str, start, end) {
|
|
var queryStart = start;
|
|
var queryEnd = end;
|
|
var escape = false;
|
|
var autoEscapeCharacters = _autoEscapeCharacters;
|
|
|
|
for (var i = start; i <= end; ++i) {
|
|
var ch = str.charCodeAt(i);
|
|
|
|
if (ch === 0x23 /*'#'*/) {
|
|
this._parseHash(str, i, end);
|
|
queryEnd = i - 1;
|
|
break;
|
|
} else if (!escape && autoEscapeCharacters[ch] === 1)
|
|
escape = true;
|
|
}
|
|
|
|
if (queryStart > queryEnd) {
|
|
this._search = '';
|
|
return;
|
|
}
|
|
|
|
var query = escape ?
|
|
getComponentEscaped(str, queryStart, queryEnd, true) :
|
|
str.slice(queryStart, queryEnd + 1);
|
|
|
|
this._search = query;
|
|
};
|
|
|
|
Url.prototype._parseHash = function(str, start, end) {
|
|
if (start > end) {
|
|
this._hash = '';
|
|
return;
|
|
}
|
|
this._hash = getComponentEscaped(str, start, end, true);
|
|
};
|
|
|
|
Object.defineProperty(Url.prototype, 'port', {
|
|
get: function() {
|
|
if (this._hasValidPort())
|
|
return this._port;
|
|
|
|
return null;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._port = -1;
|
|
if (this._host)
|
|
this._host = null;
|
|
} else {
|
|
this._port = toPortNumber(v);
|
|
|
|
if (this._hostname)
|
|
this._host = this._hostname + ':' + this._port;
|
|
else
|
|
this._host = ':' + this._port;
|
|
}
|
|
this._href = '';
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'query', {
|
|
get: function() {
|
|
var query = this._query;
|
|
if (query != null)
|
|
return query;
|
|
|
|
var search = this._search;
|
|
|
|
if (search) {
|
|
if (search.charCodeAt(0) === 0x3F /*'?'*/)
|
|
search = search.slice(1);
|
|
if (search !== '') {
|
|
this._query = search;
|
|
return search;
|
|
}
|
|
}
|
|
return search;
|
|
},
|
|
set: function(v) {
|
|
if (typeof v === 'string') {
|
|
if (v !== '') this.search = '?' + v;
|
|
this._query = v;
|
|
} else if (v !== null && typeof v === 'object') {
|
|
var string = querystring.stringify(v);
|
|
this._search = string !== '' ? '?' + querystring.stringify(v) : '';
|
|
this._query = v;
|
|
} else {
|
|
this._query = this._search = null;
|
|
}
|
|
this._href = '';
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'path', {
|
|
get: function() {
|
|
var p = this.pathname || '';
|
|
var s = this.search || '';
|
|
if (p || s)
|
|
return p + s;
|
|
return (p == null && s) ? ('/' + s) : null;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._pathname = this._search = null;
|
|
return;
|
|
}
|
|
var path = '' + v;
|
|
var matches = path.match(/([^\?]*)(\?.*)$/);
|
|
|
|
if (matches) {
|
|
this.pathname = matches[1];
|
|
this.search = matches[2];
|
|
} else {
|
|
this.pathname = path;
|
|
this.search = null;
|
|
}
|
|
this._href = '';
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'protocol', {
|
|
get: function() {
|
|
var proto = this._protocol;
|
|
|
|
if (typeof proto === 'string' && proto.length > 0) {
|
|
if (proto.charCodeAt(proto.length - 1) !== 0x3A/*':'*/)
|
|
return proto + ':';
|
|
return proto;
|
|
}
|
|
return proto;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._protocol = null;
|
|
} else {
|
|
var proto = '' + v;
|
|
if (proto.length > 0) {
|
|
if (proto.charCodeAt(proto.length - 1) !== 0x3A /*':'*/)
|
|
proto = proto + ':';
|
|
this._parseProtocol(proto, 0, proto.length - 1);
|
|
this._href = '';
|
|
}
|
|
}
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'href', {
|
|
get: function() {
|
|
var href = this._href;
|
|
if (!href)
|
|
href = this._href = this.format();
|
|
return href;
|
|
},
|
|
set: function(v) {
|
|
this._href = '';
|
|
var parsesQueryStrings = this._parsesQueryStrings;
|
|
// Reset properties.
|
|
Url.call(this);
|
|
if (v !== null && v !== '')
|
|
this.parse('' + v, parsesQueryStrings, false);
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'auth', {
|
|
get: function() {
|
|
return this._auth;
|
|
},
|
|
set: function(v) {
|
|
this._auth = v === null ? null : '' + v;
|
|
this._href = '';
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
// host = hostname + port.
|
|
Object.defineProperty(Url.prototype, 'host', {
|
|
get: function() {
|
|
return this._host;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._port = -1;
|
|
this._hostname = this._host = null;
|
|
} else {
|
|
var host = '' + v;
|
|
var matches = host.match(/(.*):(.+)$/);
|
|
|
|
if (matches) {
|
|
this._hostname = encodeURIComponent(matches[1]);
|
|
this._port = toPortNumber(matches[2]);
|
|
this._host = this._hostname + ':' + this._port;
|
|
} else {
|
|
this._port = -1;
|
|
this._hostname = this._host = encodeURIComponent(host);
|
|
}
|
|
this._href = '';
|
|
}
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'hostname', {
|
|
get: function() {
|
|
return this._hostname;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._hostname = null;
|
|
|
|
if (this._hasValidPort())
|
|
this._host = ':' + this._port;
|
|
else
|
|
this._host = null;
|
|
} else {
|
|
var hostname = encodeURIComponent('' + v);
|
|
this._hostname = hostname;
|
|
|
|
if (this._hasValidPort())
|
|
this._host = hostname + ':' + this._port;
|
|
else
|
|
this._host = hostname;
|
|
|
|
this._href = '';
|
|
}
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'hash', {
|
|
get: function() {
|
|
return this._hash;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._hash = null;
|
|
} else {
|
|
var hash = '' + v;
|
|
if (hash.charCodeAt(0) !== 0x23 /*'#'*/) {
|
|
hash = '#' + hash;
|
|
}
|
|
this._hash = hash;
|
|
this._href = '';
|
|
}
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'search', {
|
|
get: function() {
|
|
return this._search;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._search = this._query = null;
|
|
} else {
|
|
var search = escapeSearch('' + v);
|
|
|
|
if (search.charCodeAt(0) !== 0x3F /*'?'*/)
|
|
search = '?' + search;
|
|
|
|
this._search = search;
|
|
|
|
if (this._parsesQueryStrings) {
|
|
this.query = querystring.parse(search.slice(1));
|
|
}
|
|
this._href = '';
|
|
}
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
Object.defineProperty(Url.prototype, 'pathname', {
|
|
get: function() {
|
|
return this._pathname;
|
|
},
|
|
set: function(v) {
|
|
if (v === null) {
|
|
this._pathname = null;
|
|
} else {
|
|
var pathname = escapePathName('' + v).replace(/\\/g, '/');
|
|
|
|
if (this.hostname &&
|
|
_slashProtocols[this._protocol] &&
|
|
pathname.charCodeAt(0) !== 0x2F /*'/'*/) {
|
|
pathname = '/' + pathname;
|
|
}
|
|
|
|
this._pathname = pathname;
|
|
this._href = '';
|
|
}
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
|
|
// Search `char1` (integer code for a character) in `string`
|
|
// starting from `fromIndex` and ending at `string.length - 1`
|
|
// or when a stop character is found.
|
|
function containsCharacter(string, char1, fromIndex, stopCharacterTable) {
|
|
var len = string.length;
|
|
for (var i = fromIndex; i < len; ++i) {
|
|
var ch = string.charCodeAt(i);
|
|
|
|
if (ch === char1)
|
|
return true;
|
|
else if (stopCharacterTable[ch] === 1)
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// See if `char1` or `char2` (integer codes for characters)
|
|
// is contained in `string`.
|
|
function containsCharacter2(string, char1, char2) {
|
|
for (var i = 0, len = string.length; i < len; ++i) {
|
|
var ch = string.charCodeAt(i);
|
|
if (ch === char1 || ch === char2)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Makes an array of 128 uint8's which represent boolean values.
|
|
// Spec is an array of ascii code points or ascii code point ranges
|
|
// ranges are expressed as [start, end].
|
|
|
|
// For example, to create a table with the characters
|
|
// 0x30-0x39 (decimals '0' - '9') and
|
|
// 0x7A (lowercaseletter 'z') as `true`:
|
|
|
|
// var a = makeAsciiTable([[0x30, 0x39], 0x7A]);
|
|
// a[0x30]; //1
|
|
// a[0x15]; //0
|
|
// a[0x35]; //1
|
|
function makeAsciiTable(spec) {
|
|
var ret = new Uint8Array(128);
|
|
spec.forEach(function(item) {
|
|
if (typeof item === 'number') {
|
|
ret[item] = 1;
|
|
} else {
|
|
var start = item[0];
|
|
var end = item[1];
|
|
for (var j = start; j <= end; ++j) {
|
|
ret[j] = 1;
|
|
}
|
|
}
|
|
});
|
|
|
|
return ret;
|
|
}
|
|
|
|
function escapePathName(pathname) {
|
|
if (!containsCharacter2(pathname, 0x23 /*'#'*/, 0x3F /*'?'*/))
|
|
return pathname;
|
|
|
|
return pathname.replace(/[?#]/g, function(match) {
|
|
return encodeURIComponent(match);
|
|
});
|
|
}
|
|
|
|
function escapeSearch(search) {
|
|
if (!containsCharacter2(search, 0x23 /*'#'*/, -1))
|
|
return search;
|
|
|
|
return search.replace(/#/g, function(match) {
|
|
return encodeURIComponent(match);
|
|
});
|
|
}
|
|
|
|
function getComponentEscaped(str, start, end, isAfterQuery) {
|
|
var cur = start;
|
|
var i = start;
|
|
var ret = '';
|
|
var autoEscapeMap = isAfterQuery ? _afterQueryAutoEscapeMap : _autoEscapeMap;
|
|
for (; i <= end; ++i) {
|
|
var ch = str.charCodeAt(i);
|
|
var escaped = autoEscapeMap[ch];
|
|
|
|
if (escaped !== '' && escaped !== undefined) {
|
|
if (cur < i) ret += str.slice(cur, i);
|
|
ret += escaped;
|
|
cur = i + 1;
|
|
}
|
|
}
|
|
if (cur < i + 1) ret += str.slice(cur, i);
|
|
return ret;
|
|
}
|
|
|
|
function toPortNumber(v) {
|
|
v = parseInt(v);
|
|
if (!Number.isFinite(v))
|
|
v = 0;
|
|
|
|
v = Math.max(0, v) % 0x10000;
|
|
return '' + v;
|
|
}
|
|
|
|
// Optimize back from normalized object caused by non-identifier keys.
|
|
function FakeConstructor() {}
|
|
FakeConstructor.prototype = _slashProtocols;
|
|
/*jshint nonew: false */
|
|
new FakeConstructor();
|
|
|