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