lib/goog/uri/uri.js

1// Copyright 2006 The Closure Library Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS-IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/**
16 * @fileoverview Class for parsing and formatting URIs.
17 *
18 * Use goog.Uri(string) to parse a URI string. Use goog.Uri.create(...) to
19 * create a new instance of the goog.Uri object from Uri parts.
20 *
21 * e.g: <code>var myUri = new goog.Uri(window.location);</code>
22 *
23 * Implements RFC 3986 for parsing/formatting URIs.
24 * http://www.ietf.org/rfc/rfc3986.txt
25 *
26 * Some changes have been made to the interface (more like .NETs), though the
27 * internal representation is now of un-encoded parts, this will change the
28 * behavior slightly.
29 *
30 */
31
32goog.provide('goog.Uri');
33goog.provide('goog.Uri.QueryData');
34
35goog.require('goog.array');
36goog.require('goog.string');
37goog.require('goog.structs');
38goog.require('goog.structs.Map');
39goog.require('goog.uri.utils');
40goog.require('goog.uri.utils.ComponentIndex');
41goog.require('goog.uri.utils.StandardQueryParam');
42
43
44
45/**
46 * This class contains setters and getters for the parts of the URI.
47 * The <code>getXyz</code>/<code>setXyz</code> methods return the decoded part
48 * -- so<code>goog.Uri.parse('/foo%20bar').getPath()</code> will return the
49 * decoded path, <code>/foo bar</code>.
50 *
51 * The constructor accepts an optional unparsed, raw URI string. The parser
52 * is relaxed, so special characters that aren't escaped but don't cause
53 * ambiguities will not cause parse failures.
54 *
55 * All setters return <code>this</code> and so may be chained, a la
56 * <code>goog.Uri.parse('/foo').setFragment('part').toString()</code>.
57 *
58 * @param {*=} opt_uri Optional string URI to parse
59 * (use goog.Uri.create() to create a URI from parts), or if
60 * a goog.Uri is passed, a clone is created.
61 * @param {boolean=} opt_ignoreCase If true, #getParameterValue will ignore
62 * the case of the parameter name.
63 *
64 * @constructor
65 */
66goog.Uri = function(opt_uri, opt_ignoreCase) {
67 // Parse in the uri string
68 var m;
69 if (opt_uri instanceof goog.Uri) {
70 this.ignoreCase_ = goog.isDef(opt_ignoreCase) ?
71 opt_ignoreCase : opt_uri.getIgnoreCase();
72 this.setScheme(opt_uri.getScheme());
73 this.setUserInfo(opt_uri.getUserInfo());
74 this.setDomain(opt_uri.getDomain());
75 this.setPort(opt_uri.getPort());
76 this.setPath(opt_uri.getPath());
77 this.setQueryData(opt_uri.getQueryData().clone());
78 this.setFragment(opt_uri.getFragment());
79 } else if (opt_uri && (m = goog.uri.utils.split(String(opt_uri)))) {
80 this.ignoreCase_ = !!opt_ignoreCase;
81
82 // Set the parts -- decoding as we do so.
83 // COMPATABILITY NOTE - In IE, unmatched fields may be empty strings,
84 // whereas in other browsers they will be undefined.
85 this.setScheme(m[goog.uri.utils.ComponentIndex.SCHEME] || '', true);
86 this.setUserInfo(m[goog.uri.utils.ComponentIndex.USER_INFO] || '', true);
87 this.setDomain(m[goog.uri.utils.ComponentIndex.DOMAIN] || '', true);
88 this.setPort(m[goog.uri.utils.ComponentIndex.PORT]);
89 this.setPath(m[goog.uri.utils.ComponentIndex.PATH] || '', true);
90 this.setQueryData(m[goog.uri.utils.ComponentIndex.QUERY_DATA] || '', true);
91 this.setFragment(m[goog.uri.utils.ComponentIndex.FRAGMENT] || '', true);
92
93 } else {
94 this.ignoreCase_ = !!opt_ignoreCase;
95 this.queryData_ = new goog.Uri.QueryData(null, null, this.ignoreCase_);
96 }
97};
98
99
100/**
101 * If true, we preserve the type of query parameters set programmatically.
102 *
103 * This means that if you set a parameter to a boolean, and then call
104 * getParameterValue, you will get a boolean back.
105 *
106 * If false, we will coerce parameters to strings, just as they would
107 * appear in real URIs.
108 *
109 * TODO(nicksantos): Remove this once people have time to fix all tests.
110 *
111 * @type {boolean}
112 */
113goog.Uri.preserveParameterTypesCompatibilityFlag = false;
114
115
116/**
117 * Parameter name added to stop caching.
118 * @type {string}
119 */
120goog.Uri.RANDOM_PARAM = goog.uri.utils.StandardQueryParam.RANDOM;
121
122
123/**
124 * Scheme such as "http".
125 * @type {string}
126 * @private
127 */
128goog.Uri.prototype.scheme_ = '';
129
130
131/**
132 * User credentials in the form "username:password".
133 * @type {string}
134 * @private
135 */
136goog.Uri.prototype.userInfo_ = '';
137
138
139/**
140 * Domain part, e.g. "www.google.com".
141 * @type {string}
142 * @private
143 */
144goog.Uri.prototype.domain_ = '';
145
146
147/**
148 * Port, e.g. 8080.
149 * @type {?number}
150 * @private
151 */
152goog.Uri.prototype.port_ = null;
153
154
155/**
156 * Path, e.g. "/tests/img.png".
157 * @type {string}
158 * @private
159 */
160goog.Uri.prototype.path_ = '';
161
162
163/**
164 * Object representing query data.
165 * @type {!goog.Uri.QueryData}
166 * @private
167 */
168goog.Uri.prototype.queryData_;
169
170
171/**
172 * The fragment without the #.
173 * @type {string}
174 * @private
175 */
176goog.Uri.prototype.fragment_ = '';
177
178
179/**
180 * Whether or not this Uri should be treated as Read Only.
181 * @type {boolean}
182 * @private
183 */
184goog.Uri.prototype.isReadOnly_ = false;
185
186
187/**
188 * Whether or not to ignore case when comparing query params.
189 * @type {boolean}
190 * @private
191 */
192goog.Uri.prototype.ignoreCase_ = false;
193
194
195/**
196 * @return {string} The string form of the url.
197 * @override
198 */
199goog.Uri.prototype.toString = function() {
200 var out = [];
201
202 var scheme = this.getScheme();
203 if (scheme) {
204 out.push(goog.Uri.encodeSpecialChars_(
205 scheme, goog.Uri.reDisallowedInSchemeOrUserInfo_), ':');
206 }
207
208 var domain = this.getDomain();
209 if (domain) {
210 out.push('//');
211
212 var userInfo = this.getUserInfo();
213 if (userInfo) {
214 out.push(goog.Uri.encodeSpecialChars_(
215 userInfo, goog.Uri.reDisallowedInSchemeOrUserInfo_), '@');
216 }
217
218 out.push(goog.string.urlEncode(domain));
219
220 var port = this.getPort();
221 if (port != null) {
222 out.push(':', String(port));
223 }
224 }
225
226 var path = this.getPath();
227 if (path) {
228 if (this.hasDomain() && path.charAt(0) != '/') {
229 out.push('/');
230 }
231 out.push(goog.Uri.encodeSpecialChars_(
232 path,
233 path.charAt(0) == '/' ?
234 goog.Uri.reDisallowedInAbsolutePath_ :
235 goog.Uri.reDisallowedInRelativePath_));
236 }
237
238 var query = this.getEncodedQuery();
239 if (query) {
240 out.push('?', query);
241 }
242
243 var fragment = this.getFragment();
244 if (fragment) {
245 out.push('#', goog.Uri.encodeSpecialChars_(
246 fragment, goog.Uri.reDisallowedInFragment_));
247 }
248 return out.join('');
249};
250
251
252/**
253 * Resolves the given relative URI (a goog.Uri object), using the URI
254 * represented by this instance as the base URI.
255 *
256 * There are several kinds of relative URIs:<br>
257 * 1. foo - replaces the last part of the path, the whole query and fragment<br>
258 * 2. /foo - replaces the the path, the query and fragment<br>
259 * 3. //foo - replaces everything from the domain on. foo is a domain name<br>
260 * 4. ?foo - replace the query and fragment<br>
261 * 5. #foo - replace the fragment only
262 *
263 * Additionally, if relative URI has a non-empty path, all ".." and "."
264 * segments will be resolved, as described in RFC 3986.
265 *
266 * @param {goog.Uri} relativeUri The relative URI to resolve.
267 * @return {!goog.Uri} The resolved URI.
268 */
269goog.Uri.prototype.resolve = function(relativeUri) {
270
271 var absoluteUri = this.clone();
272
273 // we satisfy these conditions by looking for the first part of relativeUri
274 // that is not blank and applying defaults to the rest
275
276 var overridden = relativeUri.hasScheme();
277
278 if (overridden) {
279 absoluteUri.setScheme(relativeUri.getScheme());
280 } else {
281 overridden = relativeUri.hasUserInfo();
282 }
283
284 if (overridden) {
285 absoluteUri.setUserInfo(relativeUri.getUserInfo());
286 } else {
287 overridden = relativeUri.hasDomain();
288 }
289
290 if (overridden) {
291 absoluteUri.setDomain(relativeUri.getDomain());
292 } else {
293 overridden = relativeUri.hasPort();
294 }
295
296 var path = relativeUri.getPath();
297 if (overridden) {
298 absoluteUri.setPort(relativeUri.getPort());
299 } else {
300 overridden = relativeUri.hasPath();
301 if (overridden) {
302 // resolve path properly
303 if (path.charAt(0) != '/') {
304 // path is relative
305 if (this.hasDomain() && !this.hasPath()) {
306 // RFC 3986, section 5.2.3, case 1
307 path = '/' + path;
308 } else {
309 // RFC 3986, section 5.2.3, case 2
310 var lastSlashIndex = absoluteUri.getPath().lastIndexOf('/');
311 if (lastSlashIndex != -1) {
312 path = absoluteUri.getPath().substr(0, lastSlashIndex + 1) + path;
313 }
314 }
315 }
316 path = goog.Uri.removeDotSegments(path);
317 }
318 }
319
320 if (overridden) {
321 absoluteUri.setPath(path);
322 } else {
323 overridden = relativeUri.hasQuery();
324 }
325
326 if (overridden) {
327 absoluteUri.setQueryData(relativeUri.getDecodedQuery());
328 } else {
329 overridden = relativeUri.hasFragment();
330 }
331
332 if (overridden) {
333 absoluteUri.setFragment(relativeUri.getFragment());
334 }
335
336 return absoluteUri;
337};
338
339
340/**
341 * Clones the URI instance.
342 * @return {!goog.Uri} New instance of the URI objcet.
343 */
344goog.Uri.prototype.clone = function() {
345 return new goog.Uri(this);
346};
347
348
349/**
350 * @return {string} The encoded scheme/protocol for the URI.
351 */
352goog.Uri.prototype.getScheme = function() {
353 return this.scheme_;
354};
355
356
357/**
358 * Sets the scheme/protocol.
359 * @param {string} newScheme New scheme value.
360 * @param {boolean=} opt_decode Optional param for whether to decode new value.
361 * @return {!goog.Uri} Reference to this URI object.
362 */
363goog.Uri.prototype.setScheme = function(newScheme, opt_decode) {
364 this.enforceReadOnly();
365 this.scheme_ = opt_decode ? goog.Uri.decodeOrEmpty_(newScheme) : newScheme;
366
367 // remove an : at the end of the scheme so somebody can pass in
368 // window.location.protocol
369 if (this.scheme_) {
370 this.scheme_ = this.scheme_.replace(/:$/, '');
371 }
372 return this;
373};
374
375
376/**
377 * @return {boolean} Whether the scheme has been set.
378 */
379goog.Uri.prototype.hasScheme = function() {
380 return !!this.scheme_;
381};
382
383
384/**
385 * @return {string} The decoded user info.
386 */
387goog.Uri.prototype.getUserInfo = function() {
388 return this.userInfo_;
389};
390
391
392/**
393 * Sets the userInfo.
394 * @param {string} newUserInfo New userInfo value.
395 * @param {boolean=} opt_decode Optional param for whether to decode new value.
396 * @return {!goog.Uri} Reference to this URI object.
397 */
398goog.Uri.prototype.setUserInfo = function(newUserInfo, opt_decode) {
399 this.enforceReadOnly();
400 this.userInfo_ = opt_decode ? goog.Uri.decodeOrEmpty_(newUserInfo) :
401 newUserInfo;
402 return this;
403};
404
405
406/**
407 * @return {boolean} Whether the user info has been set.
408 */
409goog.Uri.prototype.hasUserInfo = function() {
410 return !!this.userInfo_;
411};
412
413
414/**
415 * @return {string} The decoded domain.
416 */
417goog.Uri.prototype.getDomain = function() {
418 return this.domain_;
419};
420
421
422/**
423 * Sets the domain.
424 * @param {string} newDomain New domain value.
425 * @param {boolean=} opt_decode Optional param for whether to decode new value.
426 * @return {!goog.Uri} Reference to this URI object.
427 */
428goog.Uri.prototype.setDomain = function(newDomain, opt_decode) {
429 this.enforceReadOnly();
430 this.domain_ = opt_decode ? goog.Uri.decodeOrEmpty_(newDomain) : newDomain;
431 return this;
432};
433
434
435/**
436 * @return {boolean} Whether the domain has been set.
437 */
438goog.Uri.prototype.hasDomain = function() {
439 return !!this.domain_;
440};
441
442
443/**
444 * @return {?number} The port number.
445 */
446goog.Uri.prototype.getPort = function() {
447 return this.port_;
448};
449
450
451/**
452 * Sets the port number.
453 * @param {*} newPort Port number. Will be explicitly casted to a number.
454 * @return {!goog.Uri} Reference to this URI object.
455 */
456goog.Uri.prototype.setPort = function(newPort) {
457 this.enforceReadOnly();
458
459 if (newPort) {
460 newPort = Number(newPort);
461 if (isNaN(newPort) || newPort < 0) {
462 throw Error('Bad port number ' + newPort);
463 }
464 this.port_ = newPort;
465 } else {
466 this.port_ = null;
467 }
468
469 return this;
470};
471
472
473/**
474 * @return {boolean} Whether the port has been set.
475 */
476goog.Uri.prototype.hasPort = function() {
477 return this.port_ != null;
478};
479
480
481/**
482 * @return {string} The decoded path.
483 */
484goog.Uri.prototype.getPath = function() {
485 return this.path_;
486};
487
488
489/**
490 * Sets the path.
491 * @param {string} newPath New path value.
492 * @param {boolean=} opt_decode Optional param for whether to decode new value.
493 * @return {!goog.Uri} Reference to this URI object.
494 */
495goog.Uri.prototype.setPath = function(newPath, opt_decode) {
496 this.enforceReadOnly();
497 this.path_ = opt_decode ? goog.Uri.decodeOrEmpty_(newPath) : newPath;
498 return this;
499};
500
501
502/**
503 * @return {boolean} Whether the path has been set.
504 */
505goog.Uri.prototype.hasPath = function() {
506 return !!this.path_;
507};
508
509
510/**
511 * @return {boolean} Whether the query string has been set.
512 */
513goog.Uri.prototype.hasQuery = function() {
514 return this.queryData_.toString() !== '';
515};
516
517
518/**
519 * Sets the query data.
520 * @param {goog.Uri.QueryData|string|undefined} queryData QueryData object.
521 * @param {boolean=} opt_decode Optional param for whether to decode new value.
522 * Applies only if queryData is a string.
523 * @return {!goog.Uri} Reference to this URI object.
524 */
525goog.Uri.prototype.setQueryData = function(queryData, opt_decode) {
526 this.enforceReadOnly();
527
528 if (queryData instanceof goog.Uri.QueryData) {
529 this.queryData_ = queryData;
530 this.queryData_.setIgnoreCase(this.ignoreCase_);
531 } else {
532 if (!opt_decode) {
533 // QueryData accepts encoded query string, so encode it if
534 // opt_decode flag is not true.
535 queryData = goog.Uri.encodeSpecialChars_(queryData,
536 goog.Uri.reDisallowedInQuery_);
537 }
538 this.queryData_ = new goog.Uri.QueryData(queryData, null, this.ignoreCase_);
539 }
540
541 return this;
542};
543
544
545/**
546 * Sets the URI query.
547 * @param {string} newQuery New query value.
548 * @param {boolean=} opt_decode Optional param for whether to decode new value.
549 * @return {!goog.Uri} Reference to this URI object.
550 */
551goog.Uri.prototype.setQuery = function(newQuery, opt_decode) {
552 return this.setQueryData(newQuery, opt_decode);
553};
554
555
556/**
557 * @return {string} The encoded URI query, not including the ?.
558 */
559goog.Uri.prototype.getEncodedQuery = function() {
560 return this.queryData_.toString();
561};
562
563
564/**
565 * @return {string} The decoded URI query, not including the ?.
566 */
567goog.Uri.prototype.getDecodedQuery = function() {
568 return this.queryData_.toDecodedString();
569};
570
571
572/**
573 * Returns the query data.
574 * @return {goog.Uri.QueryData} QueryData object.
575 */
576goog.Uri.prototype.getQueryData = function() {
577 return this.queryData_;
578};
579
580
581/**
582 * @return {string} The encoded URI query, not including the ?.
583 *
584 * Warning: This method, unlike other getter methods, returns encoded
585 * value, instead of decoded one.
586 */
587goog.Uri.prototype.getQuery = function() {
588 return this.getEncodedQuery();
589};
590
591
592/**
593 * Sets the value of the named query parameters, clearing previous values for
594 * that key.
595 *
596 * @param {string} key The parameter to set.
597 * @param {*} value The new value.
598 * @return {!goog.Uri} Reference to this URI object.
599 */
600goog.Uri.prototype.setParameterValue = function(key, value) {
601 this.enforceReadOnly();
602 this.queryData_.set(key, value);
603 return this;
604};
605
606
607/**
608 * Sets the values of the named query parameters, clearing previous values for
609 * that key. Not new values will currently be moved to the end of the query
610 * string.
611 *
612 * So, <code>goog.Uri.parse('foo?a=b&c=d&e=f').setParameterValues('c', ['new'])
613 * </code> yields <tt>foo?a=b&e=f&c=new</tt>.</p>
614 *
615 * @param {string} key The parameter to set.
616 * @param {*} values The new values. If values is a single
617 * string then it will be treated as the sole value.
618 * @return {!goog.Uri} Reference to this URI object.
619 */
620goog.Uri.prototype.setParameterValues = function(key, values) {
621 this.enforceReadOnly();
622
623 if (!goog.isArray(values)) {
624 values = [String(values)];
625 }
626
627 // TODO(nicksantos): This cast shouldn't be necessary.
628 this.queryData_.setValues(key, /** @type {Array} */ (values));
629
630 return this;
631};
632
633
634/**
635 * Returns the value<b>s</b> for a given cgi parameter as a list of decoded
636 * query parameter values.
637 * @param {string} name The parameter to get values for.
638 * @return {Array} The values for a given cgi parameter as a list of
639 * decoded query parameter values.
640 */
641goog.Uri.prototype.getParameterValues = function(name) {
642 return this.queryData_.getValues(name);
643};
644
645
646/**
647 * Returns the first value for a given cgi parameter or undefined if the given
648 * parameter name does not appear in the query string.
649 * @param {string} paramName Unescaped parameter name.
650 * @return {string|undefined} The first value for a given cgi parameter or
651 * undefined if the given parameter name does not appear in the query
652 * string.
653 */
654goog.Uri.prototype.getParameterValue = function(paramName) {
655 // NOTE(nicksantos): This type-cast is a lie when
656 // preserveParameterTypesCompatibilityFlag is set to true.
657 // But this should only be set to true in tests.
658 return /** @type {string|undefined} */ (this.queryData_.get(paramName));
659};
660
661
662/**
663 * @return {string} The URI fragment, not including the #.
664 */
665goog.Uri.prototype.getFragment = function() {
666 return this.fragment_;
667};
668
669
670/**
671 * Sets the URI fragment.
672 * @param {string} newFragment New fragment value.
673 * @param {boolean=} opt_decode Optional param for whether to decode new value.
674 * @return {!goog.Uri} Reference to this URI object.
675 */
676goog.Uri.prototype.setFragment = function(newFragment, opt_decode) {
677 this.enforceReadOnly();
678 this.fragment_ = opt_decode ? goog.Uri.decodeOrEmpty_(newFragment) :
679 newFragment;
680 return this;
681};
682
683
684/**
685 * @return {boolean} Whether the URI has a fragment set.
686 */
687goog.Uri.prototype.hasFragment = function() {
688 return !!this.fragment_;
689};
690
691
692/**
693 * Returns true if this has the same domain as that of uri2.
694 * @param {goog.Uri} uri2 The URI object to compare to.
695 * @return {boolean} true if same domain; false otherwise.
696 */
697goog.Uri.prototype.hasSameDomainAs = function(uri2) {
698 return ((!this.hasDomain() && !uri2.hasDomain()) ||
699 this.getDomain() == uri2.getDomain()) &&
700 ((!this.hasPort() && !uri2.hasPort()) ||
701 this.getPort() == uri2.getPort());
702};
703
704
705/**
706 * Adds a random parameter to the Uri.
707 * @return {!goog.Uri} Reference to this Uri object.
708 */
709goog.Uri.prototype.makeUnique = function() {
710 this.enforceReadOnly();
711 this.setParameterValue(goog.Uri.RANDOM_PARAM, goog.string.getRandomString());
712
713 return this;
714};
715
716
717/**
718 * Removes the named query parameter.
719 *
720 * @param {string} key The parameter to remove.
721 * @return {!goog.Uri} Reference to this URI object.
722 */
723goog.Uri.prototype.removeParameter = function(key) {
724 this.enforceReadOnly();
725 this.queryData_.remove(key);
726 return this;
727};
728
729
730/**
731 * Sets whether Uri is read only. If this goog.Uri is read-only,
732 * enforceReadOnly_ will be called at the start of any function that may modify
733 * this Uri.
734 * @param {boolean} isReadOnly whether this goog.Uri should be read only.
735 * @return {!goog.Uri} Reference to this Uri object.
736 */
737goog.Uri.prototype.setReadOnly = function(isReadOnly) {
738 this.isReadOnly_ = isReadOnly;
739 return this;
740};
741
742
743/**
744 * @return {boolean} Whether the URI is read only.
745 */
746goog.Uri.prototype.isReadOnly = function() {
747 return this.isReadOnly_;
748};
749
750
751/**
752 * Checks if this Uri has been marked as read only, and if so, throws an error.
753 * This should be called whenever any modifying function is called.
754 */
755goog.Uri.prototype.enforceReadOnly = function() {
756 if (this.isReadOnly_) {
757 throw Error('Tried to modify a read-only Uri');
758 }
759};
760
761
762/**
763 * Sets whether to ignore case.
764 * NOTE: If there are already key/value pairs in the QueryData, and
765 * ignoreCase_ is set to false, the keys will all be lower-cased.
766 * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
767 * @return {!goog.Uri} Reference to this Uri object.
768 */
769goog.Uri.prototype.setIgnoreCase = function(ignoreCase) {
770 this.ignoreCase_ = ignoreCase;
771 if (this.queryData_) {
772 this.queryData_.setIgnoreCase(ignoreCase);
773 }
774 return this;
775};
776
777
778/**
779 * @return {boolean} Whether to ignore case.
780 */
781goog.Uri.prototype.getIgnoreCase = function() {
782 return this.ignoreCase_;
783};
784
785
786//==============================================================================
787// Static members
788//==============================================================================
789
790
791/**
792 * Creates a uri from the string form. Basically an alias of new goog.Uri().
793 * If a Uri object is passed to parse then it will return a clone of the object.
794 *
795 * @param {*} uri Raw URI string or instance of Uri
796 * object.
797 * @param {boolean=} opt_ignoreCase Whether to ignore the case of parameter
798 * names in #getParameterValue.
799 * @return {!goog.Uri} The new URI object.
800 */
801goog.Uri.parse = function(uri, opt_ignoreCase) {
802 return uri instanceof goog.Uri ?
803 uri.clone() : new goog.Uri(uri, opt_ignoreCase);
804};
805
806
807/**
808 * Creates a new goog.Uri object from unencoded parts.
809 *
810 * @param {?string=} opt_scheme Scheme/protocol or full URI to parse.
811 * @param {?string=} opt_userInfo username:password.
812 * @param {?string=} opt_domain www.google.com.
813 * @param {?number=} opt_port 9830.
814 * @param {?string=} opt_path /some/path/to/a/file.html.
815 * @param {string|goog.Uri.QueryData=} opt_query a=1&b=2.
816 * @param {?string=} opt_fragment The fragment without the #.
817 * @param {boolean=} opt_ignoreCase Whether to ignore parameter name case in
818 * #getParameterValue.
819 *
820 * @return {!goog.Uri} The new URI object.
821 */
822goog.Uri.create = function(opt_scheme, opt_userInfo, opt_domain, opt_port,
823 opt_path, opt_query, opt_fragment, opt_ignoreCase) {
824
825 var uri = new goog.Uri(null, opt_ignoreCase);
826
827 // Only set the parts if they are defined and not empty strings.
828 opt_scheme && uri.setScheme(opt_scheme);
829 opt_userInfo && uri.setUserInfo(opt_userInfo);
830 opt_domain && uri.setDomain(opt_domain);
831 opt_port && uri.setPort(opt_port);
832 opt_path && uri.setPath(opt_path);
833 opt_query && uri.setQueryData(opt_query);
834 opt_fragment && uri.setFragment(opt_fragment);
835
836 return uri;
837};
838
839
840/**
841 * Resolves a relative Uri against a base Uri, accepting both strings and
842 * Uri objects.
843 *
844 * @param {*} base Base Uri.
845 * @param {*} rel Relative Uri.
846 * @return {!goog.Uri} Resolved uri.
847 */
848goog.Uri.resolve = function(base, rel) {
849 if (!(base instanceof goog.Uri)) {
850 base = goog.Uri.parse(base);
851 }
852
853 if (!(rel instanceof goog.Uri)) {
854 rel = goog.Uri.parse(rel);
855 }
856
857 return base.resolve(rel);
858};
859
860
861/**
862 * Removes dot segments in given path component, as described in
863 * RFC 3986, section 5.2.4.
864 *
865 * @param {string} path A non-empty path component.
866 * @return {string} Path component with removed dot segments.
867 */
868goog.Uri.removeDotSegments = function(path) {
869 if (path == '..' || path == '.') {
870 return '';
871
872 } else if (!goog.string.contains(path, './') &&
873 !goog.string.contains(path, '/.')) {
874 // This optimization detects uris which do not contain dot-segments,
875 // and as a consequence do not require any processing.
876 return path;
877
878 } else {
879 var leadingSlash = goog.string.startsWith(path, '/');
880 var segments = path.split('/');
881 var out = [];
882
883 for (var pos = 0; pos < segments.length; ) {
884 var segment = segments[pos++];
885
886 if (segment == '.') {
887 if (leadingSlash && pos == segments.length) {
888 out.push('');
889 }
890 } else if (segment == '..') {
891 if (out.length > 1 || out.length == 1 && out[0] != '') {
892 out.pop();
893 }
894 if (leadingSlash && pos == segments.length) {
895 out.push('');
896 }
897 } else {
898 out.push(segment);
899 leadingSlash = true;
900 }
901 }
902
903 return out.join('/');
904 }
905};
906
907
908/**
909 * Decodes a value or returns the empty string if it isn't defined or empty.
910 * @param {string|undefined} val Value to decode.
911 * @return {string} Decoded value.
912 * @private
913 */
914goog.Uri.decodeOrEmpty_ = function(val) {
915 // Don't use UrlDecode() here because val is not a query parameter.
916 return val ? decodeURIComponent(val) : '';
917};
918
919
920/**
921 * If unescapedPart is non null, then escapes any characters in it that aren't
922 * valid characters in a url and also escapes any special characters that
923 * appear in extra.
924 *
925 * @param {*} unescapedPart The string to encode.
926 * @param {RegExp} extra A character set of characters in [\01-\177].
927 * @return {?string} null iff unescapedPart == null.
928 * @private
929 */
930goog.Uri.encodeSpecialChars_ = function(unescapedPart, extra) {
931 if (goog.isString(unescapedPart)) {
932 return encodeURI(unescapedPart).replace(extra, goog.Uri.encodeChar_);
933 }
934 return null;
935};
936
937
938/**
939 * Converts a character in [\01-\177] to its unicode character equivalent.
940 * @param {string} ch One character string.
941 * @return {string} Encoded string.
942 * @private
943 */
944goog.Uri.encodeChar_ = function(ch) {
945 var n = ch.charCodeAt(0);
946 return '%' + ((n >> 4) & 0xf).toString(16) + (n & 0xf).toString(16);
947};
948
949
950/**
951 * Regular expression for characters that are disallowed in the scheme or
952 * userInfo part of the URI.
953 * @type {RegExp}
954 * @private
955 */
956goog.Uri.reDisallowedInSchemeOrUserInfo_ = /[#\/\?@]/g;
957
958
959/**
960 * Regular expression for characters that are disallowed in a relative path.
961 * @type {RegExp}
962 * @private
963 */
964goog.Uri.reDisallowedInRelativePath_ = /[\#\?:]/g;
965
966
967/**
968 * Regular expression for characters that are disallowed in an absolute path.
969 * @type {RegExp}
970 * @private
971 */
972goog.Uri.reDisallowedInAbsolutePath_ = /[\#\?]/g;
973
974
975/**
976 * Regular expression for characters that are disallowed in the query.
977 * @type {RegExp}
978 * @private
979 */
980goog.Uri.reDisallowedInQuery_ = /[\#\?@]/g;
981
982
983/**
984 * Regular expression for characters that are disallowed in the fragment.
985 * @type {RegExp}
986 * @private
987 */
988goog.Uri.reDisallowedInFragment_ = /#/g;
989
990
991/**
992 * Checks whether two URIs have the same domain.
993 * @param {string} uri1String First URI string.
994 * @param {string} uri2String Second URI string.
995 * @return {boolean} true if the two URIs have the same domain; false otherwise.
996 */
997goog.Uri.haveSameDomain = function(uri1String, uri2String) {
998 // Differs from goog.uri.utils.haveSameDomain, since this ignores scheme.
999 // TODO(gboyer): Have this just call goog.uri.util.haveSameDomain.
1000 var pieces1 = goog.uri.utils.split(uri1String);
1001 var pieces2 = goog.uri.utils.split(uri2String);
1002 return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==
1003 pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&
1004 pieces1[goog.uri.utils.ComponentIndex.PORT] ==
1005 pieces2[goog.uri.utils.ComponentIndex.PORT];
1006};
1007
1008
1009
1010/**
1011 * Class used to represent URI query parameters. It is essentially a hash of
1012 * name-value pairs, though a name can be present more than once.
1013 *
1014 * Has the same interface as the collections in goog.structs.
1015 *
1016 * @param {?string=} opt_query Optional encoded query string to parse into
1017 * the object.
1018 * @param {goog.Uri=} opt_uri Optional uri object that should have its
1019 * cache invalidated when this object updates. Deprecated -- this
1020 * is no longer required.
1021 * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
1022 * name in #get.
1023 * @constructor
1024 */
1025goog.Uri.QueryData = function(opt_query, opt_uri, opt_ignoreCase) {
1026 /**
1027 * Encoded query string, or null if it requires computing from the key map.
1028 * @type {?string}
1029 * @private
1030 */
1031 this.encodedQuery_ = opt_query || null;
1032
1033 /**
1034 * If true, ignore the case of the parameter name in #get.
1035 * @type {boolean}
1036 * @private
1037 */
1038 this.ignoreCase_ = !!opt_ignoreCase;
1039};
1040
1041
1042/**
1043 * If the underlying key map is not yet initialized, it parses the
1044 * query string and fills the map with parsed data.
1045 * @private
1046 */
1047goog.Uri.QueryData.prototype.ensureKeyMapInitialized_ = function() {
1048 if (!this.keyMap_) {
1049 this.keyMap_ = new goog.structs.Map();
1050 this.count_ = 0;
1051
1052 if (this.encodedQuery_) {
1053 var pairs = this.encodedQuery_.split('&');
1054 for (var i = 0; i < pairs.length; i++) {
1055 var indexOfEquals = pairs[i].indexOf('=');
1056 var name = null;
1057 var value = null;
1058 if (indexOfEquals >= 0) {
1059 name = pairs[i].substring(0, indexOfEquals);
1060 value = pairs[i].substring(indexOfEquals + 1);
1061 } else {
1062 name = pairs[i];
1063 }
1064 name = goog.string.urlDecode(name);
1065 name = this.getKeyName_(name);
1066 this.add(name, value ? goog.string.urlDecode(value) : '');
1067 }
1068 }
1069 }
1070};
1071
1072
1073/**
1074 * Creates a new query data instance from a map of names and values.
1075 *
1076 * @param {!goog.structs.Map|!Object} map Map of string parameter
1077 * names to parameter value. If parameter value is an array, it is
1078 * treated as if the key maps to each individual value in the
1079 * array.
1080 * @param {goog.Uri=} opt_uri URI object that should have its cache
1081 * invalidated when this object updates.
1082 * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
1083 * name in #get.
1084 * @return {!goog.Uri.QueryData} The populated query data instance.
1085 */
1086goog.Uri.QueryData.createFromMap = function(map, opt_uri, opt_ignoreCase) {
1087 var keys = goog.structs.getKeys(map);
1088 if (typeof keys == 'undefined') {
1089 throw Error('Keys are undefined');
1090 }
1091
1092 var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
1093 var values = goog.structs.getValues(map);
1094 for (var i = 0; i < keys.length; i++) {
1095 var key = keys[i];
1096 var value = values[i];
1097 if (!goog.isArray(value)) {
1098 queryData.add(key, value);
1099 } else {
1100 queryData.setValues(key, value);
1101 }
1102 }
1103 return queryData;
1104};
1105
1106
1107/**
1108 * Creates a new query data instance from parallel arrays of parameter names
1109 * and values. Allows for duplicate parameter names. Throws an error if the
1110 * lengths of the arrays differ.
1111 *
1112 * @param {Array.<string>} keys Parameter names.
1113 * @param {Array} values Parameter values.
1114 * @param {goog.Uri=} opt_uri URI object that should have its cache
1115 * invalidated when this object updates.
1116 * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
1117 * name in #get.
1118 * @return {!goog.Uri.QueryData} The populated query data instance.
1119 */
1120goog.Uri.QueryData.createFromKeysValues = function(
1121 keys, values, opt_uri, opt_ignoreCase) {
1122 if (keys.length != values.length) {
1123 throw Error('Mismatched lengths for keys/values');
1124 }
1125 var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
1126 for (var i = 0; i < keys.length; i++) {
1127 queryData.add(keys[i], values[i]);
1128 }
1129 return queryData;
1130};
1131
1132
1133/**
1134 * The map containing name/value or name/array-of-values pairs.
1135 * May be null if it requires parsing from the query string.
1136 *
1137 * We need to use a Map because we cannot guarantee that the key names will
1138 * not be problematic for IE.
1139 *
1140 * @type {goog.structs.Map}
1141 * @private
1142 */
1143goog.Uri.QueryData.prototype.keyMap_ = null;
1144
1145
1146/**
1147 * The number of params, or null if it requires computing.
1148 * @type {?number}
1149 * @private
1150 */
1151goog.Uri.QueryData.prototype.count_ = null;
1152
1153
1154/**
1155 * @return {?number} The number of parameters.
1156 */
1157goog.Uri.QueryData.prototype.getCount = function() {
1158 this.ensureKeyMapInitialized_();
1159 return this.count_;
1160};
1161
1162
1163/**
1164 * Adds a key value pair.
1165 * @param {string} key Name.
1166 * @param {*} value Value.
1167 * @return {!goog.Uri.QueryData} Instance of this object.
1168 */
1169goog.Uri.QueryData.prototype.add = function(key, value) {
1170 this.ensureKeyMapInitialized_();
1171 this.invalidateCache_();
1172
1173 key = this.getKeyName_(key);
1174 var values = this.keyMap_.get(key);
1175 if (!values) {
1176 this.keyMap_.set(key, (values = []));
1177 }
1178 values.push(value);
1179 this.count_++;
1180 return this;
1181};
1182
1183
1184/**
1185 * Removes all the params with the given key.
1186 * @param {string} key Name.
1187 * @return {boolean} Whether any parameter was removed.
1188 */
1189goog.Uri.QueryData.prototype.remove = function(key) {
1190 this.ensureKeyMapInitialized_();
1191
1192 key = this.getKeyName_(key);
1193 if (this.keyMap_.containsKey(key)) {
1194 this.invalidateCache_();
1195
1196 // Decrement parameter count.
1197 this.count_ -= this.keyMap_.get(key).length;
1198 return this.keyMap_.remove(key);
1199 }
1200 return false;
1201};
1202
1203
1204/**
1205 * Clears the parameters.
1206 */
1207goog.Uri.QueryData.prototype.clear = function() {
1208 this.invalidateCache_();
1209 this.keyMap_ = null;
1210 this.count_ = 0;
1211};
1212
1213
1214/**
1215 * @return {boolean} Whether we have any parameters.
1216 */
1217goog.Uri.QueryData.prototype.isEmpty = function() {
1218 this.ensureKeyMapInitialized_();
1219 return this.count_ == 0;
1220};
1221
1222
1223/**
1224 * Whether there is a parameter with the given name
1225 * @param {string} key The parameter name to check for.
1226 * @return {boolean} Whether there is a parameter with the given name.
1227 */
1228goog.Uri.QueryData.prototype.containsKey = function(key) {
1229 this.ensureKeyMapInitialized_();
1230 key = this.getKeyName_(key);
1231 return this.keyMap_.containsKey(key);
1232};
1233
1234
1235/**
1236 * Whether there is a parameter with the given value.
1237 * @param {*} value The value to check for.
1238 * @return {boolean} Whether there is a parameter with the given value.
1239 */
1240goog.Uri.QueryData.prototype.containsValue = function(value) {
1241 // NOTE(arv): This solution goes through all the params even if it was the
1242 // first param. We can get around this by not reusing code or by switching to
1243 // iterators.
1244 var vals = this.getValues();
1245 return goog.array.contains(vals, value);
1246};
1247
1248
1249/**
1250 * Returns all the keys of the parameters. If a key is used multiple times
1251 * it will be included multiple times in the returned array
1252 * @return {!Array.<string>} All the keys of the parameters.
1253 */
1254goog.Uri.QueryData.prototype.getKeys = function() {
1255 this.ensureKeyMapInitialized_();
1256 // We need to get the values to know how many keys to add.
1257 var vals = /** @type {Array.<Array|*>} */ (this.keyMap_.getValues());
1258 var keys = this.keyMap_.getKeys();
1259 var rv = [];
1260 for (var i = 0; i < keys.length; i++) {
1261 var val = vals[i];
1262 for (var j = 0; j < val.length; j++) {
1263 rv.push(keys[i]);
1264 }
1265 }
1266 return rv;
1267};
1268
1269
1270/**
1271 * Returns all the values of the parameters with the given name. If the query
1272 * data has no such key this will return an empty array. If no key is given
1273 * all values wil be returned.
1274 * @param {string=} opt_key The name of the parameter to get the values for.
1275 * @return {!Array} All the values of the parameters with the given name.
1276 */
1277goog.Uri.QueryData.prototype.getValues = function(opt_key) {
1278 this.ensureKeyMapInitialized_();
1279 var rv = [];
1280 if (opt_key) {
1281 if (this.containsKey(opt_key)) {
1282 rv = goog.array.concat(rv, this.keyMap_.get(this.getKeyName_(opt_key)));
1283 }
1284 } else {
1285 // Return all values.
1286 var values = /** @type {Array.<Array|*>} */ (this.keyMap_.getValues());
1287 for (var i = 0; i < values.length; i++) {
1288 rv = goog.array.concat(rv, values[i]);
1289 }
1290 }
1291 return rv;
1292};
1293
1294
1295/**
1296 * Sets a key value pair and removes all other keys with the same value.
1297 *
1298 * @param {string} key Name.
1299 * @param {*} value Value.
1300 * @return {!goog.Uri.QueryData} Instance of this object.
1301 */
1302goog.Uri.QueryData.prototype.set = function(key, value) {
1303 this.ensureKeyMapInitialized_();
1304 this.invalidateCache_();
1305
1306 // TODO(user): This could be better written as
1307 // this.remove(key), this.add(key, value), but that would reorder
1308 // the key (since the key is first removed and then added at the
1309 // end) and we would have to fix unit tests that depend on key
1310 // ordering.
1311 key = this.getKeyName_(key);
1312 if (this.containsKey(key)) {
1313 this.count_ -= this.keyMap_.get(key).length;
1314 }
1315 this.keyMap_.set(key, [value]);
1316 this.count_++;
1317 return this;
1318};
1319
1320
1321/**
1322 * Returns the first value associated with the key. If the query data has no
1323 * such key this will return undefined or the optional default.
1324 * @param {string} key The name of the parameter to get the value for.
1325 * @param {*=} opt_default The default value to return if the query data
1326 * has no such key.
1327 * @return {*} The first string value associated with the key, or opt_default
1328 * if there's no value.
1329 */
1330goog.Uri.QueryData.prototype.get = function(key, opt_default) {
1331 var values = key ? this.getValues(key) : [];
1332 if (goog.Uri.preserveParameterTypesCompatibilityFlag) {
1333 return values.length > 0 ? values[0] : opt_default;
1334 } else {
1335 return values.length > 0 ? String(values[0]) : opt_default;
1336 }
1337};
1338
1339
1340/**
1341 * Sets the values for a key. If the key already exists, this will
1342 * override all of the existing values that correspond to the key.
1343 * @param {string} key The key to set values for.
1344 * @param {Array} values The values to set.
1345 */
1346goog.Uri.QueryData.prototype.setValues = function(key, values) {
1347 this.remove(key);
1348
1349 if (values.length > 0) {
1350 this.invalidateCache_();
1351 this.keyMap_.set(this.getKeyName_(key), goog.array.clone(values));
1352 this.count_ += values.length;
1353 }
1354};
1355
1356
1357/**
1358 * @return {string} Encoded query string.
1359 * @override
1360 */
1361goog.Uri.QueryData.prototype.toString = function() {
1362 if (this.encodedQuery_) {
1363 return this.encodedQuery_;
1364 }
1365
1366 if (!this.keyMap_) {
1367 return '';
1368 }
1369
1370 var sb = [];
1371
1372 // In the past, we use this.getKeys() and this.getVals(), but that
1373 // generates a lot of allocations as compared to simply iterating
1374 // over the keys.
1375 var keys = this.keyMap_.getKeys();
1376 for (var i = 0; i < keys.length; i++) {
1377 var key = keys[i];
1378 var encodedKey = goog.string.urlEncode(key);
1379 var val = this.getValues(key);
1380 for (var j = 0; j < val.length; j++) {
1381 var param = encodedKey;
1382 // Ensure that null and undefined are encoded into the url as
1383 // literal strings.
1384 if (val[j] !== '') {
1385 param += '=' + goog.string.urlEncode(val[j]);
1386 }
1387 sb.push(param);
1388 }
1389 }
1390
1391 return this.encodedQuery_ = sb.join('&');
1392};
1393
1394
1395/**
1396 * @return {string} Decoded query string.
1397 */
1398goog.Uri.QueryData.prototype.toDecodedString = function() {
1399 return goog.Uri.decodeOrEmpty_(this.toString());
1400};
1401
1402
1403/**
1404 * Invalidate the cache.
1405 * @private
1406 */
1407goog.Uri.QueryData.prototype.invalidateCache_ = function() {
1408 this.encodedQuery_ = null;
1409};
1410
1411
1412/**
1413 * Removes all keys that are not in the provided list. (Modifies this object.)
1414 * @param {Array.<string>} keys The desired keys.
1415 * @return {!goog.Uri.QueryData} a reference to this object.
1416 */
1417goog.Uri.QueryData.prototype.filterKeys = function(keys) {
1418 this.ensureKeyMapInitialized_();
1419 goog.structs.forEach(this.keyMap_,
1420 /** @this {goog.Uri.QueryData} */
1421 function(value, key, map) {
1422 if (!goog.array.contains(keys, key)) {
1423 this.remove(key);
1424 }
1425 }, this);
1426 return this;
1427};
1428
1429
1430/**
1431 * Clone the query data instance.
1432 * @return {!goog.Uri.QueryData} New instance of the QueryData object.
1433 */
1434goog.Uri.QueryData.prototype.clone = function() {
1435 var rv = new goog.Uri.QueryData();
1436 rv.encodedQuery_ = this.encodedQuery_;
1437 if (this.keyMap_) {
1438 rv.keyMap_ = this.keyMap_.clone();
1439 rv.count_ = this.count_;
1440 }
1441 return rv;
1442};
1443
1444
1445/**
1446 * Helper function to get the key name from a JavaScript object. Converts
1447 * the object to a string, and to lower case if necessary.
1448 * @private
1449 * @param {*} arg The object to get a key name from.
1450 * @return {string} valid key name which can be looked up in #keyMap_.
1451 */
1452goog.Uri.QueryData.prototype.getKeyName_ = function(arg) {
1453 var keyName = String(arg);
1454 if (this.ignoreCase_) {
1455 keyName = keyName.toLowerCase();
1456 }
1457 return keyName;
1458};
1459
1460
1461/**
1462 * Ignore case in parameter names.
1463 * NOTE: If there are already key/value pairs in the QueryData, and
1464 * ignoreCase_ is set to false, the keys will all be lower-cased.
1465 * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
1466 */
1467goog.Uri.QueryData.prototype.setIgnoreCase = function(ignoreCase) {
1468 var resetKeys = ignoreCase && !this.ignoreCase_;
1469 if (resetKeys) {
1470 this.ensureKeyMapInitialized_();
1471 this.invalidateCache_();
1472 goog.structs.forEach(this.keyMap_,
1473 /** @this {goog.Uri.QueryData} */
1474 function(value, key) {
1475 var lowerCase = key.toLowerCase();
1476 if (key != lowerCase) {
1477 this.remove(key);
1478 this.setValues(lowerCase, value);
1479 }
1480 }, this);
1481 }
1482 this.ignoreCase_ = ignoreCase;
1483};
1484
1485
1486/**
1487 * Extends a query data object with another query data or map like object. This
1488 * operates 'in-place', it does not create a new QueryData object.
1489 *
1490 * @param {...(goog.Uri.QueryData|goog.structs.Map|Object)} var_args The object
1491 * from which key value pairs will be copied.
1492 */
1493goog.Uri.QueryData.prototype.extend = function(var_args) {
1494 for (var i = 0; i < arguments.length; i++) {
1495 var data = arguments[i];
1496 goog.structs.forEach(data,
1497 /** @this {goog.Uri.QueryData} */
1498 function(value, key) {
1499 this.add(key, value);
1500 }, this);
1501 }
1502};