Localisation updates from https://translatewiki.net.
[mediawiki.git] / resources / lib / url / URL.js
blobe0efbdab875bf33b3d86b6f4c813b1bb55a2ce06
1 /* global Symbol */
2 // URL Polyfill
3 // Draft specification: https://url.spec.whatwg.org
5 // Notes:
6 // - Primarily useful for parsing URLs and modifying query parameters
7 // - Should work in IE8+ and everything more modern, with es5.js polyfills
9 (function (global) {
10         'use strict';
12         function isSequence(o) {
13                 if (!o) return false;
14                 if ('Symbol' in global && 'iterator' in global.Symbol &&
15                                 typeof o[Symbol.iterator] === 'function') return true;
16                 if (Array.isArray(o)) return true;
17                 return false;
18         }
20         ;(function() { // eslint-disable-line no-extra-semi
22                 // Browsers may have:
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;
28                 var nativeURL;
29                 try {
30                         if (origURL) {
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') {
36                                                 url.search = '';
37                                                 if (url.href === 'http://example.com/') {
38                                                         return;
39                                                 }
40                                         }
41                                 }
42                                 if (!('href' in nativeURL)) {
43                                         nativeURL = undefined;
44                                 }
45                                 nativeURL = undefined;
46                         }
47                 // eslint-disable-next-line no-empty
48                 } catch (_) {}
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;
58                                 first = false;
59                         });
60                         return output.replace(/%20/g, '+');
61                 }
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.
70                         //
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.
78                         //
79                         // Instead we:
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
92                                 //
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 + ')',
113                                         'gi'
114                                 );
115                         }
117                         return bytes.replace(cachedDecodePattern, function (match, u4, u3, u2, u1, uBad) {
118                                 return (uBad !== undefined) ? '\uFFFD' : decodeURIComponent(match);
119                         });
120                 }
122                 // NOTE: Doesn't do the encoding/decoding dance
123                 //
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];
129                         var pairs = [];
130                         sequences.forEach(function (bytes) {
131                                 if (bytes.length === 0) return;
132                                 var index = bytes.indexOf('=');
133                                 if (index !== -1) {
134                                         var name = bytes.substring(0, index);
135                                         var value = bytes.substring(index + 1);
136                                 } else {
137                                         name = bytes;
138                                         value = '';
139                                 }
140                                 name = name.replace(/\+/g, ' ');
141                                 value = value.replace(/\+/g, ' ');
142                                 pairs.push({ name: name, value: value });
143                         });
144                         var output = [];
145                         pairs.forEach(function (pair) {
146                                 output.push({
147                                         name: percent_decode(pair.name),
148                                         value: percent_decode(pair.value)
149                                 });
150                         });
151                         return output;
152                 }
154                 function URLUtils(url) {
155                         if (nativeURL)
156                                 return new origURL(url);
157                         var anchor = document.createElement('a');
158                         anchor.href = url;
159                         return anchor;
160                 }
162                 function URLSearchParams(init) {
163                         var $this = this;
164                         this._list = [];
166                         if (init === undefined || init === null) {
167                                 // no-op
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])});
177                                 });
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])});
181                                 });
182                         } else {
183                                 init = String(init);
184                                 if (init.substring(0, 1) === '?')
185                                         init = init.substring(1);
186                                 this._list = urlencoded_parse(init);
187                         }
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;
195                                 updating = true;
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];
203                                 }
205                                 $this._url_object.search = urlencoded_serialize($this._list);
207                                 updating = false;
208                         };
209                 }
212                 Object.defineProperties(URLSearchParams.prototype, {
213                         append: {
214                                 value: function (name, value) {
215                                         this._list.push({ name: name, value: value });
216                                         this._update_steps();
217                                 }, writable: true, enumerable: true, configurable: true
218                         },
220                         'delete': {
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);
225                                                 else
226                                                         ++i;
227                                         }
228                                         this._update_steps();
229                                 }, writable: true, enumerable: true, configurable: true
230                         },
232                         get: {
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;
237                                         }
238                                         return null;
239                                 }, writable: true, enumerable: true, configurable: true
240                         },
242                         getAll: {
243                                 value: function (name) {
244                                         var result = [];
245                                         for (var i = 0; i < this._list.length; ++i) {
246                                                 if (this._list[i].name === name)
247                                                         result.push(this._list[i].value);
248                                         }
249                                         return result;
250                                 }, writable: true, enumerable: true, configurable: true
251                         },
253                         has: {
254                                 value: function (name) {
255                                         for (var i = 0; i < this._list.length; ++i) {
256                                                 if (this._list[i].name === name)
257                                                         return true;
258                                         }
259                                         return false;
260                                 }, writable: true, enumerable: true, configurable: true
261                         },
263                         set: {
264                                 value: function (name, value) {
265                                         var found = false;
266                                         for (var i = 0; i < this._list.length;) {
267                                                 if (this._list[i].name === name) {
268                                                         if (!found) {
269                                                                 this._list[i].value = value;
270                                                                 found = true;
271                                                                 ++i;
272                                                         } else {
273                                                                 this._list.splice(i, 1);
274                                                         }
275                                                 } else {
276                                                         ++i;
277                                                 }
278                                         }
280                                         if (!found)
281                                                 this._list.push({ name: name, value: value });
283                                         this._update_steps();
284                                 }, writable: true, enumerable: true, configurable: true
285                         },
287                         entries: {
288                                 value: function() { return new Iterator(this._list, 'key+value'); },
289                                 writable: true, enumerable: true, configurable: true
290                         },
292                         keys: {
293                                 value: function() { return new Iterator(this._list, 'key'); },
294                                 writable: true, enumerable: true, configurable: true
295                         },
297                         values: {
298                                 value: function() { return new Iterator(this._list, 'value'); },
299                                 writable: true, enumerable: true, configurable: true
300                         },
302                         forEach: {
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);
307                                         });
309                                 }, writable: true, enumerable: true, configurable: true
310                         },
312                         toString: {
313                                 value: function () {
314                                         return urlencoded_serialize(this._list);
315                                 }, writable: true, enumerable: false, configurable: true
316                         },
318                         sort: {
319                                 value: function sort() {
320                                         var entries = this.entries();
321                                         var entry = entries.next();
322                                         var keys = [];
323                                         var values = {};
325                                         while (!entry.done) {
326                                                 var value = entry.value;
327                                                 var key = value[0];
328                                                 keys.push(key);
329                                                 if (!(Object.prototype.hasOwnProperty.call(values, key))) {
330                                                         values[key] = [];
331                                                 }
332                                                 values[key].push(value[1]);
333                                                 entry = entries.next();
334                                         }
336                                         keys.sort();
337                                         for (var i = 0; i < keys.length; i++) {
338                                                 this["delete"](keys[i]);
339                                         }
340                                         for (var j = 0; j < keys.length; j++) {
341                                                 key = keys[j];
342                                                 this.append(key, values[key].shift());
343                                         }
344                                 }
345                         }
346                 });
348                 function Iterator(source, kind) {
349                         var index = 0;
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]};
358                         };
359                 }
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});
368                 }
370                 function URL(url, base) {
371                         if (!(this instanceof global.URL))
372                                 throw new TypeError("Failed to construct 'URL': Please use the 'new' operator.");
374                         if (base) {
375                                 url = (function () {
376                                         if (nativeURL) return new origURL(url, base).href;
377                                         var iframe;
378                                         try {
379                                                 var doc;
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>');
395                                                         doc.close();
396                                                 }
398                                                 if (!doc) throw Error('base not supported');
400                                                 var baseTag = doc.createElement('base');
401                                                 baseTag.href = base;
402                                                 doc.getElementsByTagName('head')[0].appendChild(baseTag);
403                                                 var anchor = doc.createElement('a');
404                                                 anchor.href = url;
405                                                 return anchor.href;
406                                         } finally {
407                                                 if (iframe)
408                                                         iframe.parentNode.removeChild(iframe);
409                                         }
410                                 }());
411                         }
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
418                         // overridden
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;
426                                 try {
427                                         var obj = {};
428                                         Object.defineProperties(obj, { prop: { get: function () { return true; } } });
429                                         return obj.prop;
430                                 } catch (_) {
431                                         return false;
432                                 }
433                         }());
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, {
444                                 href: {
445                                         get: function () { return instance.href; },
446                                         set: function (v) { instance.href = v; tidy_instance(); update_steps(); },
447                                         enumerable: true, configurable: true
448                                 },
449                                 origin: {
450                                         get: function () {
451                                                 if (this.protocol.toLowerCase() === "data:") {
452                                                         return null
453                                                 }
455                                                 if ('origin' in instance) return instance.origin;
456                                                 return this.protocol + '//' + this.host;
457                                         },
458                                         enumerable: true, configurable: true
459                                 },
460                                 protocol: {
461                                         get: function () { return instance.protocol; },
462                                         set: function (v) { instance.protocol = v; },
463                                         enumerable: true, configurable: true
464                                 },
465                                 username: {
466                                         get: function () { return instance.username; },
467                                         set: function (v) { instance.username = v; },
468                                         enumerable: true, configurable: true
469                                 },
470                                 password: {
471                                         get: function () { return instance.password; },
472                                         set: function (v) { instance.password = v; },
473                                         enumerable: true, configurable: true
474                                 },
475                                 host: {
476                                         get: function () {
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;
480                                         },
481                                         set: function (v) { instance.host = v; },
482                                         enumerable: true, configurable: true
483                                 },
484                                 hostname: {
485                                         get: function () { return instance.hostname; },
486                                         set: function (v) { instance.hostname = v; },
487                                         enumerable: true, configurable: true
488                                 },
489                                 port: {
490                                         get: function () { return instance.port; },
491                                         set: function (v) { instance.port = v; },
492                                         enumerable: true, configurable: true
493                                 },
494                                 pathname: {
495                                         get: function () {
496                                                 // IE does not include leading '/' in |pathname|
497                                                 if (instance.pathname.charAt(0) !== '/') return '/' + instance.pathname;
498                                                 return instance.pathname;
499                                         },
500                                         set: function (v) { instance.pathname = v; },
501                                         enumerable: true, configurable: true
502                                 },
503                                 search: {
504                                         get: function () { return instance.search; },
505                                         set: function (v) {
506                                                 if (instance.search === v) return;
507                                                 instance.search = v; tidy_instance(); update_steps();
508                                         },
509                                         enumerable: true, configurable: true
510                                 },
511                                 searchParams: {
512                                         get: function () { return query_object; },
513                                         enumerable: true, configurable: true
514                                 },
515                                 hash: {
516                                         get: function () { return instance.hash; },
517                                         set: function (v) { instance.hash = v; tidy_instance(); },
518                                         enumerable: true, configurable: true
519                                 },
520                                 toString: {
521                                         value: function() { return instance.toString(); },
522                                         enumerable: false, configurable: true
523                                 },
524                                 valueOf: {
525                                         value: function() { return instance.valueOf(); },
526                                         enumerable: false, configurable: true
527                                 }
528                         });
530                         function tidy_instance() {
531                                 var href = instance.href.replace(/#$|\?$|\?(?=#)/g, '');
532                                 if (instance.href !== href)
533                                         instance.href = href;
534                         }
536                         function update_steps() {
537                                 query_object._setList(instance.search ? urlencoded_parse(instance.search.substring(1)) : []);
538                                 query_object._update_steps();
539                         }
541                         return self;
542                 }
544                 if (origURL) {
545                         for (var i in origURL) {
546                                 if (Object.prototype.hasOwnProperty.call(origURL, i) && typeof origURL[i] === 'function')
547                                         URL[i] = origURL[i];
548                         }
549                 }
551                 global.URL = URL;
552                 global.URLSearchParams = URLSearchParams;
553         })();
555         // Patch native URLSearchParams constructor to handle sequences/records
556         // if necessary.
557         (function() {
558                 if (new global.URLSearchParams([['a', 1]]).get('a') === '1' &&
559                                 new global.URLSearchParams({a: 1}).get('a') === '1')
560                         return;
561                 var orig = global.URLSearchParams;
562                 global.URLSearchParams = function(init) {
563                         if (init && typeof init === 'object' && isSequence(init)) {
564                                 var o = new orig();
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]);
570                                 });
571                                 return o;
572                         } else if (init && typeof init === 'object') {
573                                 o = new orig();
574                                 Object.keys(init).forEach(function(key) {
575                                         o.set(key, init[key]);
576                                 });
577                                 return o;
578                         } else {
579                                 return new orig(init);
580                         }
581                 };
582         })();
584 }(self));