packages/fxa-content-server/app/scripts/lib/url.js (107 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// utilities to deal with urls
import _ from 'underscore';
export default {
/**
* Convert a search string to its object representation, one entry
* per query parameter. Assumes the string is a search string and
* not a full URL without a search string.
*
* @param {String} [str=''] - string to convert
* @param {String[]} [allowedFields] - list of allowed fields. If not
* declared, all fields are allowed.
* @returns {Object}
*/
searchParams(str = '', allowedFields) {
// ditch everything before the ? and from # to the end
const search = str.replace(/(^.*\?|#.*$)/g, '').trim();
if (!search) {
return {};
}
return this.splitEncodedParams(search, allowedFields);
},
/**
* Return the value of a single query parameter in the string
*
* @param {String} name - name of the query parameter
* @param {String} [str=''] - search string
* @returns {String}
*/
searchParam(name, str) {
return this.searchParams(str)[name];
},
/**
* Convert a hash string to its object representation, one entry
* per query parameter
*
* @param {String} [str=''] - string to convert
* @param {String[]} [allowedFields=[]] - list of allowed fields. If not
* declared, all fields are allowed.
* @returns {Object}
*/
hashParams(str = '', allowedFields) {
// ditch everything before the #
const hash = str.replace(/^.*#/, '').trim();
if (!hash) {
return {};
}
return this.splitEncodedParams(hash, allowedFields);
},
/**
* Convert a URI encoded string to its object representation.
*
* `&` is the expected delimiter between parameters.
* `=` is the delimiter between a key and a value.
*
* @param {String} [str=''] string to split
* @param {String[]} [allowedFields=[]] - list of allowed fields. If not
* declared, all fields are allowed.
* @returns {Object}
*/
splitEncodedParams(str = '', allowedFields) {
const pairs = str.split('&');
const terms = {};
_.each(pairs, (pair) => {
const [key, value] = pair.split('=');
terms[key] = decodeURIComponent(value).trim();
});
if (!allowedFields) {
return terms;
}
return _.pick(terms, allowedFields);
},
/**
* Convert an object to a search string.
*
* @param {Object} [obj={}] - object to convert
* @returns {String}
*/
objToSearchString(obj) {
return this.objToUrlString(obj, '?');
},
/**
* Convert an object to a hash string.
*
* @param {Object} [obj={}] - object to convert
* @returns {String}
*/
objToHashString(obj) {
return this.objToUrlString(obj, '#');
},
/**
* Recursively break an object down in to query string key/values.
* Supplementary to objToUrlString.
*
* @param {Object} [obj={}] - object to break down
* @param {Array} [keys=[]] - existing keys to supply to the pairing
* @returns {Array}
*/
_getObjPairs(obj = {}, keys = []) {
return Object.entries(obj || {}).reduce((pairs, [key, value]) => {
if (typeof value === 'object') {
pairs.push(...this._getObjPairs(value, [...keys, key]));
} else if (value != null) {
pairs.push([[...keys, key], value]);
}
return pairs;
}, []);
},
/**
* Convert an object to a URL safe string
*
* @param {Object} [obj={}] - object to convert
* @param {String} [prefix='?'] - prefix to append
* @returns {String}
*/
objToUrlString(obj = {}, prefix = '?') {
const params = this._getObjPairs(obj)
.map(([[key0, ...keysRest], value]) => {
value = value.toString();
if (value.length) {
return `${key0}${keysRest
.map((a) => `[${a}]`)
.join('')}=${encodeURIComponent(value)}`;
}
})
.filter((p) => !!p);
if (!params.length) {
return '';
}
return prefix + params.join('&');
},
/**
* Get the origin portion of the URL
*
* @param {String} url
* @returns {String}
*/
getOrigin(url) {
if (!url) {
return '';
}
// The URL API is only supported by new browsers, a workaround is used.
const anchor = document.createElement('a');
// Fx 18 (& FxOS 1.*) do not support anchor.origin. Build the origin
// out of the protocol and host.
// Use setAttribute instead of a direct set or else Fx18 does not
// update anchor.protocol & anchor.host.
anchor.setAttribute('href', url);
if (!(anchor.protocol && anchor.host)) {
// malformed URL. Return null. This is the same behavior as URL.origin
return null;
}
// IE10 always returns port, Firefox and Chrome hide the port if it is the default port e.g 443, 80
// We normalize IE10 output, use the hostname if it is a default port to match Firefox and Chrome.
// Also IE10 returns anchor.port as String, Firefox and Chrome use Number.
const host =
Number(anchor.port) === 443 || Number(anchor.port) === 80
? anchor.hostname
: anchor.host;
const origin = anchor.protocol + '//' + host;
// if only the domain is specified without a protocol, the anchor
// will use the page's origin as the URL's origin. Check that
// the created origin matches the first portion of
// the passed in URL. If not, then the anchor element
// modified the origin.
if (url.indexOf(origin) !== 0) {
return null;
}
return origin;
},
/**
* Update the search string in the given URL.
*
* @param {String} uri - uri to update
* @param {Object} newParams
* @returns {String}
*/
updateSearchString(uri, newParams) {
let params = {};
const startOfParams = uri.indexOf('?');
if (startOfParams >= 0) {
params = this.searchParams(uri.substring(startOfParams + 1));
uri = uri.substring(0, startOfParams);
}
_.extend(params, newParams);
return uri + this.objToSearchString(params);
},
/**
* Clean the search string by only allowing search parameters declared in
* `allowedFields`
*
* @param {String} uri - uri with search string to clean.
* @param {String[]} allowedFields - list of allowed fields.
* @returns {String}
*/
cleanSearchString(uri, allowedFields) {
const [base, search = ''] = uri.split('?');
const cleanedQueryParams = this.searchParams(search, allowedFields);
return base + this.objToSearchString(cleanedQueryParams);
},
/**
* Set a new value for the query search string in place. This does
* not reload the page but rather updates the window state history.
*
* @param {String} param - param to update
* @param {String} value - value to set
*/
setSearchString(param, value) {
const params = new URLSearchParams(this.window.location.search);
params.set(param, value);
// This will update the url with new params inplace
this.window.history.replaceState(
{},
'',
`${this.window.location.pathname}?${params}`
);
},
};