Skip to content
Open
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
16 changes: 14 additions & 2 deletions exportMarkdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const Changeset = require('ep_etherpad-lite/static/js/Changeset');
const padManager = require('ep_etherpad-lite/node/db/PadManager');
const readOnlyManager = require('ep_etherpad-lite/node/db/ReadOnlyManager');

const getMarkdownFromAtext = (pad, atext) => {
const apool = pad.apool();
Expand Down Expand Up @@ -311,12 +312,23 @@ const _analyzeLine = (text, aline, apool) => {
};

const getPadMarkdown = async (pad, revNum) => {
const atext = revNum == null ? pad.atext : await pad.getInternalRevisionAText(revNum);
const atext = revNum == null|undefined ? pad.atext : await pad.getInternalRevisionAText(revNum);
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revNum == null|undefined uses the bitwise OR operator, which changes the condition to effectively revNum == 0 and will cause the wrong revision text to be exported. Use a proper null/undefined check (for example revNum == null) before deciding whether to call getInternalRevisionAText().

Suggested change
const atext = revNum == null|undefined ? pad.atext : await pad.getInternalRevisionAText(revNum);
const atext = revNum == null ? pad.atext : await pad.getInternalRevisionAText(revNum);

Copilot uses AI. Check for mistakes.
return getMarkdownFromAtext(pad, atext);
};

const getPadIdIfReadOnly = async (padId) => {
if(padId.startsWith("r.")) {
return await readOnlyManager.getPadId(padId);
} else {
return padId
}
}
Comment on lines +320 to +325
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New getPadIdIfReadOnly() block doesn't match the rest of this file's style (missing spaces after if, uses double quotes, missing semicolons, and missing trailing semicolon after the function). This will likely fail eslint-config-etherpad linting; please align it with the surrounding conventions (single quotes, spaces, semicolons).

Suggested change
if(padId.startsWith("r.")) {
return await readOnlyManager.getPadId(padId);
} else {
return padId
}
}
if (padId.startsWith('r.')) {
return await readOnlyManager.getPadId(padId);
} else {
return padId;
}
};

Copilot uses AI. Check for mistakes.

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

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

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

exports.expressCreateServer = (hookName, {app}) => {
const limiter = rateLimit({
...settings.importExportRateLimiting,
handler: (request) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +
`"${request.originalUrl}" for IP address ${request.ip}`);
}
Comment on lines +10 to +15
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom handler passed to express-rate-limit only logs and never sends a response or calls next(). When the rate limit is exceeded, this will override the default handler and can leave the request unhandled (client hangs or gets no 429 body). Update the handler to use the proper (req, res, next, options) signature and end the response (and/or delegate to the default handler) after logging.

Suggested change
handler: (request) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +
`"${request.originalUrl}" for IP address ${request.ip}`);
}
handler: (request, response, next, options) => {
if (request.rateLimit.current === request.rateLimit.limit + 1) {
// when the rate limiter triggers, write a warning in the logs
console.warn('Import/Export rate limiter triggered on ' +
`"${request.originalUrl}" for IP address ${request.ip}`);
}
options.handler(request, response, next, options);

Copilot uses AI. Check for mistakes.
},
});

app.use('/p/:padId/:revNum?/export/markdown', limiter);
app.get('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
(async () => {
const {padId, revNum} = req.params;
res.attachment(`${padId}.md`);
res.header('Access-Control-Allow-Origin', '*');
Comment on lines +18 to +24
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access-Control-Allow-Origin is set only on the successful export response. If the rate limiter triggers (or an error is thrown before line 24), the response will not include the CORS header and browsers will still report a CORS failure. Consider setting the CORS header in a middleware that runs before the limiter/handler for this route (or ensure the limiter's handler also sets it).

Suggested change
app.use('/p/:padId/:revNum?/export/markdown', limiter);
app.get('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
(async () => {
const {padId, revNum} = req.params;
res.attachment(`${padId}.md`);
res.header('Access-Control-Allow-Origin', '*');
app.use('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
next();
}, limiter);
app.get('/p/:padId/:revNum?/export/markdown', (req, res, next) => {
(async () => {
const {padId, revNum} = req.params;
res.attachment(`${padId}.md`);

Copilot uses AI. Check for mistakes.
res.contentType('plain/text');
res.send(await exportMarkdown.getPadMarkdownDocument(padId, revNum));
})().catch((err) => next(err || new Error(err)));
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"url": "https://github.com/ether/ep_markdown.git"
},
"dependencies": {
"express-rate-limit": "^7.2.0",
"showdown": "*"
},
"contributors": [],
Expand Down