Merge branch 'master' into develop

This commit is contained in:
Julian Lam
2025-02-20 14:09:35 -05:00
7 changed files with 148 additions and 17 deletions

View File

@@ -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

View File

@@ -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",
@@ -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",

View File

@@ -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)) {

View File

@@ -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'],
},
};
@@ -342,23 +344,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': [
@@ -376,7 +379,7 @@ Mocks.actors.category = async (cid) => {
name,
preferredUsername,
summary,
image,
// image, // todo once categories have cover photos
icon,
publicKey: {

View File

@@ -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`) ||

View File

@@ -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.assertAccept(accept) ||
(contentType && activitypub._constants.acceptableTypes.includes(contentType));
if (!pass) {
return next('route');

View File

@@ -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;