ep_rss 11.0.24 → 11.0.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,51 @@
1
- # RSS Feeds of Pad Changes for Etherpad
1
+ # RSS feeds for Etherpad pads
2
2
 
3
- ![Publish Status](https://github.com/ether/ep_rss/workflows/Node.js%20Package/badge.svg) [![Backend Tests Status](https://github.com/ether/ep_rss/actions/workflows/test-and-release.yml/badge.svg)](https://github.com/ether/ep_rss/actions/workflows/test-and-release.yml)
3
+ [![npm](https://img.shields.io/npm/v/ep_rss.svg)](https://www.npmjs.com/package/ep_rss)
4
+ [![Backend Tests](https://github.com/ether/ep_rss/actions/workflows/backend-tests.yml/badge.svg?branch=main)](https://github.com/ether/ep_rss/actions/workflows/backend-tests.yml)
5
+ [![Frontend Tests](https://github.com/ether/ep_rss/actions/workflows/frontend-tests.yml/badge.svg?branch=main)](https://github.com/ether/ep_rss/actions/workflows/frontend-tests.yml)
4
6
 
5
- Exposes an RSS feed for each pad at `/p/{padId}/feed`.
7
+ Adds an RSS 2.0 feed for every pad so readers can subscribe to changes
8
+ in any feed reader.
6
9
 
7
10
  ## Install
8
11
 
9
- ```
12
+ From your Etherpad root, install the plugin via the Etherpad plugin
13
+ manager:
14
+
15
+ ```sh
10
16
  pnpm run plugins i ep_rss
11
17
  ```
12
18
 
19
+ Or install from the admin UI: **Admin → Manage Plugins**, search for
20
+ `ep_rss`, click *Install*. Restart Etherpad after installing.
21
+
22
+ > ⚠️ Don't run `npm i` / `pnpm add` against this plugin from inside
23
+ > the Etherpad source tree — Etherpad tracks installed plugins
24
+ > through its own plugin manager, and hand-editing `package.json`
25
+ > can leave the server unable to start.
26
+
27
+ ## Endpoints
28
+
29
+ Once installed, each pad exposes:
30
+
31
+ | Path | Behavior |
32
+ |---|---|
33
+ | `/p/<pad>/feed` | 200 with `Content-Type: application/rss+xml` and the current pad text as a single `<item>` |
34
+ | `/p/<pad>/rss` | 302 → `/p/<pad>/feed` |
35
+ | `/p/<pad>/feed.rss` | 302 → `/p/<pad>/feed` |
36
+ | `/p/<pad>/atom.xml` | 302 → `/p/<pad>/feed` |
37
+
38
+ The pad page itself also advertises the feed in `<head>` so feed
39
+ readers auto-discover it:
40
+
41
+ ```html
42
+ <link rel="alternate" type="application/rss+xml" title="Pad RSS Feed" href="feed" />
43
+ ```
44
+
13
45
  ## Settings
14
46
 
15
- Optional stale time (milliseconds before a new RSS item is generated) can be set in `settings.json`:
47
+ Optional. Add to `settings.json` to control how long a generated feed
48
+ is cached in memory before being regenerated:
16
49
 
17
50
  ```json
18
51
  "rss": {
@@ -20,7 +53,19 @@ Optional stale time (milliseconds before a new RSS item is generated) can be set
20
53
  }
21
54
  ```
22
55
 
23
- Defaults to 5 minutes if not configured.
56
+ `staleTime` is in milliseconds. Defaults to 5 minutes
57
+ (`300000`) when unset.
58
+
59
+ ## Development
60
+
61
+ ```sh
62
+ pnpm install
63
+ pnpm run lint
64
+ ```
65
+
66
+ Backend and frontend tests live under `static/tests/` and run inside
67
+ an Etherpad checkout — the CI workflows install this plugin into a
68
+ fresh Etherpad and exercise it end-to-end.
24
69
 
25
70
  ## License
26
71
 
package/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const API = require('ep_etherpad-lite/node/db/API.js');
3
+ const API = require('ep_etherpad-lite/node/db/API');
4
4
  const padManager = require('ep_etherpad-lite/node/db/PadManager');
5
5
  const settings = require('ep_etherpad-lite/node/utils/Settings');
6
6
 
@@ -20,33 +20,16 @@ exports.eejsBlock_htmlHead = (hookName, args, cb) => {
20
20
  };
21
21
 
22
22
  exports.registerRoute = (hookName, args, cb) => {
23
- args.app.get('/p/*/rss', (req, res) => {
24
- /* Sanity is in the crack of time*/
25
- const path = req.url.split('/');
26
- const padId = path[2];
27
- res.redirect(`/p/${padId}/feed`);
28
- });
29
-
30
- args.app.get('/p/*/feed.rss', (req, res) => {
31
- /* Sanity is in the crack of time*/
32
- const path = req.url.split('/');
33
- const padId = path[2];
34
- res.redirect(`/p/${padId}/feed`);
35
- });
36
-
37
-
38
- args.app.get('/p/*/atom.xml', (req, res) => {
39
- /* Sanity is in the crack of time*/
40
- const path = req.url.split('/');
41
- const padId = path[2];
42
- res.redirect(`/p/${padId}/feed`);
43
- });
44
-
45
- args.app.get('/p/*/feed', async (req, res) => {
46
- /* Sanity is in the cracks of lime*/
23
+ const redirectToFeed = (req, res) => {
24
+ res.redirect(`/p/${encodeURIComponent(req.params.padId)}/feed`);
25
+ };
26
+ args.app.get('/p/:padId/rss', redirectToFeed);
27
+ args.app.get('/p/:padId/feed.rss', redirectToFeed);
28
+ args.app.get('/p/:padId/atom.xml', redirectToFeed);
29
+
30
+ args.app.get('/p/:padId/feed', async (req, res) => {
47
31
  const fullURL = `${req.protocol}://${req.get('host')}${req.url}`;
48
- const path = req.url.split('/');
49
- const padId = path[2];
32
+ const padId = req.params.padId;
50
33
  const padURL = `${req.protocol}://${req.get('host')}/p/${padId}`;
51
34
  const dateString = new Date();
52
35
  let isPublished = false; // is this item already published?
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ep_rss",
3
3
  "description": "Get an RSS feed of pad updates",
4
- "version": "11.0.24",
4
+ "version": "11.0.26",
5
5
  "author": {
6
6
  "name": "John McLear",
7
7
  "email": "john@mclear.co.uk",
@@ -0,0 +1,54 @@
1
+ import {expect, test} from '@playwright/test';
2
+ import {goToNewPad, writeToPad} from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper';
3
+
4
+ const BASE_URL = 'http://localhost:9001';
5
+
6
+ const padIdFromUrl = (url: string) => url.split('/p/')[1].split('?')[0];
7
+
8
+ test.describe('ep_rss feed', () => {
9
+ test('declares the RSS alternate link in the pad page head', async ({page}) => {
10
+ await goToNewPad(page);
11
+ const link = page.locator('head link[rel="alternate"][type="application/rss+xml"]');
12
+ await expect(link).toHaveAttribute('href', 'feed');
13
+ });
14
+
15
+ test('serves an RSS document at /p/<pad>/feed containing the pad text', async ({page, request}) => {
16
+ await goToNewPad(page);
17
+ const padId = padIdFromUrl(page.url());
18
+ const marker = `ep-rss-marker-${Date.now()}`;
19
+ await writeToPad(page, marker);
20
+ // Give the socket time to flush the change to the pad store before the
21
+ // route reads it back via API.getLastEdited / padManager.getPad.
22
+ await page.waitForTimeout(1200);
23
+
24
+ const res = await request.get(`${BASE_URL}/p/${padId}/feed`);
25
+ expect(res.status()).toBe(200);
26
+ expect(res.headers()['content-type']).toContain('application/rss+xml');
27
+ const body = await res.text();
28
+ expect(body).toMatch(/^<rss /);
29
+ expect(body).toContain(`<title>${padId}</title>`);
30
+ expect(body).toContain(marker);
31
+ expect(body.trim().endsWith('</rss>')).toBe(true);
32
+ });
33
+
34
+ test('escapes HTML special characters in the description', async ({page, request}) => {
35
+ await goToNewPad(page);
36
+ const padId = padIdFromUrl(page.url());
37
+ await writeToPad(page, 'tags <b>&</b>');
38
+ await page.waitForTimeout(1200);
39
+
40
+ const body = await (await request.get(`${BASE_URL}/p/${padId}/feed`)).text();
41
+ expect(body).toContain('&lt;b&gt;&amp;&lt;/b&gt;');
42
+ expect(body).not.toContain('<b>&</b>');
43
+ });
44
+
45
+ for (const alias of ['rss', 'feed.rss', 'atom.xml']) {
46
+ test(`/p/<pad>/${alias} redirects to /p/<pad>/feed`, async ({page, request}) => {
47
+ await goToNewPad(page);
48
+ const padId = padIdFromUrl(page.url());
49
+ const res = await request.get(`${BASE_URL}/p/${padId}/${alias}`, {maxRedirects: 0});
50
+ expect(res.status()).toBe(302);
51
+ expect(res.headers()['location']).toBe(`/p/${padId}/feed`);
52
+ });
53
+ }
54
+ });