From b8200095c011ac363e287742033d941ef57f3d8a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 11:38:47 -0500 Subject: [PATCH 1/7] fix: #13129, serve category backgroundImage as actor `icon`, not `image` + tests for category actor --- src/activitypub/mocks.js | 21 ++++++------ test/activitypub.js | 73 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 003d392333..223d1a8cbd 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -341,23 +341,24 @@ Mocks.actors.category = async (cid) => { } = await categories.getCategoryData(cid); const publicKey = await activitypub.getPublicKey('cid', cid); - let image; + let icon; if (backgroundImage) { const filename = path.basename(utils.decodeHTMLEntities(backgroundImage)); - image = { + icon = { type: 'Image', mediaType: mime.getType(filename), url: `${nconf.get('url')}${utils.decodeHTMLEntities(backgroundImage)}`, }; + } else { + icon = await categories.icons.get(cid); + icon = icon.get('png'); + icon = { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}${icon}`, + }; } - let icon = await categories.icons.get(cid); - icon = icon.get('png'); - icon = { - type: 'Image', - mediaType: 'image/png', - url: `${nconf.get('url')}${icon}`, - }; return { '@context': [ @@ -375,7 +376,7 @@ Mocks.actors.category = async (cid) => { name, preferredUsername, summary, - image, + // image, // todo once categories have cover photos icon, publicKey: { diff --git a/test/activitypub.js b/test/activitypub.js index aee20d8037..d2c2334140 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -350,6 +350,79 @@ describe('ActivityPub integration', () => { }); }); + describe.only('Category Actor endpoint', () => { + let cid; + let slug; + let description; + + beforeEach(async () => { + slug = slugify(utils.generateUUID().slice(0, 8)); + description = utils.generateUUID(); + ({ cid } = await categories.create({ + name: slug, + description, + })); + }); + + it('should return a valid ActivityPub Actor JSON-LD payload', async () => { + const { response, body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(response); + assert.strictEqual(response.statusCode, 200); + assert(body.hasOwnProperty('@context')); + assert(body['@context'].includes('https://www.w3.org/ns/activitystreams')); + + ['id', 'url', /* 'followers', 'following', */ 'inbox', 'outbox'].forEach((prop) => { + assert(body.hasOwnProperty(prop)); + assert(body[prop]); + }); + + assert.strictEqual(body.id, `${nconf.get('url')}/category/${cid}`); + assert.strictEqual(body.type, 'Group'); + assert.strictEqual(body.summary, description); + assert.deepStrictEqual(body.icon, { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}/assets/uploads/category/category-${cid}-icon.png`, + }); + }); + + it('should contain a `publicKey` property with a public key', async () => { + const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert(body.hasOwnProperty('publicKey')); + assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop))); + }); + + it('should serve the the backgroundImage in `icon` if set', async () => { + const payload = {}; + payload[cid] = { + backgroundImage: `/assets/uploads/files/test.png`, + }; + await categories.update(payload); + + const { body } = await request.get(`${nconf.get('url')}/category/${cid}`, { + headers: { + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + }, + }); + + assert.deepStrictEqual(body.icon, { + type: 'Image', + mediaType: 'image/png', + url: `${nconf.get('url')}/assets/uploads/files/test.png`, + }); + }); + }); + describe('Instance Actor endpoint', () => { let response; let body; From 7520e4f64de58ab195dd9a78b15dc4945d6fb052 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 11:54:24 -0500 Subject: [PATCH 2/7] chore: bump composer to 10.2.46 for #13132 --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index a1a3f5b729..715b472581 100644 --- a/install/package.json +++ b/install/package.json @@ -99,7 +99,7 @@ "multiparty": "4.2.3", "nconf": "0.12.1", "nodebb-plugin-2factor": "7.5.9", - "nodebb-plugin-composer-default": "10.2.45", + "nodebb-plugin-composer-default": "10.2.46", "nodebb-plugin-dbsearch": "6.2.12", "nodebb-plugin-emoji": "6.0.2", "nodebb-plugin-emoji-android": "4.1.1", @@ -200,4 +200,4 @@ "url": "https://github.com/barisusakli" } ] -} \ No newline at end of file +} From 93f48409c5aa297b7ce75ea901f07013ad90b57c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 12:24:17 -0500 Subject: [PATCH 3/7] fix: #13136, do not log 404s for AP requests --- src/activitypub/helpers.js | 5 +++++ src/controllers/404.js | 7 +++++++ src/middleware/activitypub.js | 6 ++---- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/activitypub/helpers.js b/src/activitypub/helpers.js index 5a50e2e8ed..79aebc25dc 100644 --- a/src/activitypub/helpers.js +++ b/src/activitypub/helpers.js @@ -64,6 +64,11 @@ Helpers.isUri = (value) => { }); }; +Helpers.assertAccept = accept => (accept && accept.split(',').some((value) => { + const parts = value.split(';').map(v => v.trim()); + return activitypub._constants.acceptableTypes.includes(value || parts[0]); +})); + Helpers.isWebfinger = (value) => { // N.B. returns normalized handle, so truthy check! if (webfingerRegex.test(value) && !Helpers.isUri(value)) { diff --git a/src/controllers/404.js b/src/controllers/404.js index becc206e76..bed1a085e3 100644 --- a/src/controllers/404.js +++ b/src/controllers/404.js @@ -6,6 +6,7 @@ const validator = require('validator'); const meta = require('../meta'); const plugins = require('../plugins'); +const activitypub = require('../activitypub'); const middleware = require('../middleware'); const helpers = require('../middleware/helpers'); const { secureRandom } = require('../utils'); @@ -24,6 +25,12 @@ exports.handle404 = helpers.try(async (req, res) => { if (isClientScript.test(req.url)) { res.type('text/javascript').status(404).send('Not Found'); + } else if ( + activitypub.helpers.assertAccept(req.headers.accept) || + (req.headers['Content-Type'] && activitypub._constants.acceptableTypes.includes(req.headers['Content-Type'])) + ) { + // todo: separate logging of AP 404s + res.sendStatus(404); } else if ( !res.locals.isAPI && ( req.path.startsWith(`${relativePath}/assets/uploads`) || diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index f9b8dcd009..ee7d8a2460 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -16,10 +16,8 @@ middleware.assertS2S = async function (req, res, next) { return next('route'); } - const pass = (accept && accept.split(',').some((value) => { - const parts = value.split(';').map(v => v.trim()); - return activitypub._constants.acceptableTypes.includes(value || parts[0]); - })) || (contentType && activitypub._constants.acceptableTypes.includes(contentType)); + const pass = activitypub.helpers.assertAccepts(accept) || + (contentType && activitypub._constants.acceptableTypes.includes(contentType)); if (!pass) { return next('route'); From e63f1234a7fa4c5d9d9be7dafd07be498dcbe1ee Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 12:50:05 -0500 Subject: [PATCH 4/7] fix: typo --- src/middleware/activitypub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index ee7d8a2460..05f67d3338 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -16,7 +16,7 @@ middleware.assertS2S = async function (req, res, next) { return next('route'); } - const pass = activitypub.helpers.assertAccepts(accept) || + const pass = activitypub.helpers.assertAccept(accept) || (contentType && activitypub._constants.acceptableTypes.includes(contentType)); if (!pass) { From 4fe23e5c85c6cbba6905f275b75d61a8f3a95c6f Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 20 Feb 2025 18:17:39 +0000 Subject: [PATCH 5/7] chore: incrementing version number - v4.0.5 --- install/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/package.json b/install/package.json index 715b472581..23cdac6288 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.0.4", + "version": "4.0.5", "homepage": "https://www.nodebb.org", "repository": { "type": "git", @@ -200,4 +200,4 @@ "url": "https://github.com/barisusakli" } ] -} +} \ No newline at end of file From f84b9fc75b1193c6efe4390f45a2b58afcfccb24 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Thu, 20 Feb 2025 18:17:39 +0000 Subject: [PATCH 6/7] chore: update changelog for v4.0.5 --- CHANGELOG.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8de9449cc..34402b38c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +#### v4.0.5 (2025-02-20) + +##### Chores + +* bump composer to 10.2.46 for #13132 (7520e4f6) +* up harmony (f82f00e5) +* up widgets (e23a14c1) +* up harmony (c0996a80) +* up dbsearch (d0a9ddea) +* up dbsearch (310fab65) +* add test helper to activitypub file (4bc0031f) +* incrementing version number - v4.0.4 (b1125cce) +* update changelog for v4.0.4 (d3b69a39) +* incrementing version number - v4.0.3 (2b65c735) +* incrementing version number - v4.0.2 (73fe5fcf) +* incrementing version number - v4.0.1 (a461b758) +* incrementing version number - v4.0.0 (c1eaee45) + +##### New Features + +* add upload button to quickreply (f67a0a12) +* remove activities older than a week (9997189a) + +##### Bug Fixes + +* typo (e63f1234) +* #13136, do not log 404s for AP requests (93f48409) +* #13129, serve category backgroundImage as actor `icon`, not `image` (b8200095) +* escape ip blacklist rules (625f4751) +* closes #13180, don't execute cron jobs if ap disabled (a410587c) +* #13172, Topics.addParentPosts not sending sourceContent in calling parsePosts (bb9687bd) +* #13179, fix context resolution failure bug with frequency (6245e33d) +* add back chronological sorting of asserted notes (de6e63bb) +* #13170, remove mime-type and regex test for "Emoji" attachment, wrap tag name in colons if not provided (92708d2f) +* closes #13176, check if uid is number when creating tokens (80cc1d34) +* notes.assertPrivate sanity checks (5e71d597) +* page index for single page, closes #13173 (b0e8058f) +* remove handle on category purge (4134a075) + +##### Tests + +* dont clear local when testing (669755d1) +* show objects on fail (f2824073) +* wait after post request (64318242) + #### v4.0.4 (2025-02-17) ##### Chores From 2ad48f1714fa5281657f0e86e60255e90d669f24 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 20 Feb 2025 14:07:39 -0500 Subject: [PATCH 7/7] fix: relaxing strict allowedTags configuration for incoming AP content (allowing picture, source, and additional attributes for img) re: #13185 --- src/activitypub/mocks.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/activitypub/mocks.js b/src/activitypub/mocks.js index 223d1a8cbd..4a2fa1e9b2 100644 --- a/src/activitypub/mocks.js +++ b/src/activitypub/mocks.js @@ -29,12 +29,14 @@ const Mocks = module.exports; * Done so the output HTML is stripped of all non-essential items; mainly classes from plugins.. */ const sanitizeConfig = { - allowedTags: sanitize.defaults.allowedTags.concat(['img']), + allowedTags: sanitize.defaults.allowedTags.concat(['img', 'picture', 'source']), allowedClasses: { '*': [], }, allowedAttributes: { a: ['href', 'rel'], + source: ['type', 'src', 'srcset', 'sizes', 'media', 'height', 'width'], + img: ['alt', 'height', 'ismap', 'src', 'usemap', 'width', 'srcset'], }, };