Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion exportMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {StringIterator} from 'ep_etherpad-lite/static/js/StringIterator'
import {StringAssembler} from 'ep_etherpad-lite/static/js/StringAssembler'

const padManager = require('ep_etherpad-lite/node/db/PadManager');
// ReadOnlyManager uses `export default {...}` (ESM-style), so when loaded
// via CommonJS `require` the API lives under `.default`. Unwrap explicitly
// so `readOnlyManager.isReadOnlyId` resolves.
const readOnlyManager = require('ep_etherpad-lite/node/db/ReadOnlyManager').default;

const getMarkdownFromAtext = (pad, atext) => {
const apool = pad.apool();
Expand Down Expand Up @@ -344,8 +348,22 @@ const getPadMarkdown = async (pad, revNum) => {
return getMarkdownFromAtext(pad, atext);
};

// Readonly pad IDs (`r.*`) must be resolved to their underlying pad ID
// before loading — otherwise padManager.getPad creates an empty pad under
// the readonly ID and the export returns nothing. Matches the behavior of
// Etherpad core's /export/:type handler (see importexport.ts).
const resolvePadId = async (padId) => {
if (readOnlyManager.isReadOnlyId(padId)) {
return await readOnlyManager.getPadId(padId);
}
return padId;
};

exports.getPadMarkdownDocument =
async (padId, revNum) => await getPadMarkdown(await padManager.getPad(padId), revNum);
async (padId, revNum) => {
const resolvedId = await resolvePadId(padId);
return await getPadMarkdown(await padManager.getPad(resolvedId), revNum);
};

// copied from ACE
const _REGEX_WORDCHAR = new RegExp([
Expand Down
26 changes: 26 additions & 0 deletions express.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
'use strict';

const exportMarkdown = require('./exportMarkdown');
const settings = require('ep_etherpad-lite/node/utils/Settings');
const rateLimit = require('express-rate-limit');

// Mirrors the rate limiting, CORS header, and readonly-pad handling that
// Etherpad core applies to its native /p/:pad/export/:type routes
// (src/node/hooks/express/importexport.ts). Without these, integrators hit
// browser CORS errors on repeated fetches (issue #139) and the readonly
// `r.*` pad IDs fail because markdown's export path did not resolve them
// to their underlying pad IDs.

exports.expressCreateServer = (hookName, {app}) => {
const limiter = rateLimit({
...settings.importExportRateLimiting,
handler: (request: any) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) {
console.warn('Import/Export rate limiter triggered on ' +
`"${request.originalUrl}" for IP address ${request.ip}`);
}
},
});

// Apply the core rate limiter to both with-revision and without-revision
// markdown export endpoints, matching the pattern used by Etherpad core.
app.use('/p/:padId/export/markdown', limiter);
app.use('/p/:padId/:revNum/export/markdown', limiter);

app.get('/p/:padId/export/markdown', async (req: any, res: any, next: any) => {
try {
const {padId} = req.params;
res.header('Access-Control-Allow-Origin', '*');
res.attachment(`${padId}.md`);
res.contentType('plain/text');
res.send(await exportMarkdown.getPadMarkdownDocument(padId));
Expand All @@ -17,6 +42,7 @@ exports.expressCreateServer = (hookName, {app}) => {
app.get('/p/:padId/:revNum/export/markdown', async (req: any, res: any, next: any) => {
try {
const {padId, revNum} = req.params;
res.header('Access-Control-Allow-Origin', '*');
res.attachment(`${padId}.md`);
res.contentType('plain/text');
res.send(await exportMarkdown.getPadMarkdownDocument(padId, revNum));
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"express-rate-limit": "^8.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"ep_headings2": {
"optional": true
Expand Down
Loading
Loading