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