From 633fa1407d523dfe13fa466380c0754fb2d03bf6 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 8 Apr 2026 15:37:04 +0000 Subject: [PATCH 01/10] chore: incrementing version number - v4.10.2 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 01011ea150..78456960cc 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "4.10.1", + "version": "4.10.2", "homepage": "https://www.nodebb.org", "repository": { "type": "git", From 64c6417cecb06436f4b102b5f708fb68114b44a8 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Wed, 8 Apr 2026 15:37:05 +0000 Subject: [PATCH 02/10] chore: update changelog for v4.10.2 --- CHANGELOG.md | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8058f2350d..8f1cfa95ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,75 @@ +#### v4.10.2 (2026-04-08) + +##### Chores + +* incrementing version number - v4.10.1 (0c9bbcea) +* update changelog for v4.10.1 (afe8683e) +* incrementing version number - v4.10.0 (5b703104) +* incrementing version number - v4.9.2 (e6846052) +* incrementing version number - v4.9.1 (72e44c86) +* incrementing version number - v4.9.0 (3fdd1bef) +* incrementing version number - v4.8.1 (713ae0c0) +* incrementing version number - v4.8.0 (3fac737a) +* incrementing version number - v4.7.2 (cd419d8a) +* incrementing version number - v4.7.1 (afb88805) +* incrementing version number - v4.7.0 (e82d40f8) +* incrementing version number - v4.6.3 (9fc5b0f3) +* incrementing version number - v4.6.2 (f98747db) +* incrementing version number - v4.6.1 (f47aa678) +* incrementing version number - v4.6.0 (ee395bc5) +* incrementing version number - v4.5.2 (ad2da639) +* incrementing version number - v4.5.1 (69f4b61f) +* incrementing version number - v4.5.0 (f05c5d06) +* incrementing version number - v4.4.6 (074043ad) +* incrementing version number - v4.4.5 (6f106923) +* incrementing version number - v4.4.4 (d323af44) +* incrementing version number - v4.4.3 (d354c2eb) +* incrementing version number - v4.4.2 (55c510ae) +* incrementing version number - v4.4.1 (5ae79b4e) +* incrementing version number - v4.4.0 (0a75eee3) +* incrementing version number - v4.3.2 (b92b5d80) +* incrementing version number - v4.3.1 (308e6b9f) +* incrementing version number - v4.3.0 (bff291db) +* incrementing version number - v4.2.2 (17fecc24) +* incrementing version number - v4.2.1 (852a270c) +* incrementing version number - v4.2.0 (87581958) +* incrementing version number - v4.1.1 (b2afbb16) +* incrementing version number - v4.1.0 (36c80850) +* incrementing version number - v4.0.6 (4a52fb2e) +* incrementing version number - v4.0.5 (1792a62b) +* incrementing version number - v4.0.4 (b1125cce) +* 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 unreadNids to /api/notifications (83572348) + +##### Bug Fixes + +* use file.exists instead of try/catch to detect missing email logo (#14154) (4366bdd0) +* closes #14151, handle null req.body (62b65e69) +* remove optional (20e751f0) +* #14147, dont create wrong backlinks (0568ef43) +* user image og:image (fb48ab34) +* closes #14133, don't modify displayName for system groups (af0e3d96) +* try a save point in retry (203f4cc7) +* try upsert type if it fails (991e9778) +* on exit, dont write analytics data on all nodes (6c4e9284) +* align-center user and name on post queue (1a0c2a21) +* ./nodebb upgrade on windows (82d380a3) + +##### Refactors + +* use renderHeaderType instead of two variables (55290da0) +* break long line (4b503db4) + +##### Tests + +* dont create users parallel (b04976ed) + #### v4.10.1 (2026-03-25) ##### Chores From 71c6132f992f6cd8cffd5bdafa8d56cc3e4d2998 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 10 Apr 2026 11:47:32 -0400 Subject: [PATCH 03/10] chore: up persona --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 78456960cc..eb758b636d 100644 --- a/install/package.json +++ b/install/package.json @@ -111,7 +111,7 @@ "nodebb-theme-harmony": "2.2.63", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.58", - "nodebb-theme-persona": "14.2.34", + "nodebb-theme-persona": "14.2.35", "nodebb-widget-essentials": "7.0.43", "nodemailer": "8.0.3", "nprogress": "0.2.0", From 648344a2c94e5c9a6c9a8402e172944aa6a6f8c4 Mon Sep 17 00:00:00 2001 From: omprakash kumar Date: Fri, 10 Apr 2026 22:37:20 +0530 Subject: [PATCH 04/10] docs: add development setup overview for contributors (#14132) Adds a minimal development setup overview section, including native and Docker-based approaches for running NodeBB locally. This does not replace existing documentation but provides a quick entry point for contributors. --- README.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.md b/README.md index dbf2ff306f..e508592149 100644 --- a/README.md +++ b/README.md @@ -43,12 +43,49 @@ NodeBB requires the following software to be installed: * MongoDB, version 5 or greater **or** Redis, version 7.2 or greater * If you are using [clustering](https://docs.nodebb.org/configuring/scaling/) you need Redis installed and configured. * nginx, version 1.3.13 or greater (**only if** intending to use nginx to proxy requests to a NodeBB) +* (Optional) [Docker](https://docs.docker.com/get-docker/) for container-based setup +> Installation steps vary by operating system. Please follow the official documentation links above. + ## Installation [Please refer to platform-specific installation documentation](https://docs.nodebb.org/installing/os). If installing via the cloud (or using Docker), [please see cloud-based installation documentation](https://docs.nodebb.org/installing/cloud/). +## Development Setup Overview + +> NodeBB uses a CLI-based setup and does not run via standard `npm start`. + +You can run NodeBB locally in two ways: + +### Option 1: Native Setup (Recommended for Beginners & Contributors) + +This approach helps you understand how NodeBB works internally. + +**Basic flow:** +1. Clone the repository ```` https://github.com/NodeBB/NodeBB.git ```` +2. Run the setup script ```` cd NodeBB ```` ```` ./nodebb setup ```` +3. Start the application ```` ./nodebb start ```` + +**During setup, you will configure:** + - Database (MongoDB / Redis) + - Admin account + - Port (default: 4567) + +### Option 2: Docker Setup (Quick & Isolated) + +> Requires Docker to be installed: https://docs.docker.com/get-docker/ + +Run: + +```bash +docker-compose up +```` + +This will start NodeBB along with required services at: ```` http://localhost:4567 ```` + +**For more details, see: https://docs.nodebb.org** + ## Securing NodeBB It is important to ensure that your NodeBB and database servers are secured. Bear these points in mind: From 15d65943fb178e7446f498065d608cd9fd403ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20U=C5=9Fakl=C4=B1?= Date: Fri, 10 Apr 2026 13:05:29 -0400 Subject: [PATCH 05/10] test: fix unread deleted topic test (#14164) instead of posting and immediately deleting, add a deleted param to topics.post which deletes the topic before sending notifications --- src/topics/create.js | 7 ++++--- test/topics.js | 10 ++++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/topics/create.js b/src/topics/create.js index d2b709333c..ef94fdaf42 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -167,14 +167,15 @@ module.exports = function (Topics) { topicData.index = 0; postData.index = 0; - if (topicData.scheduled) { - await Topics.delete(tid); + if (data.deleted || topicData.scheduled) { + await Topics.delete(tid, uid); + topicData.deleted = true; } analytics.increment(['topics', `topics:byCid:${topicData.cid}`]); plugins.hooks.fire('action:topic.post', { topic: topicData, post: postData, data: data }); - if (!topicData.scheduled) { + if (!topicData.scheduled && !topicData.deleted) { setImmediate(async () => { try { if (utils.isNumber(uid)) { diff --git a/test/topics.js b/test/topics.js index f67e0fc40e..479167717a 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1420,8 +1420,14 @@ describe('Topic\'s', () => { it('should not return topic as unread if topic is deleted', async () => { const uid = await User.create({ username: 'regularJoe' }); - const result = await topics.post({ uid: adminUid, title: 'deleted unread', content: 'not unread', cid: categoryObj.cid }); - await topics.delete(result.topicData.tid, adminUid); + const result = await topics.post({ + uid: adminUid, + title: 'deleted unread', + content: 'not unread', + cid: categoryObj.cid, + deleted: 1, + }); + const unreadTids = await topics.getUnreadTids({ cid: 0, uid: uid }); await sleep(2000); From ab6d462becbf20d3d3ecee7eefeac9162f55702d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 10 Apr 2026 16:27:47 -0400 Subject: [PATCH 06/10] test: another delete after create --- test/categories.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/categories.js b/test/categories.js index 40c3039310..c204a00768 100644 --- a/test/categories.js +++ b/test/categories.js @@ -197,13 +197,13 @@ describe('Categories', () => { content: 'The content of test topic', tags: ['nodebb'], }); - const data = await Topics.post({ + await Topics.post({ uid: posterUid, cid: categoryObj.cid, title: 'will delete', content: 'The content of deleted topic', + deleted: 1, }); - await Topics.delete(data.topicData.tid, adminUid); }); it('should get recent replies in category', (done) => { From ac8bad8bc95394e27445b696515e3d115373bca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 10 Apr 2026 16:43:14 -0400 Subject: [PATCH 07/10] test: set thumbs during topic.post --- test/topics/thumbs.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/topics/thumbs.js b/test/topics/thumbs.js index 85517588a0..657a2e7e60 100644 --- a/test/topics/thumbs.js +++ b/test/topics/thumbs.js @@ -64,15 +64,11 @@ describe('Topic thumbs', () => { cid: categoryObj.cid, title: 'Test Topic Title', content: 'The content of test topic', + thumbs: [relativeThumbPaths[0]], }); // Touch a couple files and associate it to a topic createFiles(); - - await topics.setTopicFields(topicObj.topicData.tid, { - numThumbs: 1, - thumbs: JSON.stringify([relativeThumbPaths[0]]), - }); }); it('should return bool for whether a thumb exists', async () => { From c40708f4ca3fe9f8633985e3009009d74fff7a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 13 Apr 2026 21:14:45 -0400 Subject: [PATCH 08/10] fix: memberPostCids saving, closes #14170 validCids is an array of strings if a group was named 3rd party devs, then the isNaN(parseInt()) check was failing --- src/groups/update.js | 2 +- src/privileges/helpers.js | 2 +- test/groups.js | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/groups/update.js b/src/groups/update.js index 804268c587..caaedf38e4 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -77,7 +77,7 @@ module.exports = function (Groups) { if (values.hasOwnProperty('memberPostCids')) { const validCids = await categories.getCidsByPrivilege('categories:cid', groupName, 'topics:read'); - const cidsArray = values.memberPostCids.split(',').map(cid => parseInt(cid.trim(), 10)).filter(Boolean); + const cidsArray = values.memberPostCids.split(',').map(cid => (cid || '').trim()).filter(Boolean); payload.memberPostCids = cidsArray.filter(cid => validCids.includes(cid)).join(',') || ''; } diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index 6ba57cdf66..6c4e32a77e 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -78,7 +78,7 @@ async function isAllowedToCids(privilege, uidOrGroupName, cids) { const groupKeys = cids.map(cid => `cid:${cid}:privileges:groups:${privilege}`); // Group handling - if (isNaN(parseInt(uidOrGroupName, 10)) && (uidOrGroupName || '').length) { + if (!utils.isNumber(uidOrGroupName) && (uidOrGroupName || '').length) { return await checkIfAllowedGroup(uidOrGroupName, groupKeys); } diff --git a/test/groups.js b/test/groups.js index 59d88abafd..9ea3106451 100644 --- a/test/groups.js +++ b/test/groups.js @@ -7,6 +7,7 @@ const nconf = require('nconf'); const db = require('./mocks/databasemock'); const helpers = require('./helpers'); +const Categories = require('../src/categories'); const Groups = require('../src/groups'); const User = require('../src/user'); const plugins = require('../src/plugins'); @@ -566,6 +567,20 @@ describe('Groups', () => { }); }); }); + + it('should properly set memberPostCids', async () => { + const c1 = await Categories.create({ name: 'Test Category' }); + const c2 = await Categories.create({ name: 'Test Category' }); + const c3 = await Categories.create({ name: 'Test Category' }); + await Groups.create({ + name: '3rd party devs', + }); + await Groups.update('3rd party devs', { + memberPostCids: `${c1.cid},${c2.cid},${c3.cid}`, + }); + const groupData = await Groups.get('3rd party devs', {}); + assert.strictEqual(groupData.memberPostCids, '1,2,3'); + }); }); describe('.destroy()', () => { From 37f231909194ff47adaa1bd528786077467a8ed6 Mon Sep 17 00:00:00 2001 From: Dirk Plate Date: Wed, 15 Apr 2026 14:59:36 +0200 Subject: [PATCH 09/10] fix: don't prepend relative_path to absolute URLs in getProfilePictures (#14176) Upload plugins like nodebb-plugin-cloudinary store full absolute URLs in the uid:{uid}:profile:pictures sorted set. The code unconditionally prepends relative_path, producing broken URLs like /forumhttps://... Add the same startsWith('http') check used elsewhere in the codebase. --- src/socket.io/user/picture.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js index d63f28e61c..5800a0e794 100644 --- a/src/socket.io/user/picture.js +++ b/src/socket.io/user/picture.js @@ -39,7 +39,7 @@ module.exports = function (SocketUser) { userPictures.forEach((picture) => { list.pictures.push({ type: 'uploaded', - url: `${nconf.get('relative_path')}${picture}`, + url: picture.startsWith('http') ? picture : `${nconf.get('relative_path')}${picture}`, text: '[[user:uploaded-picture]]', }); }); From d99e46ab2d8cafffd9c2178432d0c56e6cd4362f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 15 Apr 2026 09:11:34 -0400 Subject: [PATCH 10/10] fix: copy ip, closes #14177 --- public/src/admin/manage/users.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index e3d9f24db6..e2f4438fdb 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -495,6 +495,7 @@ define('admin/manage/users', [ }); const tableEl = document.querySelector('.users-table'); + const $tableEl = $(tableEl); const actionBtn = document.getElementById('action-dropdown'); tableEl.addEventListener('change', (e) => { const subselector = e.target.closest('[component="user/select/single"]') || e.target.closest('[component="user/select/all"]'); @@ -509,7 +510,7 @@ define('admin/manage/users', [ }); let lastSelectedUser; - $(tableEl).on('click', '[component="user/select/single"]', function (ev) { + $tableEl.on('click', '[component="user/select/single"]', function (ev) { function selectRange(clickedUserRow) { function selectIndexRange(start, end, isChecked) { if (start > end) { @@ -545,7 +546,7 @@ define('admin/manage/users', [ lastSelectedUser = userRow; }); - $('[data-copy]').on('click', function () { + $tableEl.on('click', '[data-copy]', function () { const btn = $(this); navigator.clipboard.writeText(this.getAttribute('data-copy')); btn.find('i')