diff --git a/CHANGELOG.md b/CHANGELOG.md index de09a109c..e51ea1f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ version, see the upgrade guides in `docs/`. - **router:** The text after `**` is matched literally, e.g. `/f/**/x.json` doesn't match `/f/a/xajson` anymore, and params after `**` can have any name, e.g. `:my-id`, which threw an `Invalid capture group name` error (#333). - **router:** Params starting with `$` keep their names next to wildcards, e.g. `$x` in `/a/*/:$x`, instead of being renamed to `$` or `$$` and replacing the wildcard's value (#334). - **admin:** Everything behind the topmost dialog is inert: the page, the navigation, the sidebar and other dialogs. Select All in a dialog no longer selects the values behind it, and assistive technology can't reach them either. +- **server:** `toJSON()` signs copies of the asset files inside JSON columns, e.g. `content.header.file`, where it replaced the record's own file objects with the signed copies (#388). - **utils:** `isUrl()` returns `false` for invalid URLs with long paths right away, where the time doubled with each path segment, taking seconds for 28 segments. URLs ending in a lone `?` or `#` are valid too (#348). - **utils:** `clone()` copies typed arrays, and Node.js buffers stay buffers, where it copied their elements one by one into a plain object (#350). - **utils:** `deindent` interpolates `null` and `undefined` as in template literals, where it threw a `TypeError` (#352). diff --git a/packages/server/src/models/Model.js b/packages/server/src/models/Model.js index 5c81648bd..0ac8485e7 100644 --- a/packages/server/src/models/Model.js +++ b/packages/server/src/models/Model.js @@ -13,7 +13,6 @@ import { parseDataPath, normalizeDataPath, getEntriesAtDataPath, - setValueAtDataPath, groupBy, assignDeeply, formatPlainDate, @@ -1108,10 +1107,23 @@ export class Model extends objection.Model { static _mapAssetFiles(json, callback) { const { assets } = this.definition if (assets) { - // Write each file back to its own location: + // Write each file back to its own location, into copies of the objects + // along its path, as nested objects are shared with the model: + const copies = new Set() for (const { file, path, dataPath } of this._getAssetFiles(json)) { const storage = this.app.getStorage(assets[dataPath].storage) - setValueAtDataPath(json, path, callback(file, storage)) + const parts = parseDataPath(path) + const last = parts.pop() + let parent = json + for (const part of parts) { + let value = parent[part] + if (!copies.has(value)) { + value = parent[part] = isArray(value) ? [...value] : { ...value } + copies.add(value) + } + parent = value + } + parent[last] = callback(file, storage) } } } diff --git a/tests/server/assets.test.ts b/tests/server/assets.test.ts index 2e8d27c5e..920d3fc07 100644 --- a/tests/server/assets.test.ts +++ b/tests/server/assets.test.ts @@ -1309,6 +1309,11 @@ describe('asset references', () => { expect(storage.verifyAssetFile(signedFile)).toBe(true) } expect(content.footer.file).toBeNull() + // Sign copies, leaving the files of the record untouched: + for (const loadedFile of [header.file, ...gallery.files]) { + expect(loadedFile).toBeInstanceOf(AssetFile) + expect(loadedFile).not.toHaveProperty('signature') + } }) it('recounts each location of overlapping data paths once', async () => {