summaryrefslogtreecommitdiff
path: root/resources/src/mediawiki/mediawiki.Uri.js
blob: 29b224ee6aeb65aa79629a8daa3f10123e88c115 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
/**
 * Library for simple URI parsing and manipulation.
 *
 * Intended to be minimal, but featureful; do not expect full RFC 3986 compliance. The use cases we
 * have in mind are constructing 'next page' or 'previous page' URLs, detecting whether we need to
 * use cross-domain proxies for an API, constructing simple URL-based API calls, etc. Parsing here
 * is regex-based, so may not work on all URIs, but is good enough for most.
 *
 * You can modify the properties directly, then use the #toString method to extract the full URI
 * string again. Example:
 *
 *     var uri = new mw.Uri( 'http://example.com/mysite/mypage.php?quux=2' );
 *
 *     if ( uri.host == 'example.com' ) {
 *         uri.host = 'foo.example.com';
 *         uri.extend( { bar: 1 } );
 *
 *         $( 'a#id1' ).attr( 'href', uri );
 *         // anchor with id 'id1' now links to http://foo.example.com/mysite/mypage.php?bar=1&quux=2
 *
 *         $( 'a#id2' ).attr( 'href', uri.clone().extend( { bar: 3, pif: 'paf' } ) );
 *         // anchor with id 'id2' now links to http://foo.example.com/mysite/mypage.php?bar=3&quux=2&pif=paf
 *     }
 *
 * Given a URI like
 * `http://usr:pwd@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=&test3=value+%28escaped%29&r=1&r=2#top`
 * the returned object will have the following properties:
 *
 *     protocol  'http'
 *     user      'usr'
 *     password  'pwd'
 *     host      'www.example.com'
 *     port      '81'
 *     path      '/dir/dir.2/index.htm'
 *     query     {
 *                   q1: '0',
 *                   test1: null,
 *                   test2: '',
 *                   test3: 'value (escaped)'
 *                   r: ['1', '2']
 *               }
 *     fragment  'top'
 *
 * (N.b., 'password' is technically not allowed for HTTP URIs, but it is possible with other kinds
 * of URIs.)
 *
 * Parsing based on parseUri 1.2.2 (c) Steven Levithan <http://stevenlevithan.com>, MIT License.
 * <http://stevenlevithan.com/demo/parseuri/js/>
 *
 * @class mw.Uri
 */

( function ( mw, $ ) {
	/**
	 * Function that's useful when constructing the URI string -- we frequently encounter the pattern
	 * of having to add something to the URI as we go, but only if it's present, and to include a
	 * character before or after if so.
	 *
	 * @private
	 * @static
	 * @param {string|undefined} pre To prepend
	 * @param {string} val To include
	 * @param {string} post To append
	 * @param {boolean} raw If true, val will not be encoded
	 * @return {string} Result
	 */
	function cat( pre, val, post, raw ) {
		if ( val === undefined || val === null || val === '' ) {
			return '';
		}
		/* jshint latedef:false */
		return pre + ( raw ? val : mw.Uri.encode( val ) ) + post;
		/* jshint latedef:true */
	}

	/**
	 * Regular expressions to parse many common URIs.
	 *
	 * As they are gnarly, they have been moved to separate files to allow us to format them in the
	 * 'extended' regular expression format (which JavaScript normally doesn't support). The subset of
	 * features handled is minimal, but just the free whitespace gives us a lot.
	 *
	 * @private
	 * @static
	 * @property {Object} parser
	 */
	var parser = {
		strict: mw.template.get( 'mediawiki.Uri', 'strict.regexp' ).render(),
		loose: mw.template.get( 'mediawiki.Uri', 'loose.regexp' ).render()
	},

	/**
	 * The order here matches the order of captured matches in the `parser` property regexes.
	 *
	 * @private
	 * @static
	 * @property {Array} properties
	 */
	properties = [
		'protocol',
		'user',
		'password',
		'host',
		'port',
		'path',
		'query',
		'fragment'
	];

	/**
	 * @property {string} protocol For example `http` (always present)
	 */
	/**
	 * @property {string|undefined} user For example `usr`
	 */
	/**
	 * @property {string|undefined} password For example `pwd`
	 */
	/**
	 * @property {string} host For example `www.example.com` (always present)
	 */
	/**
	 * @property {string|undefined} port For example `81`
	 */
	/**
	 * @property {string} path For example `/dir/dir.2/index.htm` (always present)
	 */
	/**
	 * @property {Object} query For example `{ a: '0', b: '', c: 'value' }` (always present)
	 */
	/**
	 * @property {string|undefined} fragment For example `top`
	 */

	/**
	 * A factory method to create a Uri class with a default location to resolve relative URLs
	 * against (including protocol-relative URLs).
	 *
	 * @method
	 * @param {string|Function} documentLocation A full url, or function returning one.
	 *  If passed a function, the return value may change over time and this will be honoured. (T74334)
	 * @member mw
	 */
	mw.UriRelative = function ( documentLocation ) {
		var getDefaultUri = ( function () {
			// Cache
			var href, uri;

			return function () {
				var hrefCur = typeof documentLocation === 'string' ? documentLocation : documentLocation();
				if ( href === hrefCur ) {
					return uri;
				}
				href = hrefCur;
				uri = new Uri( href );
				return uri;
			};
		}() );

		/**
		 * @class mw.Uri
		 * @constructor
		 *
		 * Construct a new URI object. Throws error if arguments are illegal/impossible, or
		 * otherwise don't parse.
		 *
		 * @param {Object|string} [uri] URI string, or an Object with appropriate properties (especially
		 *  another URI object to clone). Object must have non-blank `protocol`, `host`, and `path`
		 *  properties. If omitted (or set to `undefined`, `null` or empty string), then an object
		 *  will be created for the default `uri` of this constructor (`location.href` for mw.Uri,
		 *  other values for other instances -- see mw.UriRelative for details).
		 * @param {Object|boolean} [options] Object with options, or (backwards compatibility) a boolean
		 *  for strictMode
		 * @param {boolean} [options.strictMode=false] Trigger strict mode parsing of the url.
		 * @param {boolean} [options.overrideKeys=false] Whether to let duplicate query parameters
		 *  override each other (`true`) or automagically convert them to an array (`false`).
		 */
		/* jshint latedef:false */
		function Uri( uri, options ) {
			var prop,
				defaultUri = getDefaultUri();

			options = typeof options === 'object' ? options : { strictMode: !!options };
			options = $.extend( {
				strictMode: false,
				overrideKeys: false
			}, options );

			if ( uri !== undefined && uri !== null && uri !== '' ) {
				if ( typeof uri === 'string' ) {
					this.parse( uri, options );
				} else if ( typeof uri === 'object' ) {
					// Copy data over from existing URI object
					for ( prop in uri ) {
						// Only copy direct properties, not inherited ones
						if ( uri.hasOwnProperty( prop ) ) {
							// Deep copy object properties
							if ( $.isArray( uri[ prop ] ) || $.isPlainObject( uri[ prop ] ) ) {
								this[ prop ] = $.extend( true, {}, uri[ prop ] );
							} else {
								this[ prop ] = uri[ prop ];
							}
						}
					}
					if ( !this.query ) {
						this.query = {};
					}
				}
			} else {
				// If we didn't get a URI in the constructor, use the default one.
				return defaultUri.clone();
			}

			// protocol-relative URLs
			if ( !this.protocol ) {
				this.protocol = defaultUri.protocol;
			}
			// No host given:
			if ( !this.host ) {
				this.host = defaultUri.host;
				// port ?
				if ( !this.port ) {
					this.port = defaultUri.port;
				}
			}
			if ( this.path && this.path[ 0 ] !== '/' ) {
				// A real relative URL, relative to defaultUri.path. We can't really handle that since we cannot
				// figure out whether the last path component of defaultUri.path is a directory or a file.
				throw new Error( 'Bad constructor arguments' );
			}
			if ( !( this.protocol && this.host && this.path ) ) {
				throw new Error( 'Bad constructor arguments' );
			}
		}

		/**
		 * Encode a value for inclusion in a url.
		 *
		 * Standard encodeURIComponent, with extra stuff to make all browsers work similarly and more
		 * compliant with RFC 3986. Similar to rawurlencode from PHP and our JS library
		 * mw.util.rawurlencode, except this also replaces spaces with `+`.
		 *
		 * @static
		 * @param {string} s String to encode
		 * @return {string} Encoded string for URI
		 */
		Uri.encode = function ( s ) {
			return encodeURIComponent( s )
				.replace( /!/g, '%21' ).replace( /'/g, '%27' ).replace( /\(/g, '%28' )
				.replace( /\)/g, '%29' ).replace( /\*/g, '%2A' )
				.replace( /%20/g, '+' );
		};

		/**
		 * Decode a url encoded value.
		 *
		 * Reversed #encode. Standard decodeURIComponent, with addition of replacing
		 * `+` with a space.
		 *
		 * @static
		 * @param {string} s String to decode
		 * @return {string} Decoded string
		 */
		Uri.decode = function ( s ) {
			return decodeURIComponent( s.replace( /\+/g, '%20' ) );
		};

		Uri.prototype = {

			/**
			 * Parse a string and set our properties accordingly.
			 *
			 * @private
			 * @param {string} str URI, see constructor.
			 * @param {Object} options See constructor.
			 */
			parse: function ( str, options ) {
				var q, matches,
					uri = this;

				// Apply parser regex and set all properties based on the result
				matches = parser[ options.strictMode ? 'strict' : 'loose' ].exec( str );
				$.each( properties, function ( i, property ) {
					uri[ property ] = matches[ i + 1 ];
				} );

				// uri.query starts out as the query string; we will parse it into key-val pairs then make
				// that object the "query" property.
				// we overwrite query in uri way to make cloning easier, it can use the same list of properties.
				q = {};
				// using replace to iterate over a string
				if ( uri.query ) {
					uri.query.replace( /(?:^|&)([^&=]*)(?:(=)([^&]*))?/g, function ( $0, $1, $2, $3 ) {
						var k, v;
						if ( $1 ) {
							k = Uri.decode( $1 );
							v = ( $2 === '' || $2 === undefined ) ? null : Uri.decode( $3 );

							// If overrideKeys, always (re)set top level value.
							// If not overrideKeys but this key wasn't set before, then we set it as well.
							if ( options.overrideKeys || q[ k ] === undefined ) {
								q[ k ] = v;

							// Use arrays if overrideKeys is false and key was already seen before
							} else {
								// Once before, still a string, turn into an array
								if ( typeof q[ k ] === 'string' ) {
									q[ k ] = [ q[ k ] ];
								}
								// Add to the array
								if ( $.isArray( q[ k ] ) ) {
									q[ k ].push( v );
								}
							}
						}
					} );
				}
				uri.query = q;
			},

			/**
			 * Get user and password section of a URI.
			 *
			 * @return {string}
			 */
			getUserInfo: function () {
				return cat( '', this.user, cat( ':', this.password, '' ) );
			},

			/**
			 * Get host and port section of a URI.
			 *
			 * @return {string}
			 */
			getHostPort: function () {
				return this.host + cat( ':', this.port, '' );
			},

			/**
			 * Get the userInfo, host and port section of the URI.
			 *
			 * In most real-world URLs this is simply the hostname, but the definition of 'authority' section is more general.
			 *
			 * @return {string}
			 */
			getAuthority: function () {
				return cat( '', this.getUserInfo(), '@' ) + this.getHostPort();
			},

			/**
			 * Get the query arguments of the URL, encoded into a string.
			 *
			 * Does not preserve the original order of arguments passed in the URI. Does handle escaping.
			 *
			 * @return {string}
			 */
			getQueryString: function () {
				var args = [];
				$.each( this.query, function ( key, val ) {
					var k = Uri.encode( key ),
						vals = $.isArray( val ) ? val : [ val ];
					$.each( vals, function ( i, v ) {
						if ( v === null ) {
							args.push( k );
						} else if ( k === 'title' ) {
							args.push( k + '=' + mw.util.wikiUrlencode( v ) );
						} else {
							args.push( k + '=' + Uri.encode( v ) );
						}
					} );
				} );
				return args.join( '&' );
			},

			/**
			 * Get everything after the authority section of the URI.
			 *
			 * @return {string}
			 */
			getRelativePath: function () {
				return this.path + cat( '?', this.getQueryString(), '', true ) + cat( '#', this.fragment, '' );
			},

			/**
			 * Get the entire URI string.
			 *
			 * May not be precisely the same as input due to order of query arguments.
			 *
			 * @return {string} The URI string
			 */
			toString: function () {
				return this.protocol + '://' + this.getAuthority() + this.getRelativePath();
			},

			/**
			 * Clone this URI
			 *
			 * @return {Object} New URI object with same properties
			 */
			clone: function () {
				return new Uri( this );
			},

			/**
			 * Extend the query section of the URI with new parameters.
			 *
			 * @param {Object} parameters Query parameters to add to ours (or to override ours with) as an
			 *  object
			 * @return {Object} This URI object
			 */
			extend: function ( parameters ) {
				$.extend( this.query, parameters );
				return this;
			}
		};

		return Uri;
	};

	// Default to the current browsing location (for relative URLs).
	mw.Uri = mw.UriRelative( function () {
		return location.href;
	} );

}( mediaWiki, jQuery ) );