3 // Draft specification: https://url.spec.whatwg.org
6 // - Primarily useful for parsing URLs and modifying query parameters
7 // - Should work in IE8+ and everything more modern, with es5.js polyfills
12 function isSequence(o) {
14 if ('Symbol' in global && 'iterator' in global.Symbol &&
15 typeof o[Symbol.iterator] === 'function') return true;
16 if (Array.isArray(o)) return true;
20 ;(function() { // eslint-disable-line no-extra-semi
23 // * No global URL object
24 // * URL with static methods only - may have a dummy constructor
25 // * URL with members except searchParams
26 // * Full URL API support
27 var origURL = global.URL;
31 nativeURL = new global.URL('http://example.com');
32 if ('searchParams' in nativeURL) {
33 var url = new URL('http://example.com');
34 url.search = 'a=1&b=2';
35 if (url.href === 'http://example.com/?a=1&b=2') {
37 if (url.href === 'http://example.com/') {
42 if (!('href' in nativeURL)) {
43 nativeURL = undefined;
45 nativeURL = undefined;
47 // eslint-disable-next-line no-empty
50 // NOTE: Doesn't do the encoding/decoding dance
51 function urlencoded_serialize(pairs) {
52 var output = '', first = true;
53 pairs.forEach(function (pair) {
54 var name = encodeURIComponent(pair.name);
55 var value = encodeURIComponent(pair.value);
56 if (!first) output += '&';
57 output += name + '=' + value;
60 return output.replace(/%20/g, '+');
63 // https://url.spec.whatwg.org/#percent-decode
64 var cachedDecodePattern;
65 function percent_decode(bytes) {
66 // This can't simply use decodeURIComponent (part of ECMAScript) as that's limited to
67 // decoding to valid UTF-8 only. It throws URIError for literals that look like percent
68 // encoding (e.g. `x=%`, `x=%a`, and `x=a%2sf`) and for non-UTF8 binary data that was
69 // percent encoded and cannot be turned back into binary within a JavaScript string.
71 // The spec deals with this as follows:
72 // * Read input as UTF-8 encoded bytes. This needs low-level access or a modern
73 // Web API, like TextDecoder. Old browsers don't have that, and it'd a large
74 // dependency to add to this polyfill.
75 // * For each percentage sign followed by two hex, blindly decode the byte in binary
76 // form. This would require TextEncoder to not corrupt multi-byte chars.
77 // * Replace any bytes that would be invalid under UTF-8 with U+FFFD.
80 // * Use the fact that UTF-8 is designed to make validation easy in binary.
81 // You don't have to decode first. There are only a handful of valid prefixes and
82 // ranges, per RFC 3629. <https://datatracker.ietf.org/doc/html/rfc3629#section-3>
83 // * Safely create multi-byte chars with decodeURIComponent, by only passing it
84 // valid and full characters (e.g. "%F0" separately from "%F0%9F%92%A9" throws).
85 // Anything else is kept as literal or replaced with U+FFFD, as per the URL spec.
87 if (!cachedDecodePattern) {
88 // In a UTF-8 multibyte sequence, non-initial bytes are always between %80 and %BF
89 var uContinuation = '%[89AB][0-9A-F]';
91 // The length of a UTF-8 sequence is specified by the first byte
93 // One-byte sequences: 0xxxxxxx
94 // So the byte is between %00 and %7F
95 var u1Bytes = '%[0-7][0-9A-F]';
96 // Two-byte sequences: 110xxxxx 10xxxxxx
97 // So the first byte is between %C0 and %DF
98 var u2Bytes = '%[CD][0-9A-F]' + uContinuation;
99 // Three-byte sequences: 1110xxxx 10xxxxxx 10xxxxxx
100 // So the first byte is between %E0 and %EF
101 var u3Bytes = '%E[0-9A-F]' + uContinuation + uContinuation;
102 // Four-byte sequences: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
103 // So the first byte is between %F0 and %F7
104 var u4Bytes = '%F[0-7]' + uContinuation + uContinuation +uContinuation;
106 var anyByte = '%[0-9A-F][0-9A-F]';
108 // Match some consecutive percent-escaped bytes. More precisely, match
109 // 1-4 bytes that validly encode one character in UTF-8, or 1 byte that
110 // would be invalid in UTF-8 in this location.
111 cachedDecodePattern = new RegExp(
112 '(' + u4Bytes + ')|(' + u3Bytes + ')|(' + u2Bytes + ')|(' + u1Bytes + ')|(' + anyByte + ')',
117 return bytes.replace(cachedDecodePattern, function (match, u4, u3, u2, u1, uBad) {
118 return (uBad !== undefined) ? '\uFFFD' : decodeURIComponent(match);
122 // NOTE: Doesn't do the encoding/decoding dance
124 // https://url.spec.whatwg.org/#concept-urlencoded-parser
125 function urlencoded_parse(input, isindex) {
126 var sequences = input.split('&');
127 if (isindex && sequences[0].indexOf('=') === -1)
128 sequences[0] = '=' + sequences[0];
130 sequences.forEach(function (bytes) {
131 if (bytes.length === 0) return;
132 var index = bytes.indexOf('=');
134 var name = bytes.substring(0, index);
135 var value = bytes.substring(index + 1);
140 name = name.replace(/\+/g, ' ');
141 value = value.replace(/\+/g, ' ');
142 pairs.push({ name: name, value: value });
145 pairs.forEach(function (pair) {
147 name: percent_decode(pair.name),
148 value: percent_decode(pair.value)
154 function URLUtils(url) {
156 return new origURL(url);
157 var anchor = document.createElement('a');
162 function URLSearchParams(init) {
166 if (init === undefined || init === null) {
168 } else if (init instanceof URLSearchParams) {
169 // In ES6 init would be a sequence, but special case for ES5.
170 this._list = urlencoded_parse(String(init));
171 } else if (typeof init === 'object' && isSequence(init)) {
172 Array.from(init).forEach(function(e) {
173 if (!isSequence(e)) throw TypeError();
174 var nv = Array.from(e);
175 if (nv.length !== 2) throw TypeError();
176 $this._list.push({name: String(nv[0]), value: String(nv[1])});
178 } else if (typeof init === 'object' && init) {
179 Object.keys(init).forEach(function(key) {
180 $this._list.push({name: String(key), value: String(init[key])});
184 if (init.substring(0, 1) === '?')
185 init = init.substring(1);
186 this._list = urlencoded_parse(init);
189 this._url_object = null;
190 this._setList = function (list) { if (!updating) $this._list = list; };
192 var updating = false;
193 this._update_steps = function() {
194 if (updating) return;
197 if (!$this._url_object) return;
199 // Partial workaround for IE issue with 'about:'
200 if ($this._url_object.protocol === 'about:' &&
201 $this._url_object.pathname.indexOf('?') !== -1) {
202 $this._url_object.pathname = $this._url_object.pathname.split('?')[0];
205 $this._url_object.search = urlencoded_serialize($this._list);
212 Object.defineProperties(URLSearchParams.prototype, {
214 value: function (name, value) {
215 this._list.push({ name: name, value: value });
216 this._update_steps();
217 }, writable: true, enumerable: true, configurable: true
221 value: function (name) {
222 for (var i = 0; i < this._list.length;) {
223 if (this._list[i].name === name)
224 this._list.splice(i, 1);
228 this._update_steps();
229 }, writable: true, enumerable: true, configurable: true
233 value: function (name) {
234 for (var i = 0; i < this._list.length; ++i) {
235 if (this._list[i].name === name)
236 return this._list[i].value;
239 }, writable: true, enumerable: true, configurable: true
243 value: function (name) {
245 for (var i = 0; i < this._list.length; ++i) {
246 if (this._list[i].name === name)
247 result.push(this._list[i].value);
250 }, writable: true, enumerable: true, configurable: true
254 value: function (name) {
255 for (var i = 0; i < this._list.length; ++i) {
256 if (this._list[i].name === name)
260 }, writable: true, enumerable: true, configurable: true
264 value: function (name, value) {
266 for (var i = 0; i < this._list.length;) {
267 if (this._list[i].name === name) {
269 this._list[i].value = value;
273 this._list.splice(i, 1);
281 this._list.push({ name: name, value: value });
283 this._update_steps();
284 }, writable: true, enumerable: true, configurable: true
288 value: function() { return new Iterator(this._list, 'key+value'); },
289 writable: true, enumerable: true, configurable: true
293 value: function() { return new Iterator(this._list, 'key'); },
294 writable: true, enumerable: true, configurable: true
298 value: function() { return new Iterator(this._list, 'value'); },
299 writable: true, enumerable: true, configurable: true
303 value: function(callback) {
304 var thisArg = (arguments.length > 1) ? arguments[1] : undefined;
305 this._list.forEach(function(pair) {
306 callback.call(thisArg, pair.value, pair.name);
309 }, writable: true, enumerable: true, configurable: true
314 return urlencoded_serialize(this._list);
315 }, writable: true, enumerable: false, configurable: true
319 value: function sort() {
320 var entries = this.entries();
321 var entry = entries.next();
325 while (!entry.done) {
326 var value = entry.value;
329 if (!(Object.prototype.hasOwnProperty.call(values, key))) {
332 values[key].push(value[1]);
333 entry = entries.next();
337 for (var i = 0; i < keys.length; i++) {
338 this["delete"](keys[i]);
340 for (var j = 0; j < keys.length; j++) {
342 this.append(key, values[key].shift());
348 function Iterator(source, kind) {
350 this.next = function() {
351 if (index >= source.length)
352 return {done: true, value: undefined};
353 var pair = source[index++];
354 return {done: false, value:
355 kind === 'key' ? pair.name :
356 kind === 'value' ? pair.value :
357 [pair.name, pair.value]};
361 if ('Symbol' in global && 'iterator' in global.Symbol) {
362 Object.defineProperty(URLSearchParams.prototype, global.Symbol.iterator, {
363 value: URLSearchParams.prototype.entries,
364 writable: true, enumerable: true, configurable: true});
365 Object.defineProperty(Iterator.prototype, global.Symbol.iterator, {
366 value: function() { return this; },
367 writable: true, enumerable: true, configurable: true});
370 function URL(url, base) {
371 if (!(this instanceof global.URL))
372 throw new TypeError("Failed to construct 'URL': Please use the 'new' operator.");
376 if (nativeURL) return new origURL(url, base).href;
380 // Use another document/base tag/anchor for relative URL resolution, if possible
381 if (Object.prototype.toString.call(window.operamini) === "[object OperaMini]") {
382 iframe = document.createElement('iframe');
383 iframe.style.display = 'none';
384 document.documentElement.appendChild(iframe);
385 doc = iframe.contentWindow.document;
386 } else if (document.implementation && document.implementation.createHTMLDocument) {
387 doc = document.implementation.createHTMLDocument('');
388 } else if (document.implementation && document.implementation.createDocument) {
389 doc = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null);
390 doc.documentElement.appendChild(doc.createElement('head'));
391 doc.documentElement.appendChild(doc.createElement('body'));
392 } else if (window.ActiveXObject) {
393 doc = new window.ActiveXObject('htmlfile');
394 doc.write('<head></head><body></body>');
398 if (!doc) throw Error('base not supported');
400 var baseTag = doc.createElement('base');
402 doc.getElementsByTagName('head')[0].appendChild(baseTag);
403 var anchor = doc.createElement('a');
408 iframe.parentNode.removeChild(iframe);
413 // An inner object implementing URLUtils (either a native URL
414 // object or an HTMLAnchorElement instance) is used to perform the
415 // URL algorithms. With full ES5 getter/setter support, return a
416 // regular object For IE8's limited getter/setter support, a
417 // different HTMLAnchorElement is returned with properties
420 var instance = URLUtils(url || '');
422 // Detect for ES5 getter/setter support
423 // (an Object.defineProperties polyfill that doesn't support getters/setters may throw)
424 var ES5_GET_SET = (function() {
425 if (!('defineProperties' in Object)) return false;
428 Object.defineProperties(obj, { prop: { get: function () { return true; } } });
435 var self = ES5_GET_SET ? this : document.createElement('a');
439 var query_object = new URLSearchParams(
440 instance.search ? instance.search.substring(1) : null);
441 query_object._url_object = self;
443 Object.defineProperties(self, {
445 get: function () { return instance.href; },
446 set: function (v) { instance.href = v; tidy_instance(); update_steps(); },
447 enumerable: true, configurable: true
451 if (this.protocol.toLowerCase() === "data:") {
455 if ('origin' in instance) return instance.origin;
456 return this.protocol + '//' + this.host;
458 enumerable: true, configurable: true
461 get: function () { return instance.protocol; },
462 set: function (v) { instance.protocol = v; },
463 enumerable: true, configurable: true
466 get: function () { return instance.username; },
467 set: function (v) { instance.username = v; },
468 enumerable: true, configurable: true
471 get: function () { return instance.password; },
472 set: function (v) { instance.password = v; },
473 enumerable: true, configurable: true
477 // IE returns default port in |host|
478 var re = {'http:': /:80$/, 'https:': /:443$/, 'ftp:': /:21$/}[instance.protocol];
479 return re ? instance.host.replace(re, '') : instance.host;
481 set: function (v) { instance.host = v; },
482 enumerable: true, configurable: true
485 get: function () { return instance.hostname; },
486 set: function (v) { instance.hostname = v; },
487 enumerable: true, configurable: true
490 get: function () { return instance.port; },
491 set: function (v) { instance.port = v; },
492 enumerable: true, configurable: true
496 // IE does not include leading '/' in |pathname|
497 if (instance.pathname.charAt(0) !== '/') return '/' + instance.pathname;
498 return instance.pathname;
500 set: function (v) { instance.pathname = v; },
501 enumerable: true, configurable: true
504 get: function () { return instance.search; },
506 if (instance.search === v) return;
507 instance.search = v; tidy_instance(); update_steps();
509 enumerable: true, configurable: true
512 get: function () { return query_object; },
513 enumerable: true, configurable: true
516 get: function () { return instance.hash; },
517 set: function (v) { instance.hash = v; tidy_instance(); },
518 enumerable: true, configurable: true
521 value: function() { return instance.toString(); },
522 enumerable: false, configurable: true
525 value: function() { return instance.valueOf(); },
526 enumerable: false, configurable: true
530 function tidy_instance() {
531 var href = instance.href.replace(/#$|\?$|\?(?=#)/g, '');
532 if (instance.href !== href)
533 instance.href = href;
536 function update_steps() {
537 query_object._setList(instance.search ? urlencoded_parse(instance.search.substring(1)) : []);
538 query_object._update_steps();
545 for (var i in origURL) {
546 if (Object.prototype.hasOwnProperty.call(origURL, i) && typeof origURL[i] === 'function')
552 global.URLSearchParams = URLSearchParams;
555 // Patch native URLSearchParams constructor to handle sequences/records
558 if (new global.URLSearchParams([['a', 1]]).get('a') === '1' &&
559 new global.URLSearchParams({a: 1}).get('a') === '1')
561 var orig = global.URLSearchParams;
562 global.URLSearchParams = function(init) {
563 if (init && typeof init === 'object' && isSequence(init)) {
565 Array.from(init).forEach(function (e) {
566 if (!isSequence(e)) throw TypeError();
567 var nv = Array.from(e);
568 if (nv.length !== 2) throw TypeError();
569 o.append(nv[0], nv[1]);
572 } else if (init && typeof init === 'object') {
574 Object.keys(init).forEach(function(key) {
575 o.set(key, init[key]);
579 return new orig(init);