[6a3a178] | 1 | // Copyright Joyent, Inc. and other Node contributors.
|
---|
| 2 | //
|
---|
| 3 | // Permission is hereby granted, free of charge, to any person obtaining a
|
---|
| 4 | // copy of this software and associated documentation files (the
|
---|
| 5 | // "Software"), to deal in the Software without restriction, including
|
---|
| 6 | // without limitation the rights to use, copy, modify, merge, publish,
|
---|
| 7 | // distribute, sublicense, and/or sell copies of the Software, and to permit
|
---|
| 8 | // persons to whom the Software is furnished to do so, subject to the
|
---|
| 9 | // following conditions:
|
---|
| 10 | //
|
---|
| 11 | // The above copyright notice and this permission notice shall be included
|
---|
| 12 | // in all copies or substantial portions of the Software.
|
---|
| 13 | //
|
---|
| 14 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
---|
| 15 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
---|
| 16 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
---|
| 17 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
---|
| 18 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
---|
| 19 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
---|
| 20 | // USE OR OTHER DEALINGS IN THE SOFTWARE.
|
---|
| 21 |
|
---|
| 22 | 'use strict';
|
---|
| 23 |
|
---|
| 24 | var punycode = require('punycode');
|
---|
| 25 | var util = require('./util');
|
---|
| 26 |
|
---|
| 27 | exports.parse = urlParse;
|
---|
| 28 | exports.resolve = urlResolve;
|
---|
| 29 | exports.resolveObject = urlResolveObject;
|
---|
| 30 | exports.format = urlFormat;
|
---|
| 31 |
|
---|
| 32 | exports.Url = Url;
|
---|
| 33 |
|
---|
| 34 | function Url() {
|
---|
| 35 | this.protocol = null;
|
---|
| 36 | this.slashes = null;
|
---|
| 37 | this.auth = null;
|
---|
| 38 | this.host = null;
|
---|
| 39 | this.port = null;
|
---|
| 40 | this.hostname = null;
|
---|
| 41 | this.hash = null;
|
---|
| 42 | this.search = null;
|
---|
| 43 | this.query = null;
|
---|
| 44 | this.pathname = null;
|
---|
| 45 | this.path = null;
|
---|
| 46 | this.href = null;
|
---|
| 47 | }
|
---|
| 48 |
|
---|
| 49 | // Reference: RFC 3986, RFC 1808, RFC 2396
|
---|
| 50 |
|
---|
| 51 | // define these here so at least they only have to be
|
---|
| 52 | // compiled once on the first module load.
|
---|
| 53 | var protocolPattern = /^([a-z0-9.+-]+:)/i,
|
---|
| 54 | portPattern = /:[0-9]*$/,
|
---|
| 55 |
|
---|
| 56 | // Special case for a simple path URL
|
---|
| 57 | simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,
|
---|
| 58 |
|
---|
| 59 | // RFC 2396: characters reserved for delimiting URLs.
|
---|
| 60 | // We actually just auto-escape these.
|
---|
| 61 | delims = ['<', '>', '"', '`', ' ', '\r', '\n', '\t'],
|
---|
| 62 |
|
---|
| 63 | // RFC 2396: characters not allowed for various reasons.
|
---|
| 64 | unwise = ['{', '}', '|', '\\', '^', '`'].concat(delims),
|
---|
| 65 |
|
---|
| 66 | // Allowed by RFCs, but cause of XSS attacks. Always escape these.
|
---|
| 67 | autoEscape = ['\''].concat(unwise),
|
---|
| 68 | // Characters that are never ever allowed in a hostname.
|
---|
| 69 | // Note that any invalid chars are also handled, but these
|
---|
| 70 | // are the ones that are *expected* to be seen, so we fast-path
|
---|
| 71 | // them.
|
---|
| 72 | nonHostChars = ['%', '/', '?', ';', '#'].concat(autoEscape),
|
---|
| 73 | hostEndingChars = ['/', '?', '#'],
|
---|
| 74 | hostnameMaxLen = 255,
|
---|
| 75 | hostnamePartPattern = /^[+a-z0-9A-Z_-]{0,63}$/,
|
---|
| 76 | hostnamePartStart = /^([+a-z0-9A-Z_-]{0,63})(.*)$/,
|
---|
| 77 | // protocols that can allow "unsafe" and "unwise" chars.
|
---|
| 78 | unsafeProtocol = {
|
---|
| 79 | 'javascript': true,
|
---|
| 80 | 'javascript:': true
|
---|
| 81 | },
|
---|
| 82 | // protocols that never have a hostname.
|
---|
| 83 | hostlessProtocol = {
|
---|
| 84 | 'javascript': true,
|
---|
| 85 | 'javascript:': true
|
---|
| 86 | },
|
---|
| 87 | // protocols that always contain a // bit.
|
---|
| 88 | slashedProtocol = {
|
---|
| 89 | 'http': true,
|
---|
| 90 | 'https': true,
|
---|
| 91 | 'ftp': true,
|
---|
| 92 | 'gopher': true,
|
---|
| 93 | 'file': true,
|
---|
| 94 | 'http:': true,
|
---|
| 95 | 'https:': true,
|
---|
| 96 | 'ftp:': true,
|
---|
| 97 | 'gopher:': true,
|
---|
| 98 | 'file:': true
|
---|
| 99 | },
|
---|
| 100 | querystring = require('querystring');
|
---|
| 101 |
|
---|
| 102 | function urlParse(url, parseQueryString, slashesDenoteHost) {
|
---|
| 103 | if (url && util.isObject(url) && url instanceof Url) return url;
|
---|
| 104 |
|
---|
| 105 | var u = new Url;
|
---|
| 106 | u.parse(url, parseQueryString, slashesDenoteHost);
|
---|
| 107 | return u;
|
---|
| 108 | }
|
---|
| 109 |
|
---|
| 110 | Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) {
|
---|
| 111 | if (!util.isString(url)) {
|
---|
| 112 | throw new TypeError("Parameter 'url' must be a string, not " + typeof url);
|
---|
| 113 | }
|
---|
| 114 |
|
---|
| 115 | // Copy chrome, IE, opera backslash-handling behavior.
|
---|
| 116 | // Back slashes before the query string get converted to forward slashes
|
---|
| 117 | // See: https://code.google.com/p/chromium/issues/detail?id=25916
|
---|
| 118 | var queryIndex = url.indexOf('?'),
|
---|
| 119 | splitter =
|
---|
| 120 | (queryIndex !== -1 && queryIndex < url.indexOf('#')) ? '?' : '#',
|
---|
| 121 | uSplit = url.split(splitter),
|
---|
| 122 | slashRegex = /\\/g;
|
---|
| 123 | uSplit[0] = uSplit[0].replace(slashRegex, '/');
|
---|
| 124 | url = uSplit.join(splitter);
|
---|
| 125 |
|
---|
| 126 | var rest = url;
|
---|
| 127 |
|
---|
| 128 | // trim before proceeding.
|
---|
| 129 | // This is to support parse stuff like " http://foo.com \n"
|
---|
| 130 | rest = rest.trim();
|
---|
| 131 |
|
---|
| 132 | if (!slashesDenoteHost && url.split('#').length === 1) {
|
---|
| 133 | // Try fast path regexp
|
---|
| 134 | var simplePath = simplePathPattern.exec(rest);
|
---|
| 135 | if (simplePath) {
|
---|
| 136 | this.path = rest;
|
---|
| 137 | this.href = rest;
|
---|
| 138 | this.pathname = simplePath[1];
|
---|
| 139 | if (simplePath[2]) {
|
---|
| 140 | this.search = simplePath[2];
|
---|
| 141 | if (parseQueryString) {
|
---|
| 142 | this.query = querystring.parse(this.search.substr(1));
|
---|
| 143 | } else {
|
---|
| 144 | this.query = this.search.substr(1);
|
---|
| 145 | }
|
---|
| 146 | } else if (parseQueryString) {
|
---|
| 147 | this.search = '';
|
---|
| 148 | this.query = {};
|
---|
| 149 | }
|
---|
| 150 | return this;
|
---|
| 151 | }
|
---|
| 152 | }
|
---|
| 153 |
|
---|
| 154 | var proto = protocolPattern.exec(rest);
|
---|
| 155 | if (proto) {
|
---|
| 156 | proto = proto[0];
|
---|
| 157 | var lowerProto = proto.toLowerCase();
|
---|
| 158 | this.protocol = lowerProto;
|
---|
| 159 | rest = rest.substr(proto.length);
|
---|
| 160 | }
|
---|
| 161 |
|
---|
| 162 | // figure out if it's got a host
|
---|
| 163 | // user@server is *always* interpreted as a hostname, and url
|
---|
| 164 | // resolution will treat //foo/bar as host=foo,path=bar because that's
|
---|
| 165 | // how the browser resolves relative URLs.
|
---|
| 166 | if (slashesDenoteHost || proto || rest.match(/^\/\/[^@\/]+@[^@\/]+/)) {
|
---|
| 167 | var slashes = rest.substr(0, 2) === '//';
|
---|
| 168 | if (slashes && !(proto && hostlessProtocol[proto])) {
|
---|
| 169 | rest = rest.substr(2);
|
---|
| 170 | this.slashes = true;
|
---|
| 171 | }
|
---|
| 172 | }
|
---|
| 173 |
|
---|
| 174 | if (!hostlessProtocol[proto] &&
|
---|
| 175 | (slashes || (proto && !slashedProtocol[proto]))) {
|
---|
| 176 |
|
---|
| 177 | // there's a hostname.
|
---|
| 178 | // the first instance of /, ?, ;, or # ends the host.
|
---|
| 179 | //
|
---|
| 180 | // If there is an @ in the hostname, then non-host chars *are* allowed
|
---|
| 181 | // to the left of the last @ sign, unless some host-ending character
|
---|
| 182 | // comes *before* the @-sign.
|
---|
| 183 | // URLs are obnoxious.
|
---|
| 184 | //
|
---|
| 185 | // ex:
|
---|
| 186 | // http://a@b@c/ => user:a@b host:c
|
---|
| 187 | // http://a@b?@c => user:a host:c path:/?@c
|
---|
| 188 |
|
---|
| 189 | // v0.12 TODO(isaacs): This is not quite how Chrome does things.
|
---|
| 190 | // Review our test case against browsers more comprehensively.
|
---|
| 191 |
|
---|
| 192 | // find the first instance of any hostEndingChars
|
---|
| 193 | var hostEnd = -1;
|
---|
| 194 | for (var i = 0; i < hostEndingChars.length; i++) {
|
---|
| 195 | var hec = rest.indexOf(hostEndingChars[i]);
|
---|
| 196 | if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
|
---|
| 197 | hostEnd = hec;
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | // at this point, either we have an explicit point where the
|
---|
| 201 | // auth portion cannot go past, or the last @ char is the decider.
|
---|
| 202 | var auth, atSign;
|
---|
| 203 | if (hostEnd === -1) {
|
---|
| 204 | // atSign can be anywhere.
|
---|
| 205 | atSign = rest.lastIndexOf('@');
|
---|
| 206 | } else {
|
---|
| 207 | // atSign must be in auth portion.
|
---|
| 208 | // http://a@b/c@d => host:b auth:a path:/c@d
|
---|
| 209 | atSign = rest.lastIndexOf('@', hostEnd);
|
---|
| 210 | }
|
---|
| 211 |
|
---|
| 212 | // Now we have a portion which is definitely the auth.
|
---|
| 213 | // Pull that off.
|
---|
| 214 | if (atSign !== -1) {
|
---|
| 215 | auth = rest.slice(0, atSign);
|
---|
| 216 | rest = rest.slice(atSign + 1);
|
---|
| 217 | this.auth = decodeURIComponent(auth);
|
---|
| 218 | }
|
---|
| 219 |
|
---|
| 220 | // the host is the remaining to the left of the first non-host char
|
---|
| 221 | hostEnd = -1;
|
---|
| 222 | for (var i = 0; i < nonHostChars.length; i++) {
|
---|
| 223 | var hec = rest.indexOf(nonHostChars[i]);
|
---|
| 224 | if (hec !== -1 && (hostEnd === -1 || hec < hostEnd))
|
---|
| 225 | hostEnd = hec;
|
---|
| 226 | }
|
---|
| 227 | // if we still have not hit it, then the entire thing is a host.
|
---|
| 228 | if (hostEnd === -1)
|
---|
| 229 | hostEnd = rest.length;
|
---|
| 230 |
|
---|
| 231 | this.host = rest.slice(0, hostEnd);
|
---|
| 232 | rest = rest.slice(hostEnd);
|
---|
| 233 |
|
---|
| 234 | // pull out port.
|
---|
| 235 | this.parseHost();
|
---|
| 236 |
|
---|
| 237 | // we've indicated that there is a hostname,
|
---|
| 238 | // so even if it's empty, it has to be present.
|
---|
| 239 | this.hostname = this.hostname || '';
|
---|
| 240 |
|
---|
| 241 | // if hostname begins with [ and ends with ]
|
---|
| 242 | // assume that it's an IPv6 address.
|
---|
| 243 | var ipv6Hostname = this.hostname[0] === '[' &&
|
---|
| 244 | this.hostname[this.hostname.length - 1] === ']';
|
---|
| 245 |
|
---|
| 246 | // validate a little.
|
---|
| 247 | if (!ipv6Hostname) {
|
---|
| 248 | var hostparts = this.hostname.split(/\./);
|
---|
| 249 | for (var i = 0, l = hostparts.length; i < l; i++) {
|
---|
| 250 | var part = hostparts[i];
|
---|
| 251 | if (!part) continue;
|
---|
| 252 | if (!part.match(hostnamePartPattern)) {
|
---|
| 253 | var newpart = '';
|
---|
| 254 | for (var j = 0, k = part.length; j < k; j++) {
|
---|
| 255 | if (part.charCodeAt(j) > 127) {
|
---|
| 256 | // we replace non-ASCII char with a temporary placeholder
|
---|
| 257 | // we need this to make sure size of hostname is not
|
---|
| 258 | // broken by replacing non-ASCII by nothing
|
---|
| 259 | newpart += 'x';
|
---|
| 260 | } else {
|
---|
| 261 | newpart += part[j];
|
---|
| 262 | }
|
---|
| 263 | }
|
---|
| 264 | // we test again with ASCII char only
|
---|
| 265 | if (!newpart.match(hostnamePartPattern)) {
|
---|
| 266 | var validParts = hostparts.slice(0, i);
|
---|
| 267 | var notHost = hostparts.slice(i + 1);
|
---|
| 268 | var bit = part.match(hostnamePartStart);
|
---|
| 269 | if (bit) {
|
---|
| 270 | validParts.push(bit[1]);
|
---|
| 271 | notHost.unshift(bit[2]);
|
---|
| 272 | }
|
---|
| 273 | if (notHost.length) {
|
---|
| 274 | rest = '/' + notHost.join('.') + rest;
|
---|
| 275 | }
|
---|
| 276 | this.hostname = validParts.join('.');
|
---|
| 277 | break;
|
---|
| 278 | }
|
---|
| 279 | }
|
---|
| 280 | }
|
---|
| 281 | }
|
---|
| 282 |
|
---|
| 283 | if (this.hostname.length > hostnameMaxLen) {
|
---|
| 284 | this.hostname = '';
|
---|
| 285 | } else {
|
---|
| 286 | // hostnames are always lower case.
|
---|
| 287 | this.hostname = this.hostname.toLowerCase();
|
---|
| 288 | }
|
---|
| 289 |
|
---|
| 290 | if (!ipv6Hostname) {
|
---|
| 291 | // IDNA Support: Returns a punycoded representation of "domain".
|
---|
| 292 | // It only converts parts of the domain name that
|
---|
| 293 | // have non-ASCII characters, i.e. it doesn't matter if
|
---|
| 294 | // you call it with a domain that already is ASCII-only.
|
---|
| 295 | this.hostname = punycode.toASCII(this.hostname);
|
---|
| 296 | }
|
---|
| 297 |
|
---|
| 298 | var p = this.port ? ':' + this.port : '';
|
---|
| 299 | var h = this.hostname || '';
|
---|
| 300 | this.host = h + p;
|
---|
| 301 | this.href += this.host;
|
---|
| 302 |
|
---|
| 303 | // strip [ and ] from the hostname
|
---|
| 304 | // the host field still retains them, though
|
---|
| 305 | if (ipv6Hostname) {
|
---|
| 306 | this.hostname = this.hostname.substr(1, this.hostname.length - 2);
|
---|
| 307 | if (rest[0] !== '/') {
|
---|
| 308 | rest = '/' + rest;
|
---|
| 309 | }
|
---|
| 310 | }
|
---|
| 311 | }
|
---|
| 312 |
|
---|
| 313 | // now rest is set to the post-host stuff.
|
---|
| 314 | // chop off any delim chars.
|
---|
| 315 | if (!unsafeProtocol[lowerProto]) {
|
---|
| 316 |
|
---|
| 317 | // First, make 100% sure that any "autoEscape" chars get
|
---|
| 318 | // escaped, even if encodeURIComponent doesn't think they
|
---|
| 319 | // need to be.
|
---|
| 320 | for (var i = 0, l = autoEscape.length; i < l; i++) {
|
---|
| 321 | var ae = autoEscape[i];
|
---|
| 322 | if (rest.indexOf(ae) === -1)
|
---|
| 323 | continue;
|
---|
| 324 | var esc = encodeURIComponent(ae);
|
---|
| 325 | if (esc === ae) {
|
---|
| 326 | esc = escape(ae);
|
---|
| 327 | }
|
---|
| 328 | rest = rest.split(ae).join(esc);
|
---|
| 329 | }
|
---|
| 330 | }
|
---|
| 331 |
|
---|
| 332 |
|
---|
| 333 | // chop off from the tail first.
|
---|
| 334 | var hash = rest.indexOf('#');
|
---|
| 335 | if (hash !== -1) {
|
---|
| 336 | // got a fragment string.
|
---|
| 337 | this.hash = rest.substr(hash);
|
---|
| 338 | rest = rest.slice(0, hash);
|
---|
| 339 | }
|
---|
| 340 | var qm = rest.indexOf('?');
|
---|
| 341 | if (qm !== -1) {
|
---|
| 342 | this.search = rest.substr(qm);
|
---|
| 343 | this.query = rest.substr(qm + 1);
|
---|
| 344 | if (parseQueryString) {
|
---|
| 345 | this.query = querystring.parse(this.query);
|
---|
| 346 | }
|
---|
| 347 | rest = rest.slice(0, qm);
|
---|
| 348 | } else if (parseQueryString) {
|
---|
| 349 | // no query string, but parseQueryString still requested
|
---|
| 350 | this.search = '';
|
---|
| 351 | this.query = {};
|
---|
| 352 | }
|
---|
| 353 | if (rest) this.pathname = rest;
|
---|
| 354 | if (slashedProtocol[lowerProto] &&
|
---|
| 355 | this.hostname && !this.pathname) {
|
---|
| 356 | this.pathname = '/';
|
---|
| 357 | }
|
---|
| 358 |
|
---|
| 359 | //to support http.request
|
---|
| 360 | if (this.pathname || this.search) {
|
---|
| 361 | var p = this.pathname || '';
|
---|
| 362 | var s = this.search || '';
|
---|
| 363 | this.path = p + s;
|
---|
| 364 | }
|
---|
| 365 |
|
---|
| 366 | // finally, reconstruct the href based on what has been validated.
|
---|
| 367 | this.href = this.format();
|
---|
| 368 | return this;
|
---|
| 369 | };
|
---|
| 370 |
|
---|
| 371 | // format a parsed object into a url string
|
---|
| 372 | function urlFormat(obj) {
|
---|
| 373 | // ensure it's an object, and not a string url.
|
---|
| 374 | // If it's an obj, this is a no-op.
|
---|
| 375 | // this way, you can call url_format() on strings
|
---|
| 376 | // to clean up potentially wonky urls.
|
---|
| 377 | if (util.isString(obj)) obj = urlParse(obj);
|
---|
| 378 | if (!(obj instanceof Url)) return Url.prototype.format.call(obj);
|
---|
| 379 | return obj.format();
|
---|
| 380 | }
|
---|
| 381 |
|
---|
| 382 | Url.prototype.format = function() {
|
---|
| 383 | var auth = this.auth || '';
|
---|
| 384 | if (auth) {
|
---|
| 385 | auth = encodeURIComponent(auth);
|
---|
| 386 | auth = auth.replace(/%3A/i, ':');
|
---|
| 387 | auth += '@';
|
---|
| 388 | }
|
---|
| 389 |
|
---|
| 390 | var protocol = this.protocol || '',
|
---|
| 391 | pathname = this.pathname || '',
|
---|
| 392 | hash = this.hash || '',
|
---|
| 393 | host = false,
|
---|
| 394 | query = '';
|
---|
| 395 |
|
---|
| 396 | if (this.host) {
|
---|
| 397 | host = auth + this.host;
|
---|
| 398 | } else if (this.hostname) {
|
---|
| 399 | host = auth + (this.hostname.indexOf(':') === -1 ?
|
---|
| 400 | this.hostname :
|
---|
| 401 | '[' + this.hostname + ']');
|
---|
| 402 | if (this.port) {
|
---|
| 403 | host += ':' + this.port;
|
---|
| 404 | }
|
---|
| 405 | }
|
---|
| 406 |
|
---|
| 407 | if (this.query &&
|
---|
| 408 | util.isObject(this.query) &&
|
---|
| 409 | Object.keys(this.query).length) {
|
---|
| 410 | query = querystring.stringify(this.query);
|
---|
| 411 | }
|
---|
| 412 |
|
---|
| 413 | var search = this.search || (query && ('?' + query)) || '';
|
---|
| 414 |
|
---|
| 415 | if (protocol && protocol.substr(-1) !== ':') protocol += ':';
|
---|
| 416 |
|
---|
| 417 | // only the slashedProtocols get the //. Not mailto:, xmpp:, etc.
|
---|
| 418 | // unless they had them to begin with.
|
---|
| 419 | if (this.slashes ||
|
---|
| 420 | (!protocol || slashedProtocol[protocol]) && host !== false) {
|
---|
| 421 | host = '//' + (host || '');
|
---|
| 422 | if (pathname && pathname.charAt(0) !== '/') pathname = '/' + pathname;
|
---|
| 423 | } else if (!host) {
|
---|
| 424 | host = '';
|
---|
| 425 | }
|
---|
| 426 |
|
---|
| 427 | if (hash && hash.charAt(0) !== '#') hash = '#' + hash;
|
---|
| 428 | if (search && search.charAt(0) !== '?') search = '?' + search;
|
---|
| 429 |
|
---|
| 430 | pathname = pathname.replace(/[?#]/g, function(match) {
|
---|
| 431 | return encodeURIComponent(match);
|
---|
| 432 | });
|
---|
| 433 | search = search.replace('#', '%23');
|
---|
| 434 |
|
---|
| 435 | return protocol + host + pathname + search + hash;
|
---|
| 436 | };
|
---|
| 437 |
|
---|
| 438 | function urlResolve(source, relative) {
|
---|
| 439 | return urlParse(source, false, true).resolve(relative);
|
---|
| 440 | }
|
---|
| 441 |
|
---|
| 442 | Url.prototype.resolve = function(relative) {
|
---|
| 443 | return this.resolveObject(urlParse(relative, false, true)).format();
|
---|
| 444 | };
|
---|
| 445 |
|
---|
| 446 | function urlResolveObject(source, relative) {
|
---|
| 447 | if (!source) return relative;
|
---|
| 448 | return urlParse(source, false, true).resolveObject(relative);
|
---|
| 449 | }
|
---|
| 450 |
|
---|
| 451 | Url.prototype.resolveObject = function(relative) {
|
---|
| 452 | if (util.isString(relative)) {
|
---|
| 453 | var rel = new Url();
|
---|
| 454 | rel.parse(relative, false, true);
|
---|
| 455 | relative = rel;
|
---|
| 456 | }
|
---|
| 457 |
|
---|
| 458 | var result = new Url();
|
---|
| 459 | var tkeys = Object.keys(this);
|
---|
| 460 | for (var tk = 0; tk < tkeys.length; tk++) {
|
---|
| 461 | var tkey = tkeys[tk];
|
---|
| 462 | result[tkey] = this[tkey];
|
---|
| 463 | }
|
---|
| 464 |
|
---|
| 465 | // hash is always overridden, no matter what.
|
---|
| 466 | // even href="" will remove it.
|
---|
| 467 | result.hash = relative.hash;
|
---|
| 468 |
|
---|
| 469 | // if the relative url is empty, then there's nothing left to do here.
|
---|
| 470 | if (relative.href === '') {
|
---|
| 471 | result.href = result.format();
|
---|
| 472 | return result;
|
---|
| 473 | }
|
---|
| 474 |
|
---|
| 475 | // hrefs like //foo/bar always cut to the protocol.
|
---|
| 476 | if (relative.slashes && !relative.protocol) {
|
---|
| 477 | // take everything except the protocol from relative
|
---|
| 478 | var rkeys = Object.keys(relative);
|
---|
| 479 | for (var rk = 0; rk < rkeys.length; rk++) {
|
---|
| 480 | var rkey = rkeys[rk];
|
---|
| 481 | if (rkey !== 'protocol')
|
---|
| 482 | result[rkey] = relative[rkey];
|
---|
| 483 | }
|
---|
| 484 |
|
---|
| 485 | //urlParse appends trailing / to urls like http://www.example.com
|
---|
| 486 | if (slashedProtocol[result.protocol] &&
|
---|
| 487 | result.hostname && !result.pathname) {
|
---|
| 488 | result.path = result.pathname = '/';
|
---|
| 489 | }
|
---|
| 490 |
|
---|
| 491 | result.href = result.format();
|
---|
| 492 | return result;
|
---|
| 493 | }
|
---|
| 494 |
|
---|
| 495 | if (relative.protocol && relative.protocol !== result.protocol) {
|
---|
| 496 | // if it's a known url protocol, then changing
|
---|
| 497 | // the protocol does weird things
|
---|
| 498 | // first, if it's not file:, then we MUST have a host,
|
---|
| 499 | // and if there was a path
|
---|
| 500 | // to begin with, then we MUST have a path.
|
---|
| 501 | // if it is file:, then the host is dropped,
|
---|
| 502 | // because that's known to be hostless.
|
---|
| 503 | // anything else is assumed to be absolute.
|
---|
| 504 | if (!slashedProtocol[relative.protocol]) {
|
---|
| 505 | var keys = Object.keys(relative);
|
---|
| 506 | for (var v = 0; v < keys.length; v++) {
|
---|
| 507 | var k = keys[v];
|
---|
| 508 | result[k] = relative[k];
|
---|
| 509 | }
|
---|
| 510 | result.href = result.format();
|
---|
| 511 | return result;
|
---|
| 512 | }
|
---|
| 513 |
|
---|
| 514 | result.protocol = relative.protocol;
|
---|
| 515 | if (!relative.host && !hostlessProtocol[relative.protocol]) {
|
---|
| 516 | var relPath = (relative.pathname || '').split('/');
|
---|
| 517 | while (relPath.length && !(relative.host = relPath.shift()));
|
---|
| 518 | if (!relative.host) relative.host = '';
|
---|
| 519 | if (!relative.hostname) relative.hostname = '';
|
---|
| 520 | if (relPath[0] !== '') relPath.unshift('');
|
---|
| 521 | if (relPath.length < 2) relPath.unshift('');
|
---|
| 522 | result.pathname = relPath.join('/');
|
---|
| 523 | } else {
|
---|
| 524 | result.pathname = relative.pathname;
|
---|
| 525 | }
|
---|
| 526 | result.search = relative.search;
|
---|
| 527 | result.query = relative.query;
|
---|
| 528 | result.host = relative.host || '';
|
---|
| 529 | result.auth = relative.auth;
|
---|
| 530 | result.hostname = relative.hostname || relative.host;
|
---|
| 531 | result.port = relative.port;
|
---|
| 532 | // to support http.request
|
---|
| 533 | if (result.pathname || result.search) {
|
---|
| 534 | var p = result.pathname || '';
|
---|
| 535 | var s = result.search || '';
|
---|
| 536 | result.path = p + s;
|
---|
| 537 | }
|
---|
| 538 | result.slashes = result.slashes || relative.slashes;
|
---|
| 539 | result.href = result.format();
|
---|
| 540 | return result;
|
---|
| 541 | }
|
---|
| 542 |
|
---|
| 543 | var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'),
|
---|
| 544 | isRelAbs = (
|
---|
| 545 | relative.host ||
|
---|
| 546 | relative.pathname && relative.pathname.charAt(0) === '/'
|
---|
| 547 | ),
|
---|
| 548 | mustEndAbs = (isRelAbs || isSourceAbs ||
|
---|
| 549 | (result.host && relative.pathname)),
|
---|
| 550 | removeAllDots = mustEndAbs,
|
---|
| 551 | srcPath = result.pathname && result.pathname.split('/') || [],
|
---|
| 552 | relPath = relative.pathname && relative.pathname.split('/') || [],
|
---|
| 553 | psychotic = result.protocol && !slashedProtocol[result.protocol];
|
---|
| 554 |
|
---|
| 555 | // if the url is a non-slashed url, then relative
|
---|
| 556 | // links like ../.. should be able
|
---|
| 557 | // to crawl up to the hostname, as well. This is strange.
|
---|
| 558 | // result.protocol has already been set by now.
|
---|
| 559 | // Later on, put the first path part into the host field.
|
---|
| 560 | if (psychotic) {
|
---|
| 561 | result.hostname = '';
|
---|
| 562 | result.port = null;
|
---|
| 563 | if (result.host) {
|
---|
| 564 | if (srcPath[0] === '') srcPath[0] = result.host;
|
---|
| 565 | else srcPath.unshift(result.host);
|
---|
| 566 | }
|
---|
| 567 | result.host = '';
|
---|
| 568 | if (relative.protocol) {
|
---|
| 569 | relative.hostname = null;
|
---|
| 570 | relative.port = null;
|
---|
| 571 | if (relative.host) {
|
---|
| 572 | if (relPath[0] === '') relPath[0] = relative.host;
|
---|
| 573 | else relPath.unshift(relative.host);
|
---|
| 574 | }
|
---|
| 575 | relative.host = null;
|
---|
| 576 | }
|
---|
| 577 | mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === '');
|
---|
| 578 | }
|
---|
| 579 |
|
---|
| 580 | if (isRelAbs) {
|
---|
| 581 | // it's absolute.
|
---|
| 582 | result.host = (relative.host || relative.host === '') ?
|
---|
| 583 | relative.host : result.host;
|
---|
| 584 | result.hostname = (relative.hostname || relative.hostname === '') ?
|
---|
| 585 | relative.hostname : result.hostname;
|
---|
| 586 | result.search = relative.search;
|
---|
| 587 | result.query = relative.query;
|
---|
| 588 | srcPath = relPath;
|
---|
| 589 | // fall through to the dot-handling below.
|
---|
| 590 | } else if (relPath.length) {
|
---|
| 591 | // it's relative
|
---|
| 592 | // throw away the existing file, and take the new path instead.
|
---|
| 593 | if (!srcPath) srcPath = [];
|
---|
| 594 | srcPath.pop();
|
---|
| 595 | srcPath = srcPath.concat(relPath);
|
---|
| 596 | result.search = relative.search;
|
---|
| 597 | result.query = relative.query;
|
---|
| 598 | } else if (!util.isNullOrUndefined(relative.search)) {
|
---|
| 599 | // just pull out the search.
|
---|
| 600 | // like href='?foo'.
|
---|
| 601 | // Put this after the other two cases because it simplifies the booleans
|
---|
| 602 | if (psychotic) {
|
---|
| 603 | result.hostname = result.host = srcPath.shift();
|
---|
| 604 | //occationaly the auth can get stuck only in host
|
---|
| 605 | //this especially happens in cases like
|
---|
| 606 | //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
|
---|
| 607 | var authInHost = result.host && result.host.indexOf('@') > 0 ?
|
---|
| 608 | result.host.split('@') : false;
|
---|
| 609 | if (authInHost) {
|
---|
| 610 | result.auth = authInHost.shift();
|
---|
| 611 | result.host = result.hostname = authInHost.shift();
|
---|
| 612 | }
|
---|
| 613 | }
|
---|
| 614 | result.search = relative.search;
|
---|
| 615 | result.query = relative.query;
|
---|
| 616 | //to support http.request
|
---|
| 617 | if (!util.isNull(result.pathname) || !util.isNull(result.search)) {
|
---|
| 618 | result.path = (result.pathname ? result.pathname : '') +
|
---|
| 619 | (result.search ? result.search : '');
|
---|
| 620 | }
|
---|
| 621 | result.href = result.format();
|
---|
| 622 | return result;
|
---|
| 623 | }
|
---|
| 624 |
|
---|
| 625 | if (!srcPath.length) {
|
---|
| 626 | // no path at all. easy.
|
---|
| 627 | // we've already handled the other stuff above.
|
---|
| 628 | result.pathname = null;
|
---|
| 629 | //to support http.request
|
---|
| 630 | if (result.search) {
|
---|
| 631 | result.path = '/' + result.search;
|
---|
| 632 | } else {
|
---|
| 633 | result.path = null;
|
---|
| 634 | }
|
---|
| 635 | result.href = result.format();
|
---|
| 636 | return result;
|
---|
| 637 | }
|
---|
| 638 |
|
---|
| 639 | // if a url ENDs in . or .., then it must get a trailing slash.
|
---|
| 640 | // however, if it ends in anything else non-slashy,
|
---|
| 641 | // then it must NOT get a trailing slash.
|
---|
| 642 | var last = srcPath.slice(-1)[0];
|
---|
| 643 | var hasTrailingSlash = (
|
---|
| 644 | (result.host || relative.host || srcPath.length > 1) &&
|
---|
| 645 | (last === '.' || last === '..') || last === '');
|
---|
| 646 |
|
---|
| 647 | // strip single dots, resolve double dots to parent dir
|
---|
| 648 | // if the path tries to go above the root, `up` ends up > 0
|
---|
| 649 | var up = 0;
|
---|
| 650 | for (var i = srcPath.length; i >= 0; i--) {
|
---|
| 651 | last = srcPath[i];
|
---|
| 652 | if (last === '.') {
|
---|
| 653 | srcPath.splice(i, 1);
|
---|
| 654 | } else if (last === '..') {
|
---|
| 655 | srcPath.splice(i, 1);
|
---|
| 656 | up++;
|
---|
| 657 | } else if (up) {
|
---|
| 658 | srcPath.splice(i, 1);
|
---|
| 659 | up--;
|
---|
| 660 | }
|
---|
| 661 | }
|
---|
| 662 |
|
---|
| 663 | // if the path is allowed to go above the root, restore leading ..s
|
---|
| 664 | if (!mustEndAbs && !removeAllDots) {
|
---|
| 665 | for (; up--; up) {
|
---|
| 666 | srcPath.unshift('..');
|
---|
| 667 | }
|
---|
| 668 | }
|
---|
| 669 |
|
---|
| 670 | if (mustEndAbs && srcPath[0] !== '' &&
|
---|
| 671 | (!srcPath[0] || srcPath[0].charAt(0) !== '/')) {
|
---|
| 672 | srcPath.unshift('');
|
---|
| 673 | }
|
---|
| 674 |
|
---|
| 675 | if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) {
|
---|
| 676 | srcPath.push('');
|
---|
| 677 | }
|
---|
| 678 |
|
---|
| 679 | var isAbsolute = srcPath[0] === '' ||
|
---|
| 680 | (srcPath[0] && srcPath[0].charAt(0) === '/');
|
---|
| 681 |
|
---|
| 682 | // put the host back
|
---|
| 683 | if (psychotic) {
|
---|
| 684 | result.hostname = result.host = isAbsolute ? '' :
|
---|
| 685 | srcPath.length ? srcPath.shift() : '';
|
---|
| 686 | //occationaly the auth can get stuck only in host
|
---|
| 687 | //this especially happens in cases like
|
---|
| 688 | //url.resolveObject('mailto:local1@domain1', 'local2@domain2')
|
---|
| 689 | var authInHost = result.host && result.host.indexOf('@') > 0 ?
|
---|
| 690 | result.host.split('@') : false;
|
---|
| 691 | if (authInHost) {
|
---|
| 692 | result.auth = authInHost.shift();
|
---|
| 693 | result.host = result.hostname = authInHost.shift();
|
---|
| 694 | }
|
---|
| 695 | }
|
---|
| 696 |
|
---|
| 697 | mustEndAbs = mustEndAbs || (result.host && srcPath.length);
|
---|
| 698 |
|
---|
| 699 | if (mustEndAbs && !isAbsolute) {
|
---|
| 700 | srcPath.unshift('');
|
---|
| 701 | }
|
---|
| 702 |
|
---|
| 703 | if (!srcPath.length) {
|
---|
| 704 | result.pathname = null;
|
---|
| 705 | result.path = null;
|
---|
| 706 | } else {
|
---|
| 707 | result.pathname = srcPath.join('/');
|
---|
| 708 | }
|
---|
| 709 |
|
---|
| 710 | //to support request.http
|
---|
| 711 | if (!util.isNull(result.pathname) || !util.isNull(result.search)) {
|
---|
| 712 | result.path = (result.pathname ? result.pathname : '') +
|
---|
| 713 | (result.search ? result.search : '');
|
---|
| 714 | }
|
---|
| 715 | result.auth = relative.auth || result.auth;
|
---|
| 716 | result.slashes = result.slashes || relative.slashes;
|
---|
| 717 | result.href = result.format();
|
---|
| 718 | return result;
|
---|
| 719 | };
|
---|
| 720 |
|
---|
| 721 | Url.prototype.parseHost = function() {
|
---|
| 722 | var host = this.host;
|
---|
| 723 | var port = portPattern.exec(host);
|
---|
| 724 | if (port) {
|
---|
| 725 | port = port[0];
|
---|
| 726 | if (port !== ':') {
|
---|
| 727 | this.port = port.substr(1);
|
---|
| 728 | }
|
---|
| 729 | host = host.substr(0, host.length - port.length);
|
---|
| 730 | }
|
---|
| 731 | if (host) this.hostname = host;
|
---|
| 732 | };
|
---|