diff --git a/doc/api/url.markdown b/doc/api/url.markdown index e4b2320f468608..2386dd8d640dbd 100644 --- a/doc/api/url.markdown +++ b/doc/api/url.markdown @@ -45,6 +45,11 @@ string will not be in the parsed object. Examples are shown for the URL Example: `'/p/a/t/h'` +* `path`: The path section of the URL, that is treated the same with `pathname` + but able to contain `query` as well. + + Example: `'/p/a/t/h?i=10&j=91'` + * `search`: The 'query string' portion of the URL, including the leading question mark. diff --git a/lib/url.js b/lib/url.js index 00e4ec9d7be207..bf48507e6ab996 100644 --- a/lib/url.js +++ b/lib/url.js @@ -9,19 +9,73 @@ exports.format = urlFormat; exports.Url = Url; -function Url() { - this.protocol = null; - this.slashes = null; - this.auth = null; - this.host = null; - this.port = null; - this.hostname = null; - this.hash = null; - this.search = null; - this.query = null; - this.pathname = null; - this.path = null; - this.href = null; +function Url(obj) { + this.protocol = obj && obj.protocol || null; + this.slashes = obj && obj.slashes || null; + this.auth = obj && obj.auth || null; + this.host = obj && obj.host || null; + this.port = obj && obj.port || null; + this.hostname = obj && obj.hostname || null; + this.hash = obj && obj.hash || null; + this.href = obj && obj.href || null; + this.path = obj && obj.path || null; + this._pathname = obj && obj.pathname || null; + this._search = obj && obj.search || null; + this._query = obj && obj.query || null; + + var self = this; + Object.defineProperty(self, 'pathname', { + get: function() { + return self._pathname; + }, + set: function(val) { + val = encodeURIComponent(val); + self._pathname = val; + self.path = (val || '') + (self._search || ''); + } + }); + + Object.defineProperty(self, 'search', { + get: function() { + return self._search; + }, + set: function(val) { + if (val && val.length && val[0] !== '?') val = '?' + val; + self._search = val; + self.path = (self._pathname) + (val || ''); + } + }); + + Object.defineProperty(self, 'query', { + get: function() { + return self._query; + }, + set: function(val) { + var qstr = typeof val !== 'string' ? querystring.stringify(val) : val; + self._query = val; + self.search = '?' + qstr; + } + }); + + Object.defineProperty(self, 'data', { + get: function() { + return { + protocol: this.protocol, + slashes: this.slashes, + auth: this.auth, + host: this.host, + port: this.port, + hostname: this.hostname, + hash: this.hash, + href: this.href, + path: this.path, + pathname: this._pathname, + search: this._search, + query: this._query, + }; + } + }); + } // Reference: RFC 3986, RFC 1808, RFC 2396 @@ -86,9 +140,8 @@ function urlParse(url, parseQueryString, slashesDenoteHost) { } Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { - if (typeof url !== 'string') { + if (typeof url !== 'string') throw new TypeError("Parameter 'url' must be a string, not " + typeof url); - } // Copy chrome, IE, opera backslash-handling behavior. // Back slashes before the query string get converted to forward slashes @@ -113,17 +166,17 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { if (simplePath) { this.path = rest; this.href = rest; - this.pathname = simplePath[1]; + this._pathname = simplePath[1]; if (simplePath[2]) { - this.search = simplePath[2]; + this._search = simplePath[2]; if (parseQueryString) { - this.query = querystring.parse(this.search.substr(1)); + this._query = querystring.parse(this._search.substr(1)); } else { - this.query = this.search.substr(1); + this._query = this._search.substr(1); } } else if (parseQueryString) { - this.search = ''; - this.query = {}; + this._search = ''; + this._query = {}; } return this; } @@ -317,32 +370,32 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { } var qm = rest.indexOf('?'); if (qm !== -1) { - this.search = rest.substr(qm); - this.query = rest.substr(qm + 1); + this._search = rest.substr(qm); + this._query = rest.substr(qm + 1); if (parseQueryString) { - this.query = querystring.parse(this.query); + this._query = querystring.parse(this._query); } rest = rest.slice(0, qm); } else if (parseQueryString) { // no query string, but parseQueryString still requested - this.search = ''; - this.query = {}; + this._search = ''; + this._query = {}; } - if (rest) this.pathname = rest; + if (rest) this._pathname = rest; if (slashedProtocol[lowerProto] && - this.hostname && !this.pathname) { - this.pathname = '/'; + this.hostname && !this._pathname) { + this._pathname = '/'; } //to support http.request - if (this.pathname || this.search) { - var p = this.pathname || ''; - var s = this.search || ''; + if (this._pathname || this._search) { + var p = this._pathname || ''; + var s = this._search || ''; this.path = p + s; } // finally, reconstruct the href based on what has been validated. - this.href = this.format(); + this.href = this.format(parseQueryString); return this; }; @@ -353,11 +406,11 @@ function urlFormat(obj) { // this way, you can call url_format() on strings // to clean up potentially wonky urls. if (typeof obj === 'string') obj = urlParse(obj); - if (!(obj instanceof Url)) return Url.prototype.format.call(obj); + if (!(obj instanceof Url)) obj = new Url(obj); return obj.format(); } -Url.prototype.format = function() { +Url.prototype.format = function(parseQueryString) { var auth = this.auth || ''; if (auth) { auth = encodeURIComponent(auth); @@ -366,10 +419,29 @@ Url.prototype.format = function() { } var protocol = this.protocol || '', - pathname = this.pathname || '', + pathname = this._pathname || '', hash = this.hash || '', host = false, - query = ''; + query = '', + search = ''; + + if (this.path) { + var qm = this.path.indexOf('?'); + if (qm !== -1) { + query = this.path.slice(qm + 1); + search = '?' + query; + pathname = this.path.slice(0, qm); + } else { + if (parseQueryString) { + this._query = {}; + this._search = ''; + } else { + this._query = null; + this._search = null; + } + pathname = this.path; + } + } if (this.host) { host = auth + this.host; @@ -382,13 +454,14 @@ Url.prototype.format = function() { } } - if (this.query !== null && - typeof this.query === 'object' && - Object.keys(this.query).length) { - query = querystring.stringify(this.query); + if (this._query !== null && + typeof this._query === 'object' && + Object.keys(this._query).length) { + query = querystring.stringify(this._query); } - var search = this.search || (query && ('?' + query)) || ''; + if (!search) + search = this._search || (query && ('?' + query)) || ''; if (protocol && protocol.substr(-1) !== ':') protocol += ':'; @@ -462,8 +535,8 @@ Url.prototype.resolveObject = function(relative) { //urlParse appends trailing / to urls like http://www.example.com if (slashedProtocol[result.protocol] && - result.hostname && !result.pathname) { - result.path = result.pathname = '/'; + result.hostname && !result._pathname) { + result.path = result._pathname = '/'; } result.href = result.format(); @@ -491,26 +564,26 @@ Url.prototype.resolveObject = function(relative) { result.protocol = relative.protocol; if (!relative.host && !hostlessProtocol[relative.protocol]) { - var relPath = (relative.pathname || '').split('/'); + 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('/'); + result._pathname = relPath.join('/'); } else { - result.pathname = relative.pathname; + result._pathname = relative._pathname; } - result.search = relative.search; - result.query = relative.query; + result._search = relative._search; + result._query = relative._query; result.host = relative.host || ''; result.auth = relative.auth; result.hostname = relative.hostname || relative.host; result.port = relative.port; // to support http.request - if (result.pathname || result.search) { - var p = result.pathname || ''; - var s = result.search || ''; + if (result._pathname || result._search) { + var p = result._pathname || ''; + var s = result._search || ''; result.path = p + s; } result.slashes = result.slashes || relative.slashes; @@ -518,16 +591,16 @@ Url.prototype.resolveObject = function(relative) { return result; } - var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'), + var isSourceAbs = (result._pathname && result._pathname.charAt(0) === '/'), isRelAbs = ( relative.host || - relative.pathname && relative.pathname.charAt(0) === '/' + relative._pathname && relative._pathname.charAt(0) === '/' ), mustEndAbs = (isRelAbs || isSourceAbs || - (result.host && relative.pathname)), + (result.host && relative._pathname)), removeAllDots = mustEndAbs, - srcPath = result.pathname && result.pathname.split('/') || [], - relPath = relative.pathname && relative.pathname.split('/') || [], + srcPath = result._pathname && result._pathname.split('/') || [], + relPath = relative._pathname && relative._pathname.split('/') || [], psychotic = result.protocol && !slashedProtocol[result.protocol]; // if the url is a non-slashed url, then relative @@ -561,8 +634,8 @@ Url.prototype.resolveObject = function(relative) { relative.host : result.host; result.hostname = (relative.hostname || relative.hostname === '') ? relative.hostname : result.hostname; - result.search = relative.search; - result.query = relative.query; + result._search = relative._search; + result._query = relative._query; srcPath = relPath; // fall through to the dot-handling below. } else if (relPath.length) { @@ -571,9 +644,9 @@ Url.prototype.resolveObject = function(relative) { if (!srcPath) srcPath = []; srcPath.pop(); srcPath = srcPath.concat(relPath); - result.search = relative.search; - result.query = relative.query; - } else if (relative.search !== null && relative.search !== undefined) { + result._search = relative._search; + result._query = relative._query; + } else if (relative._search !== null && relative._search !== undefined) { // just pull out the search. // like href='?foo'. // Put this after the other two cases because it simplifies the booleans @@ -589,12 +662,12 @@ Url.prototype.resolveObject = function(relative) { result.host = result.hostname = authInHost.shift(); } } - result.search = relative.search; - result.query = relative.query; + result._search = relative._search; + result._query = relative._query; //to support http.request - if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); + if (result._pathname !== null || result._search !== null) { + result.path = (result._pathname ? result._pathname : '') + + (result._search ? result._search : ''); } result.href = result.format(); return result; @@ -603,10 +676,10 @@ Url.prototype.resolveObject = function(relative) { if (!srcPath.length) { // no path at all. easy. // we've already handled the other stuff above. - result.pathname = null; + result._pathname = null; //to support http.request - if (result.search) { - result.path = '/' + result.search; + if (result._search) { + result.path = '/' + result._search; } else { result.path = null; } @@ -679,16 +752,16 @@ Url.prototype.resolveObject = function(relative) { } if (!srcPath.length) { - result.pathname = null; + result._pathname = null; result.path = null; } else { - result.pathname = srcPath.join('/'); + result._pathname = srcPath.join('/'); } //to support request.http - if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); + if (result._pathname !== null || result._search !== null) { + result.path = (result._pathname ? result._pathname : '') + + (result._search ? result._search : ''); } result.auth = relative.auth || result.auth; result.slashes = result.slashes || relative.slashes; diff --git a/test/parallel/test-url-setter-and-getter.js b/test/parallel/test-url-setter-and-getter.js new file mode 100644 index 00000000000000..8d4283699f60f0 --- /dev/null +++ b/test/parallel/test-url-setter-and-getter.js @@ -0,0 +1,54 @@ +var common = require('../common'); +var assert = require('assert'); + +var url = require('url'), + util = require('util'); + +var example = 'https://example.com/path?query=value'; +var obj = url.parse(example); + +// test pathname +obj.pathname = 'path2'; +assert.equal(obj.path, 'path2?query=value'); +assert.equal(obj.format(), 'https://example.com/path2?query=value'); + +// test pathname with ? -> %3F +obj.pathname = 'path3?'; +assert.equal(obj.pathname, 'path3%3F'); +assert.equal(obj.path, 'path3%3F?query=value'); +assert.equal(obj.format(), 'https://example.com/path3%3F?query=value'); + +// test pathname with # -> %23 +obj.pathname = 'path3#'; +assert.equal(obj.pathname, 'path3%23'); +assert.equal(obj.path, 'path3%23?query=value'); +assert.equal(obj.format(), 'https://example.com/path3%23?query=value'); + +// resume the pathname for the following tests +obj.pathname = 'path2'; + +// test search +obj.search = '?foo=bar'; +assert.equal(obj.search, '?foo=bar'); +assert.equal(obj.path, 'path2?foo=bar'); +assert.equal(obj.format(), 'https://example.com/path2?foo=bar'); + +// test search without ? +obj.search = 'foo=bar2'; +assert.equal(obj.search, '?foo=bar2'); +assert.equal(obj.path, 'path2?foo=bar2'); +assert.equal(obj.format(), 'https://example.com/path2?foo=bar2'); + +// test query as string +obj.query = 'foo=bar3'; +assert.equal(obj.query, 'foo=bar3'); +assert.equal(obj.search, '?foo=bar3'); +assert.equal(obj.path, 'path2?foo=bar3'); +assert.equal(obj.format(), 'https://example.com/path2?foo=bar3'); + +// test query as object +obj.query = {foo: 'bar4'}; +assert.deepEqual(obj.query, {foo: 'bar4'}); +assert.equal(obj.search, '?foo=bar4'); +assert.equal(obj.path, 'path2?foo=bar4'); +assert.equal(obj.format(), 'https://example.com/path2?foo=bar4'); diff --git a/test/parallel/test-url.js b/test/parallel/test-url.js index 7b46fc96d5acd1..83675dbb79ea22 100644 --- a/test/parallel/test-url.js +++ b/test/parallel/test-url.js @@ -810,7 +810,7 @@ var parseTests = { pathname: '/' }, - 'http://a@b?@c': { + 'http://a@b/?@c': { protocol: 'http:', slashes: true, auth: 'a', @@ -857,14 +857,13 @@ var parseTests = { }; for (var u in parseTests) { - var actual = url.parse(u), - spaced = url.parse(' \t ' + u + '\n\t'); + var actual = url.parse(u).data, + spaced = url.parse(' \t ' + u + '\n\t').data; expected = parseTests[u]; Object.keys(actual).forEach(function (i) { - if (expected[i] === undefined && actual[i] === null) { + if (expected[i] === undefined && actual[i] === null) expected[i] = null; - } }); assert.deepEqual(actual, expected); @@ -929,7 +928,7 @@ var parseTestsWithQueryString = { } }; for (var u in parseTestsWithQueryString) { - var actual = url.parse(u, true); + var actual = url.parse(u, true).data; var expected = parseTestsWithQueryString[u]; for (var i in actual) { if (actual[i] === null && expected[i] === undefined) { @@ -1128,7 +1127,93 @@ var formatTests = { hash: '#frag', search: '?abc=the#1?&foo=bar', pathname: '/fooA100%mBr', + }, + + // path + 'http://github.com/joyent/node#js1': { + href: 'http://github.com/joyent/node#js1', + protocol: 'http:', + hostname: 'github.com', + hash: '#js1', + path: '/joyent/node' + }, + + // pathname vs. path, path wins + 'http://github.com/joyent/node2#js1': { + href: 'http://github.com/joyent/node2#js1', + protocol: 'http:', + hostname: 'github.com', + hash: '#js1', + path: '/joyent/node2', + pathname: '/joyent/node' + }, + + // pathname with query/search + 'http://github.com/joyent/node?foo=bar#js2': { + href: 'http://github.com/joyent/node?foo=bar#js2', + protocol: 'http:', + hostname: 'github.com', + hash: '#js2', + path: '/joyent/node?foo=bar' + }, + + // path vs. query, path wins + 'http://github.com/joyent/node?foo=bar2#js3': { + href: 'http://github.com/joyent/node?foo=bar2#js3', + protocol: 'http:', + hostname: 'github.com', + hash: '#js3', + path: '/joyent/node?foo=bar2', + query: {foo: 'bar'} + }, + + // path vs. search, path wins + 'http://github.com/joyent/node?foo=bar3#js4': { + href: 'http://github.com/joyent/node?foo=bar3#js4', + protocol: 'http:', + hostname: 'github.com', + hash: '#js4', + path: '/joyent/node?foo=bar3', + search: '?foo=bar' + }, + + // path is present without ? vs. query given + 'http://github.com/joyent/node#js5': { + href: 'http://github.com/joyent/node#js5', + protocol: 'http:', + hostname: 'github.com', + hash: '#js5', + path: '/joyent/node', + query: {foo: 'bar'} + }, + + // path is present without ? vs. search given + 'http://github.com/joyent/node#js6': { + href: 'http://github.com/joyent/node#js6', + protocol: 'http:', + hostname: 'github.com', + hash: '#js6', + path: '/joyent/node', + search: '?foo=bar', + pathname: '/fooA100%mBr' + }, + + // git+ssh + 'git+ssh://git@github.com:iojs/io.js.git': { + protocol: 'git+ssh:', + slashes: true, + auth: 'git', + host: 'github.com', + port: null, + hostname: 'github.com', + hash: null, + search: null, + query: null, + pathname: '/:iojs/io.js.git', + path: '/:iojs/io.js.git', + href: 'git+ssh://git@github.com/:iojs/io.js.git', } + }; for (var u in formatTests) { var expect = formatTests[u].href;