diff --git a/CHANGELOG.md b/CHANGELOG.md index 680a5faa9..a5d7098ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ (low-level API) and `ep_etherpad-lite/static/js/AttributeMap` (high-level API). * The `import` server-side hook has a new `ImportError` context property. +* New `exportEtherpad` and `importEtherpad` server-side hooks. * The `handleMessageSecurity` and `handleMessage` server-side hooks have a new `sessionInfo` context property that includes the user's author ID, the pad ID, and whether the user only has read-only access. diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index b51ed5877..5998c247e 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -966,6 +966,48 @@ Example: exports.exportEtherpadAdditionalContent = () => ['comments']; ``` +## `exportEtherpad` + +Called from `src/node/utils/ExportEtherpad.js`. + +Called when exporting to an `.etherpad` file. + +Context properties: + + * `pad`: The exported pad's Pad object. + * `data`: JSONable output object. This is pre-populated with records from core + Etherpad as well as pad-specific records with prefixes from the + `exportEtherpadAdditionalContent` hook. Registered hook functions can modify + this object (but not replace the object) to perform any desired + transformations to the exported data (such as the inclusion of + plugin-specific records). All registered hook functions are executed + concurrently, so care should be taken to avoid race conditions with other + plugins. + * `dstPadId`: The pad ID that should be used when writing pad-specific records + to `data` (instead of `pad.id`). This avoids leaking the writable pad ID + when a user exports a read-only pad. This might be a dummy value; plugins + should not assume that it is either the pad's real writable ID or its + read-only ID. + +## `importEtherpad` + +Called from `src/node/utils/ImportEtherpad.js`. + +Called when importing from an `.etherpad` file. + +Context properties: + + * `pad`: Temporary Pad object containing the pad's data read from the imported + `.etherpad` file. The `pad.db` object is a temporary in-memory database + whose records will be copied to the real database after they are validated + (see the `padCheck` hook). Registered hook functions MUST NOT use the real + database to access (read or write) pad-specific records; they MUST instead + use `pad.db`. All registered hook functions are executed concurrently, so + care should be taken to avoid race conditions with other plugins. + * `data`: Raw JSONable object from the `.etherpad` file. This data must not be + modified. + * `srcPadId`: The pad ID used for the pad-specific information in `data`. + ## `import` Called from: `src/node/handler/ImportHandler.js` diff --git a/src/node/utils/ExportEtherpad.js b/src/node/utils/ExportEtherpad.js index 0cc3e336c..e20739ad3 100644 --- a/src/node/utils/ExportEtherpad.js +++ b/src/node/utils/ExportEtherpad.js @@ -55,5 +55,10 @@ exports.getPadRaw = async (padId, readOnlyId) => { })(); const data = {[dstPfx]: pad}; for (const [dstKey, p] of new Stream(records).batch(100).buffer(99)) data[dstKey] = await p; + await hooks.aCallAll('exportEtherpad', { + pad, + data, + dstPadId: readOnlyId || padId, + }); return data; }; diff --git a/src/node/utils/ImportEtherpad.js b/src/node/utils/ImportEtherpad.js index 2c7854936..da7e750ff 100644 --- a/src/node/utils/ImportEtherpad.js +++ b/src/node/utils/ImportEtherpad.js @@ -90,7 +90,8 @@ exports.setPadRaw = async (padId, r, authorId = '') => { keyParts[1] = padId; key = keyParts.join(':'); } else { - logger.warn(`(pad ${padId}) Ignoring record with unsupported key: ${key}`); + logger.debug(`(pad ${padId}) The record with the following key will be ignored unless an ` + + `importEtherpad hook function processes it: ${key}`); return; } await padDb.set(key, value); @@ -100,6 +101,13 @@ exports.setPadRaw = async (padId, r, authorId = '') => { const pad = new Pad(padId, padDb); await pad.init(null, authorId); + await hooks.aCallAll('importEtherpad', { + pad, + // Shallow freeze meant to prevent accidental bugs. It would be better to deep freeze, but + // it's not worth the added complexity. + data: Object.freeze(records), + srcPadId: originalPadId, + }); await pad.check(); } finally { await padDb.close();