Path: blob/trunk/third_party/closure/goog/uri/utils.js
4505 views
/**1* @license2* Copyright The Closure Library Authors.3* SPDX-License-Identifier: Apache-2.04*/56/**7* @fileoverview Simple utilities for dealing with URI strings.8*9* This package is deprecated in favour of the Closure URL package (goog.url)10* when manipulating URIs for use by a browser. This package uses regular11* expressions to parse a potential URI which can fall out of sync with how a12* browser will actually interpret the URI. See13* `goog.uri.utils.setUrlPackageSupportLoggingHandler` for one way to identify14* URIs that should instead be parsed using the URL package.15*16* This is intended to be a lightweight alternative to constructing goog.Uri17* objects. Whereas goog.Uri adds several kilobytes to the binary regardless18* of how much of its functionality you use, this is designed to be a set of19* mostly-independent utilities so that the compiler includes only what is20* necessary for the task. Estimated savings of porting is 5k pre-gzip and21* 1.5k post-gzip. To ensure the savings remain, future developers should22* avoid adding new functionality to existing functions, but instead create23* new ones and factor out shared code.24*25* Many of these utilities have limited functionality, tailored to common26* cases. The query parameter utilities assume that the parameter keys are27* already encoded, since most keys are compile-time alphanumeric strings. The28* query parameter mutation utilities also do not tolerate fragment identifiers.29*30* By design, these functions can be slower than goog.Uri equivalents.31* Repeated calls to some of functions may be quadratic in behavior for IE,32* although the effect is somewhat limited given the 2kb limit.33*34* One advantage of the limited functionality here is that this approach is35* less sensitive to differences in URI encodings than goog.Uri, since these36* functions operate on strings directly, rather than decoding them and37* then re-encoding.38*39* Uses features of RFC 3986 for parsing/formatting URIs:40* http://www.ietf.org/rfc/rfc3986.txt41*/4243goog.provide('goog.uri.utils');44goog.provide('goog.uri.utils.ComponentIndex');45goog.provide('goog.uri.utils.QueryArray');46goog.provide('goog.uri.utils.QueryValue');47goog.provide('goog.uri.utils.StandardQueryParam');4849goog.require('goog.asserts');50goog.require('goog.string');515253/**54* Character codes inlined to avoid object allocations due to charCode.55* @enum {number}56* @private57*/58goog.uri.utils.CharCode_ = {59AMPERSAND: 38,60EQUAL: 61,61HASH: 35,62QUESTION: 6363};646566/**67* Builds a URI string from already-encoded parts.68*69* No encoding is performed. Any component may be omitted as either null or70* undefined.71*72* @param {?string=} opt_scheme The scheme such as 'http'.73* @param {?string=} opt_userInfo The user name before the '@'.74* @param {?string=} opt_domain The domain such as 'www.google.com', already75* URI-encoded.76* @param {(string|number|null)=} opt_port The port number.77* @param {?string=} opt_path The path, already URI-encoded. If it is not78* empty, it must begin with a slash.79* @param {?string=} opt_queryData The URI-encoded query data.80* @param {?string=} opt_fragment The URI-encoded fragment identifier.81* @return {string} The fully combined URI.82*/83goog.uri.utils.buildFromEncodedParts = function(84opt_scheme, opt_userInfo, opt_domain, opt_port, opt_path, opt_queryData,85opt_fragment) {86'use strict';87var out = '';8889if (opt_scheme) {90out += opt_scheme + ':';91}9293if (opt_domain) {94out += '//';9596if (opt_userInfo) {97out += opt_userInfo + '@';98}99100out += opt_domain;101102if (opt_port) {103out += ':' + opt_port;104}105}106107if (opt_path) {108out += opt_path;109}110111if (opt_queryData) {112out += '?' + opt_queryData;113}114115if (opt_fragment) {116out += '#' + opt_fragment;117}118119return out;120};121122123/**124* A regular expression for breaking a URI into its component parts.125*126* {@link http://www.ietf.org/rfc/rfc3986.txt} says in Appendix B127* As the "first-match-wins" algorithm is identical to the "greedy"128* disambiguation method used by POSIX regular expressions, it is natural and129* commonplace to use a regular expression for parsing the potential five130* components of a URI reference.131*132* The following line is the regular expression for breaking-down a133* well-formed URI reference into its components.134*135* <pre>136* ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?137* 12 3 4 5 6 7 8 9138* </pre>139*140* The numbers in the second line above are only to assist readability; they141* indicate the reference points for each subexpression (i.e., each paired142* parenthesis). We refer to the value matched for subexpression <n> as $<n>.143* For example, matching the above expression to144* <pre>145* http://www.ics.uci.edu/pub/ietf/uri/#Related146* </pre>147* results in the following subexpression matches:148* <pre>149* $1 = http:150* $2 = http151* $3 = //www.ics.uci.edu152* $4 = www.ics.uci.edu153* $5 = /pub/ietf/uri/154* $6 = <undefined>155* $7 = <undefined>156* $8 = #Related157* $9 = Related158* </pre>159* where <undefined> indicates that the component is not present, as is the160* case for the query component in the above example. Therefore, we can161* determine the value of the five components as162* <pre>163* scheme = $2164* authority = $4165* path = $5166* query = $7167* fragment = $9168* </pre>169*170* The regular expression has been modified slightly to expose the171* userInfo, domain, and port separately from the authority.172* The modified version yields173* <pre>174* $1 = http scheme175* $2 = <undefined> userInfo -\176* $3 = www.ics.uci.edu domain | authority177* $4 = <undefined> port -/178* $5 = /pub/ietf/uri/ path179* $6 = <undefined> query without ?180* $7 = Related fragment without #181* </pre>182*183* TODO(user): separate out the authority terminating characters once this184* file is moved to ES6.185* @type {!RegExp}186* @private187*/188goog.uri.utils.splitRe_ = new RegExp(189'^' + // Anchor against the entire string.190'(?:' +191'([^:/?#.]+)' + // scheme - ignore special characters192// used by other URL parts such as :,193// ?, /, #, and .194':)?' +195'(?://' +196'(?:([^\\\\/?#]*)@)?' + // userInfo197'([^\\\\/?#]*?)' + // domain198'(?::([0-9]+))?' + // port199'(?=[\\\\/?#]|$)' + // authority-terminating character.200')?' +201'([^?#]+)?' + // path202'(?:\\?([^#]*))?' + // query203'(?:#([\\s\\S]*))?' + // fragment. Can't use '.*' with 's' flag as Firefox204// doesn't support the flag, and can't use an205// "everything set" ([^]) as IE10 doesn't match any206// characters with it.207'$');208209210/**211* The index of each URI component in the return value of goog.uri.utils.split.212* @enum {number}213*/214goog.uri.utils.ComponentIndex = {215SCHEME: 1,216USER_INFO: 2,217DOMAIN: 3,218PORT: 4,219PATH: 5,220QUERY_DATA: 6,221FRAGMENT: 7222};223224/**225* @type {?function(string)}226* @private227*/228goog.uri.utils.urlPackageSupportLoggingHandler_ = null;229230/**231* @param {?function(string)} handler The handler function to call when a URI232* with a protocol that is better supported by the Closure URL package is233* detected.234*/235goog.uri.utils.setUrlPackageSupportLoggingHandler = function(handler) {236'use strict';237goog.uri.utils.urlPackageSupportLoggingHandler_ = handler;238};239240/**241* Splits a URI into its component parts.242*243* Each component can be accessed via the component indices; for example:244* <pre>245* goog.uri.utils.split(someStr)[goog.uri.utils.ComponentIndex.QUERY_DATA];246* </pre>247*248* @param {string} uri The URI string to examine.249* @return {!Array<string|undefined>} Each component still URI-encoded.250* Each component that is present will contain the encoded value, whereas251* components that are not present will be undefined or empty, depending252* on the browser's regular expression implementation. Never null, since253* arbitrary strings may still look like path names.254*/255goog.uri.utils.split = function(uri) {256'use strict';257// See @return comment -- never null.258var result = /** @type {!Array<string|undefined>} */ (259uri.match(goog.uri.utils.splitRe_));260if (goog.uri.utils.urlPackageSupportLoggingHandler_ &&261['http', 'https', 'ws', 'wss',262'ftp'].indexOf(result[goog.uri.utils.ComponentIndex.SCHEME]) >= 0) {263goog.uri.utils.urlPackageSupportLoggingHandler_(uri);264}265return result;266};267268269/**270* @param {?string} uri A possibly null string.271* @param {boolean=} opt_preserveReserved If true, percent-encoding of RFC-3986272* reserved characters will not be removed.273* @return {?string} The string URI-decoded, or null if uri is null.274* @private275*/276goog.uri.utils.decodeIfPossible_ = function(uri, opt_preserveReserved) {277'use strict';278if (!uri) {279return uri;280}281282return opt_preserveReserved ? decodeURI(uri) : decodeURIComponent(uri);283};284285286/**287* Gets a URI component by index.288*289* It is preferred to use the getPathEncoded() variety of functions ahead,290* since they are more readable.291*292* @param {goog.uri.utils.ComponentIndex} componentIndex The component index.293* @param {string} uri The URI to examine.294* @return {?string} The still-encoded component, or null if the component295* is not present.296* @private297*/298goog.uri.utils.getComponentByIndex_ = function(componentIndex, uri) {299'use strict';300// Convert undefined, null, and empty string into null.301return goog.uri.utils.split(uri)[componentIndex] || null;302};303304305/**306* @param {string} uri The URI to examine.307* @return {?string} The protocol or scheme, or null if none. Does not308* include trailing colons or slashes.309*/310goog.uri.utils.getScheme = function(uri) {311'use strict';312return goog.uri.utils.getComponentByIndex_(313goog.uri.utils.ComponentIndex.SCHEME, uri);314};315316317/**318* Gets the effective scheme for the URL. If the URL is relative then the319* scheme is derived from the page's location.320* @param {string} uri The URI to examine.321* @return {string} The protocol or scheme, always lower case.322*/323goog.uri.utils.getEffectiveScheme = function(uri) {324'use strict';325var scheme = goog.uri.utils.getScheme(uri);326if (!scheme && goog.global.self && goog.global.self.location) {327var protocol = goog.global.self.location.protocol;328scheme = protocol.slice(0, -1);329}330// NOTE: When called from a web worker in Firefox 3.5, location may be null.331// All other browsers with web workers support self.location from the worker.332return scheme ? scheme.toLowerCase() : '';333};334335336/**337* @param {string} uri The URI to examine.338* @return {?string} The user name still encoded, or null if none.339*/340goog.uri.utils.getUserInfoEncoded = function(uri) {341'use strict';342return goog.uri.utils.getComponentByIndex_(343goog.uri.utils.ComponentIndex.USER_INFO, uri);344};345346347/**348* @param {string} uri The URI to examine.349* @return {?string} The decoded user info, or null if none.350*/351goog.uri.utils.getUserInfo = function(uri) {352'use strict';353return goog.uri.utils.decodeIfPossible_(354goog.uri.utils.getUserInfoEncoded(uri));355};356357358/**359* @param {string} uri The URI to examine.360* @return {?string} The domain name still encoded, or null if none.361*/362goog.uri.utils.getDomainEncoded = function(uri) {363'use strict';364return goog.uri.utils.getComponentByIndex_(365goog.uri.utils.ComponentIndex.DOMAIN, uri);366};367368369/**370* @param {string} uri The URI to examine.371* @return {?string} The decoded domain, or null if none.372*/373goog.uri.utils.getDomain = function(uri) {374'use strict';375return goog.uri.utils.decodeIfPossible_(376goog.uri.utils.getDomainEncoded(uri), true /* opt_preserveReserved */);377};378379380/**381* @param {string} uri The URI to examine.382* @return {?number} The port number, or null if none.383*/384goog.uri.utils.getPort = function(uri) {385'use strict';386// Coerce to a number. If the result of getComponentByIndex_ is null or387// non-numeric, the number coersion yields NaN. This will then return388// null for all non-numeric cases (though also zero, which isn't a relevant389// port number).390return Number(391goog.uri.utils.getComponentByIndex_(392goog.uri.utils.ComponentIndex.PORT, uri)) ||393null;394};395396397/**398* @param {string} uri The URI to examine.399* @return {?string} The path still encoded, or null if none. Includes the400* leading slash, if any.401*/402goog.uri.utils.getPathEncoded = function(uri) {403'use strict';404return goog.uri.utils.getComponentByIndex_(405goog.uri.utils.ComponentIndex.PATH, uri);406};407408409/**410* @param {string} uri The URI to examine.411* @return {?string} The decoded path, or null if none. Includes the leading412* slash, if any.413*/414goog.uri.utils.getPath = function(uri) {415'use strict';416return goog.uri.utils.decodeIfPossible_(417goog.uri.utils.getPathEncoded(uri), true /* opt_preserveReserved */);418};419420421/**422* @param {string} uri The URI to examine.423* @return {?string} The query data still encoded, or null if none. Does not424* include the question mark itself.425*/426goog.uri.utils.getQueryData = function(uri) {427'use strict';428return goog.uri.utils.getComponentByIndex_(429goog.uri.utils.ComponentIndex.QUERY_DATA, uri);430};431432433/**434* @param {string} uri The URI to examine.435* @return {?string} The fragment identifier, or null if none. Does not436* include the hash mark itself.437*/438goog.uri.utils.getFragmentEncoded = function(uri) {439'use strict';440// The hash mark may not appear in any other part of the URL.441var hashIndex = uri.indexOf('#');442return hashIndex < 0 ? null : uri.slice(hashIndex + 1);443};444445446/**447* @param {string} uri The URI to examine.448* @param {?string} fragment The encoded fragment identifier, or null if none.449* Does not include the hash mark itself.450* @return {string} The URI with the fragment set.451*/452goog.uri.utils.setFragmentEncoded = function(uri, fragment) {453'use strict';454return goog.uri.utils.removeFragment(uri) + (fragment ? '#' + fragment : '');455};456457458/**459* @param {string} uri The URI to examine.460* @return {?string} The decoded fragment identifier, or null if none. Does461* not include the hash mark.462*/463goog.uri.utils.getFragment = function(uri) {464'use strict';465return goog.uri.utils.decodeIfPossible_(466goog.uri.utils.getFragmentEncoded(uri));467};468469470/**471* Extracts everything up to the port of the URI.472* @param {string} uri The URI string.473* @return {string} Everything up to and including the port.474*/475goog.uri.utils.getHost = function(uri) {476'use strict';477var pieces = goog.uri.utils.split(uri);478return goog.uri.utils.buildFromEncodedParts(479pieces[goog.uri.utils.ComponentIndex.SCHEME],480pieces[goog.uri.utils.ComponentIndex.USER_INFO],481pieces[goog.uri.utils.ComponentIndex.DOMAIN],482pieces[goog.uri.utils.ComponentIndex.PORT]);483};484485486/**487* Returns the origin for a given URL.488* @param {string} uri The URI string.489* @return {string} Everything up to and including the port.490*/491goog.uri.utils.getOrigin = function(uri) {492'use strict';493var pieces = goog.uri.utils.split(uri);494return goog.uri.utils.buildFromEncodedParts(495pieces[goog.uri.utils.ComponentIndex.SCHEME], null /* opt_userInfo */,496pieces[goog.uri.utils.ComponentIndex.DOMAIN],497pieces[goog.uri.utils.ComponentIndex.PORT]);498};499500501/**502* Extracts the path of the URL and everything after.503* @param {string} uri The URI string.504* @return {string} The URI, starting at the path and including the query505* parameters and fragment identifier.506*/507goog.uri.utils.getPathAndAfter = function(uri) {508'use strict';509var pieces = goog.uri.utils.split(uri);510return goog.uri.utils.buildFromEncodedParts(511null, null, null, null, pieces[goog.uri.utils.ComponentIndex.PATH],512pieces[goog.uri.utils.ComponentIndex.QUERY_DATA],513pieces[goog.uri.utils.ComponentIndex.FRAGMENT]);514};515516517/**518* Gets the URI with the fragment identifier removed.519* @param {string} uri The URI to examine.520* @return {string} Everything preceding the hash mark.521*/522goog.uri.utils.removeFragment = function(uri) {523'use strict';524// The hash mark may not appear in any other part of the URL.525var hashIndex = uri.indexOf('#');526return hashIndex < 0 ? uri : uri.slice(0, hashIndex);527};528529530/**531* Ensures that two URI's have the exact same domain, scheme, and port.532*533* Unlike the version in goog.Uri, this checks protocol, and therefore is534* suitable for checking against the browser's same-origin policy.535*536* @param {string} uri1 The first URI.537* @param {string} uri2 The second URI.538* @return {boolean} Whether they have the same scheme, domain and port.539*/540goog.uri.utils.haveSameDomain = function(uri1, uri2) {541'use strict';542var pieces1 = goog.uri.utils.split(uri1);543var pieces2 = goog.uri.utils.split(uri2);544return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==545pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&546pieces1[goog.uri.utils.ComponentIndex.SCHEME] ==547pieces2[goog.uri.utils.ComponentIndex.SCHEME] &&548pieces1[goog.uri.utils.ComponentIndex.PORT] ==549pieces2[goog.uri.utils.ComponentIndex.PORT];550};551552553/**554* Asserts that there are no fragment or query identifiers, only in uncompiled555* mode.556* @param {string} uri The URI to examine.557* @private558*/559goog.uri.utils.assertNoFragmentsOrQueries_ = function(uri) {560'use strict';561goog.asserts.assert(562uri.indexOf('#') < 0 && uri.indexOf('?') < 0,563'goog.uri.utils: Fragment or query identifiers are not supported: [%s]',564uri);565};566567568/**569* Supported query parameter values by the parameter serializing utilities.570*571* If a value is null or undefined, the key-value pair is skipped, as an easy572* way to omit parameters conditionally. Non-array parameters are converted573* to a string and URI encoded. Array values are expanded into multiple574* &key=value pairs, with each element stringized and URI-encoded.575*576* @typedef {*}577*/578goog.uri.utils.QueryValue;579580581/**582* An array representing a set of query parameters with alternating keys583* and values.584*585* Keys are assumed to be URI encoded already and live at even indices. See586* goog.uri.utils.QueryValue for details on how parameter values are encoded.587*588* Example:589* <pre>590* var data = [591* // Simple param: ?name=BobBarker592* 'name', 'BobBarker',593* // Conditional param -- may be omitted entirely.594* 'specialDietaryNeeds', hasDietaryNeeds() ? getDietaryNeeds() : null,595* // Multi-valued param: &house=LosAngeles&house=NewYork&house=null596* 'house', ['LosAngeles', 'NewYork', null]597* ];598* </pre>599*600* @typedef {!Array<string|goog.uri.utils.QueryValue>}601*/602goog.uri.utils.QueryArray;603604605/**606* Parses encoded query parameters and calls callback function for every607* parameter found in the string.608*609* Missing value of parameter (e.g. “…&key&…”) is treated as if the value was an610* empty string. Keys may be empty strings (e.g. “…&=value&…”) which also means611* that “…&=&…” and “…&&…” will result in an empty key and value.612*613* @param {string} encodedQuery Encoded query string excluding question mark at614* the beginning.615* @param {function(string, string)} callback Function called for every616* parameter found in query string. The first argument (name) will not be617* urldecoded (so the function is consistent with buildQueryData), but the618* second will. If the parameter has no value (i.e. “=” was not present)619* the second argument (value) will be an empty string.620*/621goog.uri.utils.parseQueryData = function(encodedQuery, callback) {622'use strict';623if (!encodedQuery) {624return;625}626var pairs = encodedQuery.split('&');627for (var i = 0; i < pairs.length; i++) {628var indexOfEquals = pairs[i].indexOf('=');629var name = null;630var value = null;631if (indexOfEquals >= 0) {632name = pairs[i].substring(0, indexOfEquals);633value = pairs[i].substring(indexOfEquals + 1);634} else {635name = pairs[i];636}637callback(name, value ? goog.string.urlDecode(value) : '');638}639};640641642/**643* Split the URI into 3 parts where the [1] is the queryData without a leading644* '?'. For example, the URI http://foo.com/bar?a=b#abc returns645* ['http://foo.com/bar','a=b','#abc'].646* @param {string} uri The URI to parse.647* @return {!Array<string>} An array representation of uri of length 3 where the648* middle value is the queryData without a leading '?'.649* @private650*/651goog.uri.utils.splitQueryData_ = function(uri) {652'use strict';653// Find the query data and hash.654var hashIndex = uri.indexOf('#');655if (hashIndex < 0) {656hashIndex = uri.length;657}658var questionIndex = uri.indexOf('?');659var queryData;660if (questionIndex < 0 || questionIndex > hashIndex) {661questionIndex = hashIndex;662queryData = '';663} else {664queryData = uri.substring(questionIndex + 1, hashIndex);665}666return [uri.slice(0, questionIndex), queryData, uri.slice(hashIndex)];667};668669670/**671* Join an array created by splitQueryData_ back into a URI.672* @param {!Array<string>} parts A URI in the form generated by splitQueryData_.673* @return {string} The joined URI.674* @private675*/676goog.uri.utils.joinQueryData_ = function(parts) {677'use strict';678return parts[0] + (parts[1] ? '?' + parts[1] : '') + parts[2];679};680681682/**683* @param {string} queryData684* @param {string} newData685* @return {string}686* @private687*/688goog.uri.utils.appendQueryData_ = function(queryData, newData) {689'use strict';690if (!newData) {691return queryData;692}693return queryData ? queryData + '&' + newData : newData;694};695696697/**698* @param {string} uri699* @param {string} queryData700* @return {string}701* @private702*/703goog.uri.utils.appendQueryDataToUri_ = function(uri, queryData) {704'use strict';705if (!queryData) {706return uri;707}708var parts = goog.uri.utils.splitQueryData_(uri);709parts[1] = goog.uri.utils.appendQueryData_(parts[1], queryData);710return goog.uri.utils.joinQueryData_(parts);711};712713714/**715* Appends key=value pairs to an array, supporting multi-valued objects.716* @param {*} key The key prefix.717* @param {goog.uri.utils.QueryValue} value The value to serialize.718* @param {!Array<string>} pairs The array to which the 'key=value' strings719* should be appended.720* @private721*/722goog.uri.utils.appendKeyValuePairs_ = function(key, value, pairs) {723'use strict';724goog.asserts.assertString(key);725if (Array.isArray(value)) {726// Convince the compiler it's an array.727goog.asserts.assertArray(value);728for (var j = 0; j < value.length; j++) {729// Convert to string explicitly, to short circuit the null and array730// logic in this function -- this ensures that null and undefined get731// written as literal 'null' and 'undefined', and arrays don't get732// expanded out but instead encoded in the default way.733goog.uri.utils.appendKeyValuePairs_(key, String(value[j]), pairs);734}735} else if (value != null) {736// Skip a top-level null or undefined entirely.737pairs.push(738key +739// Check for empty string. Zero gets encoded into the url as literal740// strings. For empty string, skip the equal sign, to be consistent741// with UriBuilder.java.742(value === '' ? '' : '=' + goog.string.urlEncode(value)));743}744};745746747/**748* Builds a query data string from a sequence of alternating keys and values.749* Currently generates "&key&" for empty args.750*751* @param {!IArrayLike<string|goog.uri.utils.QueryValue>} keysAndValues752* Alternating keys and values. See the QueryArray typedef.753* @param {number=} opt_startIndex A start offset into the arary, defaults to 0.754* @return {string} The encoded query string, in the form 'a=1&b=2'.755*/756goog.uri.utils.buildQueryData = function(keysAndValues, opt_startIndex) {757'use strict';758goog.asserts.assert(759Math.max(keysAndValues.length - (opt_startIndex || 0), 0) % 2 == 0,760'goog.uri.utils: Key/value lists must be even in length.');761762var params = [];763for (var i = opt_startIndex || 0; i < keysAndValues.length; i += 2) {764var key = /** @type {string} */ (keysAndValues[i]);765goog.uri.utils.appendKeyValuePairs_(key, keysAndValues[i + 1], params);766}767return params.join('&');768};769770771/**772* Builds a query data string from a map.773* Currently generates "&key&" for empty args.774*775* @param {!Object<string, goog.uri.utils.QueryValue>} map An object where keys776* are URI-encoded parameter keys, and the values are arbitrary types777* or arrays. Keys with a null value are dropped.778* @return {string} The encoded query string, in the form 'a=1&b=2'.779*/780goog.uri.utils.buildQueryDataFromMap = function(map) {781'use strict';782var params = [];783for (var key in map) {784goog.uri.utils.appendKeyValuePairs_(key, map[key], params);785}786return params.join('&');787};788789790/**791* Appends URI parameters to an existing URI.792*793* The variable arguments may contain alternating keys and values. Keys are794* assumed to be already URI encoded. The values should not be URI-encoded,795* and will instead be encoded by this function.796* <pre>797* appendParams('http://www.foo.com?existing=true',798* 'key1', 'value1',799* 'key2', 'value?willBeEncoded',800* 'key3', ['valueA', 'valueB', 'valueC'],801* 'key4', null);802* result: 'http://www.foo.com?existing=true&' +803* 'key1=value1&' +804* 'key2=value%3FwillBeEncoded&' +805* 'key3=valueA&key3=valueB&key3=valueC'806* </pre>807*808* A single call to this function will not exhibit quadratic behavior in IE,809* whereas multiple repeated calls may, although the effect is limited by810* fact that URL's generally can't exceed 2kb.811*812* @param {string} uri The original URI, which may already have query data.813* @param {...(goog.uri.utils.QueryArray|goog.uri.utils.QueryValue)}814* var_args815* An array or argument list conforming to goog.uri.utils.QueryArray.816* @return {string} The URI with all query parameters added.817*/818goog.uri.utils.appendParams = function(uri, var_args) {819'use strict';820var queryData = arguments.length == 2 ?821goog.uri.utils.buildQueryData(arguments[1], 0) :822goog.uri.utils.buildQueryData(arguments, 1);823return goog.uri.utils.appendQueryDataToUri_(uri, queryData);824};825826827/**828* Appends query parameters from a map.829*830* @param {string} uri The original URI, which may already have query data.831* @param {!Object<goog.uri.utils.QueryValue>} map An object where keys are832* URI-encoded parameter keys, and the values are arbitrary types or arrays.833* Keys with a null value are dropped.834* @return {string} The new parameters.835*/836goog.uri.utils.appendParamsFromMap = function(uri, map) {837'use strict';838var queryData = goog.uri.utils.buildQueryDataFromMap(map);839return goog.uri.utils.appendQueryDataToUri_(uri, queryData);840};841842843/**844* Appends a single URI parameter.845*846* Repeated calls to this can exhibit quadratic behavior in IE6 due to the847* way string append works, though it should be limited given the 2kb limit.848*849* @param {string} uri The original URI, which may already have query data.850* @param {string} key The key, which must already be URI encoded.851* @param {*=} opt_value The value, which will be stringized and encoded852* (assumed not already to be encoded). If omitted, undefined, or null, the853* key will be added as a valueless parameter.854* @return {string} The URI with the query parameter added.855*/856goog.uri.utils.appendParam = function(uri, key, opt_value) {857'use strict';858var value = (opt_value != null) ? '=' + goog.string.urlEncode(opt_value) : '';859return goog.uri.utils.appendQueryDataToUri_(uri, key + value);860};861862863/**864* Finds the next instance of a query parameter with the specified name.865*866* Does not instantiate any objects.867*868* @param {string} uri The URI to search. May contain a fragment identifier869* if opt_hashIndex is specified.870* @param {number} startIndex The index to begin searching for the key at. A871* match may be found even if this is one character after the ampersand.872* @param {string} keyEncoded The URI-encoded key.873* @param {number} hashOrEndIndex Index to stop looking at. If a hash874* mark is present, it should be its index, otherwise it should be the875* length of the string.876* @return {number} The position of the first character in the key's name,877* immediately after either a question mark or a dot.878* @private879*/880goog.uri.utils.findParam_ = function(881uri, startIndex, keyEncoded, hashOrEndIndex) {882'use strict';883var index = startIndex;884var keyLength = keyEncoded.length;885886// Search for the key itself and post-filter for surronuding punctuation,887// rather than expensively building a regexp.888while ((index = uri.indexOf(keyEncoded, index)) >= 0 &&889index < hashOrEndIndex) {890var precedingChar = uri.charCodeAt(index - 1);891// Ensure that the preceding character is '&' or '?'.892if (precedingChar == goog.uri.utils.CharCode_.AMPERSAND ||893precedingChar == goog.uri.utils.CharCode_.QUESTION) {894// Ensure the following character is '&', '=', '#', or NaN895// (end of string).896var followingChar = uri.charCodeAt(index + keyLength);897if (!followingChar || followingChar == goog.uri.utils.CharCode_.EQUAL ||898followingChar == goog.uri.utils.CharCode_.AMPERSAND ||899followingChar == goog.uri.utils.CharCode_.HASH) {900return index;901}902}903index += keyLength + 1;904}905906return -1;907};908909910/**911* Regular expression for finding a hash mark or end of string.912* @type {RegExp}913* @private914*/915goog.uri.utils.hashOrEndRe_ = /#|$/;916917918/**919* Determines if the URI contains a specific key.920*921* Performs no object instantiations.922*923* @param {string} uri The URI to process. May contain a fragment924* identifier.925* @param {string} keyEncoded The URI-encoded key. Case-sensitive.926* @return {boolean} Whether the key is present.927*/928goog.uri.utils.hasParam = function(uri, keyEncoded) {929'use strict';930return goog.uri.utils.findParam_(931uri, 0, keyEncoded, uri.search(goog.uri.utils.hashOrEndRe_)) >= 0;932};933934935/**936* Gets the first value of a query parameter.937* @param {string} uri The URI to process. May contain a fragment.938* @param {string} keyEncoded The URI-encoded key. Case-sensitive.939* @return {?string} The first value of the parameter (URI-decoded), or null940* if the parameter is not found.941*/942goog.uri.utils.getParamValue = function(uri, keyEncoded) {943'use strict';944var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);945var foundIndex =946goog.uri.utils.findParam_(uri, 0, keyEncoded, hashOrEndIndex);947948if (foundIndex < 0) {949return null;950} else {951var endPosition = uri.indexOf('&', foundIndex);952if (endPosition < 0 || endPosition > hashOrEndIndex) {953endPosition = hashOrEndIndex;954}955// Progress forth to the end of the "key=" or "key&" substring.956foundIndex += keyEncoded.length + 1;957return goog.string.urlDecode(958uri.slice(foundIndex, endPosition !== -1 ? endPosition : 0));959}960};961962963/**964* Gets all values of a query parameter.965* @param {string} uri The URI to process. May contain a fragment.966* @param {string} keyEncoded The URI-encoded key. Case-sensitive.967* @return {!Array<string>} All URI-decoded values with the given key.968* If the key is not found, this will have length 0, but never be null.969*/970goog.uri.utils.getParamValues = function(uri, keyEncoded) {971'use strict';972var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);973var position = 0;974var foundIndex;975var result = [];976977while ((foundIndex = goog.uri.utils.findParam_(978uri, position, keyEncoded, hashOrEndIndex)) >= 0) {979// Find where this parameter ends, either the '&' or the end of the980// query parameters.981position = uri.indexOf('&', foundIndex);982if (position < 0 || position > hashOrEndIndex) {983position = hashOrEndIndex;984}985986// Progress forth to the end of the "key=" or "key&" substring.987foundIndex += keyEncoded.length + 1;988result.push(989goog.string.urlDecode(uri.slice(foundIndex, Math.max(position, 0))));990}991992return result;993};994995996/**997* Regexp to find trailing question marks and ampersands.998* @type {RegExp}999* @private1000*/1001goog.uri.utils.trailingQueryPunctuationRe_ = /[?&]($|#)/;100210031004/**1005* Removes all instances of a query parameter.1006* @param {string} uri The URI to process. Must not contain a fragment.1007* @param {string} keyEncoded The URI-encoded key.1008* @return {string} The URI with all instances of the parameter removed.1009*/1010goog.uri.utils.removeParam = function(uri, keyEncoded) {1011'use strict';1012var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);1013var position = 0;1014var foundIndex;1015var buffer = [];10161017// Look for a query parameter.1018while ((foundIndex = goog.uri.utils.findParam_(1019uri, position, keyEncoded, hashOrEndIndex)) >= 0) {1020// Get the portion of the query string up to, but not including, the ?1021// or & starting the parameter.1022buffer.push(uri.substring(position, foundIndex));1023// Progress to immediately after the '&'. If not found, go to the end.1024// Avoid including the hash mark.1025position = Math.min(1026(uri.indexOf('&', foundIndex) + 1) || hashOrEndIndex, hashOrEndIndex);1027}10281029// Append everything that is remaining.1030buffer.push(uri.slice(position));10311032// Join the buffer, and remove trailing punctuation that remains.1033return buffer.join('').replace(1034goog.uri.utils.trailingQueryPunctuationRe_, '$1');1035};103610371038/**1039* Replaces all existing definitions of a parameter with a single definition.1040*1041* Repeated calls to this can exhibit quadratic behavior due to the need to1042* find existing instances and reconstruct the string, though it should be1043* limited given the 2kb limit. Consider using appendParams or setParamsFromMap1044* to update multiple parameters in bulk.1045*1046* @param {string} uri The original URI, which may already have query data.1047* @param {string} keyEncoded The key, which must already be URI encoded.1048* @param {*} value The value, which will be stringized and encoded (assumed1049* not already to be encoded).1050* @return {string} The URI with the query parameter added.1051*/1052goog.uri.utils.setParam = function(uri, keyEncoded, value) {1053'use strict';1054return goog.uri.utils.appendParam(1055goog.uri.utils.removeParam(uri, keyEncoded), keyEncoded, value);1056};105710581059/**1060* Effeciently set or remove multiple query parameters in a URI. Order of1061* unchanged parameters will not be modified, all updated parameters will be1062* appended to the end of the query. Params with values of null or undefined are1063* removed.1064*1065* @param {string} uri The URI to process.1066* @param {!Object<string, goog.uri.utils.QueryValue>} params A list of1067* parameters to update. If null or undefined, the param will be removed.1068* @return {string} An updated URI where the query data has been updated with1069* the params.1070*/1071goog.uri.utils.setParamsFromMap = function(uri, params) {1072'use strict';1073var parts = goog.uri.utils.splitQueryData_(uri);1074var queryData = parts[1];1075var buffer = [];1076if (queryData) {1077queryData.split('&').forEach(function(pair) {1078'use strict';1079var indexOfEquals = pair.indexOf('=');1080var name = indexOfEquals >= 0 ? pair.slice(0, indexOfEquals) : pair;1081if (!params.hasOwnProperty(name)) {1082buffer.push(pair);1083}1084});1085}1086parts[1] = goog.uri.utils.appendQueryData_(1087buffer.join('&'), goog.uri.utils.buildQueryDataFromMap(params));1088return goog.uri.utils.joinQueryData_(parts);1089};109010911092/**1093* Generates a URI path using a given URI and a path with checks to1094* prevent consecutive "//". The baseUri passed in must not contain1095* query or fragment identifiers. The path to append may not contain query or1096* fragment identifiers.1097*1098* @param {string} baseUri URI to use as the base.1099* @param {string} path Path to append.1100* @return {string} Updated URI.1101*/1102goog.uri.utils.appendPath = function(baseUri, path) {1103'use strict';1104goog.uri.utils.assertNoFragmentsOrQueries_(baseUri);11051106// Remove any trailing '/'1107if (goog.string.endsWith(baseUri, '/')) {1108baseUri = baseUri.slice(0, -1);1109}1110// Remove any leading '/'1111if (goog.string.startsWith(path, '/')) {1112path = path.slice(1);1113}1114return '' + baseUri + '/' + path;1115};111611171118/**1119* Replaces the path.1120* @param {string} uri URI to use as the base.1121* @param {string} path New path.1122* @return {string} Updated URI.1123*/1124goog.uri.utils.setPath = function(uri, path) {1125'use strict';1126// Add any missing '/'.1127if (!goog.string.startsWith(path, '/')) {1128path = '/' + path;1129}1130var parts = goog.uri.utils.split(uri);1131return goog.uri.utils.buildFromEncodedParts(1132parts[goog.uri.utils.ComponentIndex.SCHEME],1133parts[goog.uri.utils.ComponentIndex.USER_INFO],1134parts[goog.uri.utils.ComponentIndex.DOMAIN],1135parts[goog.uri.utils.ComponentIndex.PORT], path,1136parts[goog.uri.utils.ComponentIndex.QUERY_DATA],1137parts[goog.uri.utils.ComponentIndex.FRAGMENT]);1138};113911401141/**1142* Standard supported query parameters.1143* @enum {string}1144*/1145goog.uri.utils.StandardQueryParam = {11461147/** Unused parameter for unique-ifying. */1148RANDOM: 'zx'1149};115011511152/**1153* Sets the zx parameter of a URI to a random value.1154* @param {string} uri Any URI.1155* @return {string} That URI with the "zx" parameter added or replaced to1156* contain a random string.1157*/1158goog.uri.utils.makeUnique = function(uri) {1159'use strict';1160return goog.uri.utils.setParam(1161uri, goog.uri.utils.StandardQueryParam.RANDOM,1162goog.string.getRandomString());1163};116411651166