diff --git a/allianceauth/framework/static/allianceauth/framework/js/auth-framework.js b/allianceauth/framework/static/allianceauth/framework/js/auth-framework.js index 700ea200..167d8a52 100644 --- a/allianceauth/framework/static/allianceauth/framework/js/auth-framework.js +++ b/allianceauth/framework/static/allianceauth/framework/js/auth-framework.js @@ -217,3 +217,53 @@ const fetchPost = async ({ responseIsJson: responseIsJson }); }; + +/** + * Recursively merges properties from source objects into a target object. If a property at the current level is an object, + * and both target and source have it, the property is merged. Otherwise, the source property overwrites the target property. + * This function does not modify the source objects and prevents prototype pollution by not allowing __proto__, constructor, + * and prototype property names. + * + * @usage + * ```javascript + * const target = {a: 1, b: {c: 2}}; + * const source1 = {b: {d: 3}, e: 4 }; + * const source2 = {a: 5, b: {c: 6}}; + * + * const merged = objectDeepMerge(target, source1, source2); + * + * console.log(merged); // {a: 5, b: {c: 6, d: 3}, e: 4} + * ``` + * + * @param {Object} target The target object to merge properties into. + * @param {...Object} sources One or more source objects from which to merge properties. + * @returns {Object} The target object after merging properties from sources. + */ +function objectDeepMerge (target, ...sources) { + if (!sources.length) { + return target; + } + + // Iterate through each source object without modifying the `sources` array. + sources.forEach(source => { + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue; // Skip potentially dangerous keys to prevent prototype pollution. + } + + if (!target[key] || !isObject(target[key])) { + target[key] = {}; + } + + objectDeepMerge(target[key], source[key]); + } else { + target[key] = source[key]; + } + } + } + }); + + return target; +} diff --git a/docs/development/custom/framework/js.md b/docs/development/custom/framework/js.md index 5b78fb00..70d58e59 100644 --- a/docs/development/custom/framework/js.md +++ b/docs/development/custom/framework/js.md @@ -100,3 +100,25 @@ fetchPost({ - `csrfToken`: The CSRF token to include in the request headers. - `payload`: The data as JS object to send with the request. - `responseIsJson`: Optional boolean indicating if the response should be parsed as JSON (default is `true`). + +### objectDeepMerge() + +Recursively merges properties from source objects into a target object. If a property at the current level is an object, +and both target and source have it, the property is merged. Otherwise, the source property overwrites the target property. + +This function does not modify the source objects and prevents prototype pollution by not allowing `__proto__`, `constructor`, +and `prototype` property names. + +Usage: + +```javascript +/* global objectDeepMerge */ + +const target = {a: 1, b: {c: 2}}; +const source1 = {b: {d: 3}, e: 4 }; +const source2 = {a: 5, b: {c: 6}}; + +const merged = objectDeepMerge(target, source1, source2); + +console.log(merged); // {a: 5, b: {c: 6, d: 3}, e: 4} +```