|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright CERN and copyright holders of ALICE O2. This software is |
| 4 | + * distributed under the terms of the GNU General Public License v3 (GPL |
| 5 | + * Version 3), copied verbatim in the file "COPYING". |
| 6 | + * |
| 7 | + * See http://alice-o2.web.cern.ch/license for full licensing information. |
| 8 | + * |
| 9 | + * In applying this license CERN does not waive the privileges and immunities |
| 10 | + * granted to it by virtue of its status as an Intergovernmental Organization |
| 11 | + * or submit itself to any jurisdiction. |
| 12 | + */ |
| 13 | + |
| 14 | +/** |
| 15 | + * Concatenate the given parameters key path to form proper URL query param key |
| 16 | + * |
| 17 | + * @param {string[]} parametersKeysPath the keys path to concatenate |
| 18 | + * @return {string} the concatenated keys path |
| 19 | + */ |
| 20 | +const concatenateParametersKeyPath = (parametersKeysPath) => { |
| 21 | + if (parametersKeysPath.length === 0) { |
| 22 | + return 'no parameters keys'; |
| 23 | + } |
| 24 | + |
| 25 | + const [mainKey, ...otherKeys] = parametersKeysPath; |
| 26 | + return `${mainKey}${otherKeys.map((key) => `[${key}]`).join('')}`; |
| 27 | +}; |
| 28 | + |
| 29 | +/** |
| 30 | + * Error to be used when building parameters tree from keys path |
| 31 | + */ |
| 32 | +class ParameterBuildingError extends Error { |
| 33 | + /** |
| 34 | + * Constructor |
| 35 | + * |
| 36 | + * @param {string} message the global error message |
| 37 | + * @param {string[]} parametersKeysPath the parameters keys path where the error occurred |
| 38 | + */ |
| 39 | + constructor(message, parametersKeysPath) { |
| 40 | + super(`${message} - ${concatenateParametersKeyPath(parametersKeysPath)}`); |
| 41 | + |
| 42 | + this._originalMessage = message; |
| 43 | + this._parametersKeysPath = parametersKeysPath; |
| 44 | + } |
| 45 | + |
| 46 | + /** |
| 47 | + * Return the orignal message of the error, without concatenated parameters key path |
| 48 | + * |
| 49 | + * @return {string} the original message |
| 50 | + */ |
| 51 | + get originalMessage() { |
| 52 | + return this._originalMessage; |
| 53 | + } |
| 54 | + |
| 55 | + /** |
| 56 | + * Return the parameters keys path of the error |
| 57 | + * |
| 58 | + * @return {string[]} the parameters keys path |
| 59 | + */ |
| 60 | + get parametersKeyPath() { |
| 61 | + return this._parametersKeysPath; |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +/** |
| 66 | + * Build a parameter object or array from a parameters keys path |
| 67 | + * |
| 68 | + * For example, a parameter `key1[key2][]=value` translates to keys path ['key1', 'key2', ''] and will lead to {key1: {key2: [value]}} |
| 69 | + * |
| 70 | + * @param {object|array} parentParameter the parameter's object or array up to the current key |
| 71 | + * @param {array} nestedKeys the keys path to build from the current point |
| 72 | + * @param {string} value the value of the parameter represented by the key path |
| 73 | + * @return {void} |
| 74 | + */ |
| 75 | +const buildParameterFromNestedKeys = (parentParameter, nestedKeys, value) => { |
| 76 | + const currentKey = nestedKeys.shift(); |
| 77 | + |
| 78 | + /* |
| 79 | + * Protect against prototype polluting assignment |
| 80 | + * https://codeql.github.com/codeql-query-help/javascript/js-prototype-polluting-assignment/ |
| 81 | + */ |
| 82 | + if (currentKey === '__proto__' || currentKey === 'constructor' || currentKey === 'prototype') { |
| 83 | + throw new Error(`Unauthorized parameters key ${currentKey}`); |
| 84 | + } |
| 85 | + |
| 86 | + if (currentKey === '') { |
| 87 | + // Parameter must be an array and the value is a new item in that array |
| 88 | + if (!Array.isArray(parentParameter)) { |
| 89 | + throw new ParameterBuildingError('Expected node in parameters tree to be an array', [currentKey]); |
| 90 | + } |
| 91 | + |
| 92 | + parentParameter.push(value); |
| 93 | + } else if (currentKey) { |
| 94 | + // Parameter must be an object and the value is a property in that array |
| 95 | + if (Array.isArray(parentParameter) || typeof parentParameter !== 'object' || parentParameter === null) { |
| 96 | + throw new ParameterBuildingError('Expected node in parameters tree to be an object', [currentKey]); |
| 97 | + } |
| 98 | + |
| 99 | + if (nestedKeys.length > 0) { |
| 100 | + // We still have nested keys to fill |
| 101 | + if (!(currentKey in parentParameter)) { |
| 102 | + parentParameter[currentKey] = nestedKeys[0] === '' ? [] : {}; |
| 103 | + } |
| 104 | + |
| 105 | + try { |
| 106 | + buildParameterFromNestedKeys(parentParameter[currentKey], nestedKeys, value); |
| 107 | + } catch (e) { |
| 108 | + if (e instanceof ParameterBuildingError) { |
| 109 | + throw new ParameterBuildingError(e.originalMessage, [currentKey, ...e.parametersKeyPath]); |
| 110 | + } |
| 111 | + throw e; |
| 112 | + } |
| 113 | + } else { |
| 114 | + if (Array.isArray(parentParameter[currentKey])) { |
| 115 | + throw new ParameterBuildingError('Node in parameters tree is an array but no more nested keys', [currentKey]); |
| 116 | + } else if (typeof parentParameter[currentKey] === 'object' && parentParameter[currentKey] !== null) { |
| 117 | + throw new ParameterBuildingError('Node in parameters tree is an object but no more nested keys', [currentKey]); |
| 118 | + } |
| 119 | + parentParameter[currentKey] = value; |
| 120 | + } |
| 121 | + } |
| 122 | +}; |
| 123 | + |
| 124 | +/** |
| 125 | + * Extract the parameters tree from the given URL parameters (any value after the "&" in a URL) |
| 126 | + * |
| 127 | + * @param {URLSearchParams} urlSearchParams the URL search parameters string |
| 128 | + * @param {object} [parameters] the existing parameters tree object (will be modified in place) |
| 129 | + * @return {object} the parameter tree |
| 130 | + */ |
| 131 | +exports.parseUrlParameters = (urlSearchParams, parameters) => { |
| 132 | + if (urlSearchParams.size === 0) { |
| 133 | + return {}; |
| 134 | + } |
| 135 | + |
| 136 | + if (!parameters) { |
| 137 | + parameters = {}; |
| 138 | + } |
| 139 | + |
| 140 | + for (const [key, value] of urlSearchParams.entries()) { |
| 141 | + const [firstKey, ...dirtyKeys] = key.split('['); |
| 142 | + const nestedKeys = [firstKey, ...dirtyKeys.map((key) => key.slice(0, -1))]; |
| 143 | + |
| 144 | + buildParameterFromNestedKeys(parameters, nestedKeys, value); |
| 145 | + } |
| 146 | + |
| 147 | + return parameters; |
| 148 | +}; |
0 commit comments