-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[O2B-536] Add buildUrl utility (#2750)
* [O2B-536] Add buildUrl utility * Fix lint warning * Prevent security issue * Use URLSearchParams * Revert package-lock change
- Loading branch information
1 parent
8532349
commit 3ba4600
Showing
11 changed files
with
680 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/** | ||
* @license | ||
* Copyright CERN and copyright holders of ALICE O2. This software is | ||
* distributed under the terms of the GNU General Public License v3 (GPL | ||
* Version 3), copied verbatim in the file "COPYING". | ||
* | ||
* See http://alice-o2.web.cern.ch/license for full licensing information. | ||
* | ||
* In applying this license CERN does not waive the privileges and immunities | ||
* granted to it by virtue of its status as an Intergovernmental Organization | ||
* or submit itself to any jurisdiction. | ||
*/ | ||
|
||
/** | ||
* @typedef {string|number|null|boolean} QueryParameterValue | ||
*/ | ||
|
||
const { parseUrlParameters } = require('./parseUrlParameters.js'); | ||
|
||
/** | ||
* Build a URL from a base URL (that may already have query parameters) and a list of query parameters | ||
* | ||
* @param {string} baseURL the base URL to which parameters should be added | ||
* @param {object} parameters the query parameters | ||
* @return {string} URL the built URL | ||
*/ | ||
exports.buildUrl = (baseURL, parameters) => { | ||
if (!parameters) { | ||
parameters = {}; | ||
} | ||
|
||
const [url, existingParameters] = baseURL.split('?'); | ||
|
||
parseUrlParameters(new URLSearchParams(existingParameters), parameters); | ||
|
||
const serializedQueryParameters = []; | ||
|
||
if (Object.keys(parameters).length === 0) { | ||
return url; | ||
} | ||
|
||
/** | ||
* Sanitize a value to be used as URL parameter key or value | ||
* | ||
* @param {string} value the value to sanitize | ||
* @return {string} the sanitized value | ||
*/ | ||
const sanitize = (value) => encodeURIComponent(decodeURIComponent(value)); | ||
|
||
/** | ||
* Stringify a query parameter to be used in a URL and push it in the serialized query parameters list | ||
* | ||
* @param {string} key the parameter's key | ||
* @param {QueryParameterValue} value the parameter's value | ||
* @return {void} | ||
*/ | ||
const formatAndPushQueryParameter = (key, value) => { | ||
if (value === undefined) { | ||
return; | ||
} | ||
|
||
if (Array.isArray(value)) { | ||
for (const subValue of value) { | ||
formatAndPushQueryParameter(`${key}[]`, subValue); | ||
} | ||
return; | ||
} | ||
|
||
if (typeof value === 'object' && value !== null) { | ||
for (const [subKey, subValue] of Object.entries(value)) { | ||
formatAndPushQueryParameter(`${key}[${sanitize(subKey)}]`, subValue); | ||
} | ||
return; | ||
} | ||
|
||
serializedQueryParameters.push(`${key}=${sanitize(value)}`); | ||
}; | ||
|
||
for (const [key, parameter] of Object.entries(parameters)) { | ||
formatAndPushQueryParameter(sanitize(key), parameter); | ||
} | ||
|
||
return `${url}?${serializedQueryParameters.join('&')}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
/** | ||
* @license | ||
* Copyright CERN and copyright holders of ALICE O2. This software is | ||
* distributed under the terms of the GNU General Public License v3 (GPL | ||
* Version 3), copied verbatim in the file "COPYING". | ||
* | ||
* See http://alice-o2.web.cern.ch/license for full licensing information. | ||
* | ||
* In applying this license CERN does not waive the privileges and immunities | ||
* granted to it by virtue of its status as an Intergovernmental Organization | ||
* or submit itself to any jurisdiction. | ||
*/ | ||
|
||
/** | ||
* Concatenate the given parameters key path to form proper URL query param key | ||
* | ||
* @param {string[]} parametersKeysPath the keys path to concatenate | ||
* @return {string} the concatenated keys path | ||
*/ | ||
const concatenateParametersKeyPath = (parametersKeysPath) => { | ||
if (parametersKeysPath.length === 0) { | ||
return 'no parameters keys'; | ||
} | ||
|
||
const [mainKey, ...otherKeys] = parametersKeysPath; | ||
return `${mainKey}${otherKeys.map((key) => `[${key}]`).join('')}`; | ||
}; | ||
|
||
/** | ||
* Error to be used when building parameters tree from keys path | ||
*/ | ||
class ParameterBuildingError extends Error { | ||
/** | ||
* Constructor | ||
* | ||
* @param {string} message the global error message | ||
* @param {string[]} parametersKeysPath the parameters keys path where the error occurred | ||
*/ | ||
constructor(message, parametersKeysPath) { | ||
super(`${message} - ${concatenateParametersKeyPath(parametersKeysPath)}`); | ||
|
||
this._originalMessage = message; | ||
this._parametersKeysPath = parametersKeysPath; | ||
} | ||
|
||
/** | ||
* Return the orignal message of the error, without concatenated parameters key path | ||
* | ||
* @return {string} the original message | ||
*/ | ||
get originalMessage() { | ||
return this._originalMessage; | ||
} | ||
|
||
/** | ||
* Return the parameters keys path of the error | ||
* | ||
* @return {string[]} the parameters keys path | ||
*/ | ||
get parametersKeyPath() { | ||
return this._parametersKeysPath; | ||
} | ||
} | ||
|
||
/** | ||
* Build a parameter object or array from a parameters keys path | ||
* | ||
* For example, a parameter `key1[key2][]=value` translates to keys path ['key1', 'key2', ''] and will lead to {key1: {key2: [value]}} | ||
* | ||
* @param {object|array} parentParameter the parameter's object or array up to the current key | ||
* @param {array} nestedKeys the keys path to build from the current point | ||
* @param {string} value the value of the parameter represented by the key path | ||
* @return {void} | ||
*/ | ||
const buildParameterFromNestedKeys = (parentParameter, nestedKeys, value) => { | ||
const currentKey = nestedKeys.shift(); | ||
|
||
/* | ||
* Protect against prototype polluting assignment | ||
* https://codeql.github.com/codeql-query-help/javascript/js-prototype-polluting-assignment/ | ||
*/ | ||
if (currentKey === '__proto__' || currentKey === 'constructor' || currentKey === 'prototype') { | ||
throw new Error(`Unauthorized parameters key ${currentKey}`); | ||
} | ||
|
||
if (currentKey === '') { | ||
// Parameter must be an array and the value is a new item in that array | ||
if (!Array.isArray(parentParameter)) { | ||
throw new ParameterBuildingError('Expected node in parameters tree to be an array', [currentKey]); | ||
} | ||
|
||
parentParameter.push(value); | ||
} else if (currentKey) { | ||
// Parameter must be an object and the value is a property in that array | ||
if (Array.isArray(parentParameter) || typeof parentParameter !== 'object' || parentParameter === null) { | ||
throw new ParameterBuildingError('Expected node in parameters tree to be an object', [currentKey]); | ||
} | ||
|
||
if (nestedKeys.length > 0) { | ||
// We still have nested keys to fill | ||
if (!(currentKey in parentParameter)) { | ||
parentParameter[currentKey] = nestedKeys[0] === '' ? [] : {}; | ||
} | ||
|
||
try { | ||
buildParameterFromNestedKeys(parentParameter[currentKey], nestedKeys, value); | ||
} catch (e) { | ||
if (e instanceof ParameterBuildingError) { | ||
throw new ParameterBuildingError(e.originalMessage, [currentKey, ...e.parametersKeyPath]); | ||
} | ||
throw e; | ||
} | ||
} else { | ||
if (Array.isArray(parentParameter[currentKey])) { | ||
throw new ParameterBuildingError('Node in parameters tree is an array but no more nested keys', [currentKey]); | ||
} else if (typeof parentParameter[currentKey] === 'object' && parentParameter[currentKey] !== null) { | ||
throw new ParameterBuildingError('Node in parameters tree is an object but no more nested keys', [currentKey]); | ||
} | ||
parentParameter[currentKey] = value; | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Extract the parameters tree from the given URL parameters (any value after the "&" in a URL) | ||
* | ||
* @param {URLSearchParams} urlSearchParams the URL search parameters string | ||
* @param {object} [parameters] the existing parameters tree object (will be modified in place) | ||
* @return {object} the parameter tree | ||
*/ | ||
exports.parseUrlParameters = (urlSearchParams, parameters) => { | ||
if (urlSearchParams.size === 0) { | ||
return {}; | ||
} | ||
|
||
if (!parameters) { | ||
parameters = {}; | ||
} | ||
|
||
for (const [key, value] of urlSearchParams.entries()) { | ||
const [firstKey, ...dirtyKeys] = key.split('['); | ||
const nestedKeys = [firstKey, ...dirtyKeys.map((key) => key.slice(0, -1))]; | ||
|
||
buildParameterFromNestedKeys(parameters, nestedKeys, value); | ||
} | ||
|
||
return parameters; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.