From 099a39a2a2556a1e3b17c0173a02147ed4c76610 Mon Sep 17 00:00:00 2001 From: Peter Pfeufer Date: Sat, 9 Aug 2025 15:54:39 +0200 Subject: [PATCH] [ADD] `objectDeepMerge` function 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. --- .../framework/js/auth-framework.js | 50 +++++++++++++++++++ docs/development/custom/framework/js.md | 22 ++++++++ 2 files changed, 72 insertions(+) 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} +```