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 | };
|
---|