From 76732140f3316798e9711f663add6028d7f7f03e Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 3 Feb 2023 16:39:40 +0000 Subject: [PATCH 01/13] chore: incrementing version number - v2.8.6 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 34c5e0bf05..a25c262ff1 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "2.8.5", + "version": "2.8.6", "homepage": "http://www.nodebb.org", "repository": { "type": "git", From f3306d038a36fbbe525125ac9986c11be5d86368 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Fri, 3 Feb 2023 16:39:40 +0000 Subject: [PATCH 02/13] chore: update changelog for v2.8.6 --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b793bc928..b053f14578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,54 @@ +#### v2.8.6 (2023-02-03) + +##### Chores + +* **i18n:** fallback strings for new resources: nodebb.error (8335f90a) +* incrementing version number - v2.8.5 (bff5ce2d) +* update changelog for v2.8.5 (24e58c28) +* incrementing version number - v2.8.4 (a46b2bbc) +* incrementing version number - v2.8.3 (c20b20a7) +* incrementing version number - v2.8.2 (050e43f8) +* incrementing version number - v2.8.1 (727f879e) +* incrementing version number - v2.8.0 (8e77673d) +* incrementing version number - v2.7.0 (96cc0617) +* incrementing version number - v2.6.1 (7e52a7a5) +* incrementing version number - v2.6.0 (e7fcf482) +* incrementing version number - v2.5.8 (dec0e7de) +* incrementing version number - v2.5.7 (5836bf4a) +* incrementing version number - v2.5.6 (c7bd7dbf) +* incrementing version number - v2.5.5 (3509ed94) +* incrementing version number - v2.5.4 (e83260ca) +* incrementing version number - v2.5.3 (7e922936) +* incrementing version number - v2.5.2 (babcd17e) +* incrementing version number - v2.5.1 (ce3aa950) +* incrementing version number - v2.5.0 (01d276cb) +* incrementing version number - v2.4.5 (dd3e1a28) +* incrementing version number - v2.4.4 (d5525c87) +* incrementing version number - v2.4.3 (9c647c6c) +* incrementing version number - v2.4.2 (3aa7b855) +* incrementing version number - v2.4.1 (60cbd148) +* incrementing version number - v2.4.0 (4834cde3) +* incrementing version number - v2.3.1 (d2425942) +* incrementing version number - v2.3.0 (046ea120) + +##### New Features + +* add sitemap filter hooks for categories/topic pages (bf92ee0e) +* closes #11241, add missing error lang keys (c241baf6) +* #11240, only show relevant users in flags assignee list (0713482b) + +##### Bug Fixes + +* #11254, return check for reroll property (202378b9) +* closes #11249, notification uses displayname (705cd13a) +* wrong link to topics in acp dashboard (b5598a6e) +* https://github.com/NodeBB/NodeBB/issues/11239 (1d3c0e5a) +* notif filter selecte field (6d819b05) + +##### Other Changes + +* remove unused (d68352cc) + #### v2.8.5 (2023-01-27) ##### Chores From 7a5bcc217146621fa088e3572ccb8d950b3097c5 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 3 Feb 2023 16:01:31 -0500 Subject: [PATCH 03/13] fix: #11257, onSuccessfulLogin called with improper uid --- src/routes/authentication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 9febe062a8..934fdec80e 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -154,7 +154,7 @@ Auth.reloadRoutes = async function (params) { }, Auth.middleware.validateAuth, (req, res, next) => { async.waterfall([ async.apply(req.login.bind(req), res.locals.user, { keepSessionInfo: true }), - async.apply(controllers.authentication.onSuccessfulLogin, req, req.uid), + async.apply(controllers.authentication.onSuccessfulLogin, req, res.locals.user.uid), ], (err) => { if (err) { return next(err); From 845c8013b6442ef31fa0476f1c8e2447c643f58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 6 Feb 2023 10:45:01 -0500 Subject: [PATCH 04/13] fix: #11259, clean old emails when updating via admin (#11260) when admin is changing users emails check if its avaiable and remove old email of user first upgrade script to cleanup email:uid, email:sorted, will remove entries if user doesn't exist or doesn't have email or if entry in user hash doesn't match entry in email:uid fix missing ! in email interstitial fix missing await in canSendValidation, fix broken tests dont pass sessionId to email.remove if admin is changing/removing email --- src/upgrades/2.8.7/fix-email-sorted-sets.js | 46 +++++++++++++++++++++ src/user/email.js | 18 +++++++- src/user/interstitials.js | 11 +++-- test/user/emails.js | 4 +- 4 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 src/upgrades/2.8.7/fix-email-sorted-sets.js diff --git a/src/upgrades/2.8.7/fix-email-sorted-sets.js b/src/upgrades/2.8.7/fix-email-sorted-sets.js new file mode 100644 index 0000000000..fcab69a8f4 --- /dev/null +++ b/src/upgrades/2.8.7/fix-email-sorted-sets.js @@ -0,0 +1,46 @@ +'use strict'; + + +const db = require('../../database'); +const batch = require('../../batch'); + + +module.exports = { + name: 'Fix user email sorted sets', + timestamp: Date.UTC(2023, 1, 4), + method: async function () { + const { progress } = this; + const bulkRemove = []; + await batch.processSortedSet('email:uid', async (data) => { + progress.incr(data.length); + const usersData = await db.getObjects(data.map(d => `user:${d.score}`)); + data.forEach((emailData, index) => { + const { score: uid, value: email } = emailData; + const userData = usersData[index]; + // user no longer exists or doesn't have email set in user hash + // remove the email/uid pair from email:uid, email:sorted + if (!userData || !userData.email) { + bulkRemove.push(['email:uid', email]); + bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]); + return; + } + + // user has email but doesn't match whats stored in user hash, gh#11259 + if (userData.email && userData.email.toLowerCase() !== email.toLowerCase()) { + bulkRemove.push(['email:uid', email]); + bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]); + } + }); + }, { + batch: 500, + withScores: true, + progress: progress, + }); + + await batch.processArray(bulkRemove, async (bulk) => { + await db.sortedSetRemoveBulk(bulk); + }, { + batch: 500, + }); + }, +}; diff --git a/src/user/email.js b/src/user/email.js index 1ea8bd551e..c6fc3274d4 100644 --- a/src/user/email.js +++ b/src/user/email.js @@ -39,7 +39,7 @@ UserEmail.remove = async function (uid, sessionId) { db.sortedSetRemove('email:uid', email.toLowerCase()), db.sortedSetRemove('email:sorted', `${email.toLowerCase()}:${uid}`), user.email.expireValidation(uid), - user.auth.revokeAllSessions(uid, sessionId), + sessionId ? user.auth.revokeAllSessions(uid, sessionId) : Promise.resolve(), events.log({ type: 'email-change', email, newEmail: '' }), ]); }; @@ -69,7 +69,7 @@ UserEmail.expireValidation = async (uid) => { }; UserEmail.canSendValidation = async (uid, email) => { - const pending = UserEmail.isValidationPending(uid, email); + const pending = await UserEmail.isValidationPending(uid, email); if (!pending) { return true; } @@ -196,6 +196,20 @@ UserEmail.confirmByUid = async function (uid) { throw new Error('[[error:invalid-email]]'); } + // If another uid has the same email throw error + const oldUid = await db.sortedSetScore('email:uid', currentEmail.toLowerCase()); + if (oldUid && oldUid !== parseInt(uid, 10)) { + throw new Error('[[error:email-taken]]'); + } + + const confirmedEmails = await db.getSortedSetRangeByScore(`email:uid`, 0, -1, uid, uid); + if (confirmedEmails.length) { + // remove old email of user by uid + await db.sortedSetsRemoveRangeByScore([`email:uid`], uid, uid); + await db.sortedSetRemoveBulk( + confirmedEmails.map(email => [`email:sorted`, `${email.toLowerCase()}:${uid}`]) + ); + } await Promise.all([ db.sortedSetAddBulk([ ['email:uid', uid, currentEmail.toLowerCase()], diff --git a/src/user/interstitials.js b/src/user/interstitials.js index 2a662785f9..aa70e8098f 100644 --- a/src/user/interstitials.js +++ b/src/user/interstitials.js @@ -42,6 +42,7 @@ Interstitials.email = async (data) => { callback: async (userData, formData) => { // Validate and send email confirmation if (userData.uid) { + const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10); const [isPasswordCorrect, canEdit, { email: current, 'email:confirmed': confirmed }, { allowed, error }] = await Promise.all([ user.isPasswordCorrect(userData.uid, formData.password, data.req.ip), privileges.users.canEdit(data.req.uid, userData.uid), @@ -68,13 +69,17 @@ Interstitials.email = async (data) => { if (formData.email === current) { if (confirmed) { throw new Error('[[error:email-nochange]]'); - } else if (await user.email.canSendValidation(userData.uid, current)) { + } else if (!await user.email.canSendValidation(userData.uid, current)) { throw new Error(`[[error:confirm-email-already-sent, ${meta.config.emailConfirmInterval}]]`); } } // Admins editing will auto-confirm, unless editing their own email if (isAdminOrGlobalMod && userData.uid !== data.req.uid) { + if (!await user.email.available(formData.email)) { + throw new Error('[[error:email-taken]]'); + } + await user.email.remove(userData.uid); await user.setUserField(userData.uid, 'email', formData.email); await user.email.confirmByUid(userData.uid); } else if (canEdit) { @@ -99,8 +104,8 @@ Interstitials.email = async (data) => { } if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) { - // User explicitly clearing their email - await user.email.remove(userData.uid, data.req.session.id); + // User or admin explicitly clearing their email + await user.email.remove(userData.uid, isSelf ? data.req.session.id : null); } } } else { diff --git a/test/user/emails.js b/test/user/emails.js index f413a51283..47d3fcb6d0 100644 --- a/test/user/emails.js +++ b/test/user/emails.js @@ -120,7 +120,7 @@ describe('email confirmation (library methods)', () => { await user.email.sendValidationEmail(uid, { email, }); - const ok = await user.email.canSendValidation(uid, 'test@example.com'); + const ok = await user.email.canSendValidation(uid, email); assert.strictEqual(ok, false); }); @@ -131,7 +131,7 @@ describe('email confirmation (library methods)', () => { email, }); await db.pexpire(`confirm:byUid:${uid}`, 1000); - const ok = await user.email.canSendValidation(uid, 'test@example.com'); + const ok = await user.email.canSendValidation(uid, email); assert(ok); }); From e335d0f6016c76575ed768d6c33130f8ff847b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 8 Feb 2023 13:22:16 -0500 Subject: [PATCH 05/13] fix: email expiry timestamps emailConfirmExpiry is hours and default is 24 --- src/user/email.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/user/email.js b/src/user/email.js index c6fc3274d4..9b51b43ddd 100644 --- a/src/user/email.js +++ b/src/user/email.js @@ -134,13 +134,13 @@ UserEmail.sendValidationEmail = async function (uid, options) { await UserEmail.expireValidation(uid); await db.set(`confirm:byUid:${uid}`, confirm_code); - await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 24 * 60 * 60 * 1000); + await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 60 * 60 * 1000); await db.setObject(`confirm:${confirm_code}`, { email: options.email.toLowerCase(), uid: uid, }); - await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 24 * 60 * 60 * 1000); + await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 60 * 60 * 1000); winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`); events.log({ From 326b92687fa5d2b68cc5f55275c565a43bf6a16e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 8 Feb 2023 17:35:38 -0500 Subject: [PATCH 06/13] fix: show admins/globalmods if content is purged --- src/controllers/mods.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 3656146652..760c119fbe 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -132,11 +132,11 @@ modsController.flags.detail = async function (req, res, next) { uids = _.uniq(admins.concat(uids)); } else if (flagData.type === 'post') { const cid = await posts.getCidByPid(flagData.targetId); - if (!cid) { - return []; + uids = _.uniq(admins.concat(globalMods)); + if (cid) { + const modUids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0]; + uids = _.uniq(uids.concat(modUids)); } - uids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0]; - uids = _.uniq(admins.concat(globalMods).concat(uids)); } const userData = await user.getUsersData(uids); return userData.filter(u => u && u.userslug); From 40e7b86da9b3eab82914ce557333eb257c4c43d3 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 13 Feb 2023 11:44:40 -0500 Subject: [PATCH 07/13] docs: update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying --- public/openapi/write/topics.yaml | 9 +++++++++ public/openapi/write/topics/tid.yaml | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/public/openapi/write/topics.yaml b/public/openapi/write/topics.yaml index ba00cf0024..49cae076f9 100644 --- a/public/openapi/write/topics.yaml +++ b/public/openapi/write/topics.yaml @@ -19,6 +19,15 @@ post: content: type: string example: This is the test topic's content + timestamp: + type: number + description: | + A UNIX timestamp of the topic's creation date (i.e. when it will be posted). + Specifically, this value can only be set to a value in the future if the calling user has the `topics:schedule` privilege for the passed-in category. + Otherwise, the current date and time are always assumed. + In some scenarios (e.g. forum migrations), you may want to backdate topics and posts. + Please see [this Developer FAQ topic](https://community.nodebb.org/topic/16983/how-can-i-backdate-topics-and-posts-for-migration-purposes) for more information. + example: 556084800000 tags: type: array items: diff --git a/public/openapi/write/topics/tid.yaml b/public/openapi/write/topics/tid.yaml index 8e68efe25a..4c1acab3d6 100644 --- a/public/openapi/write/topics/tid.yaml +++ b/public/openapi/write/topics/tid.yaml @@ -46,8 +46,6 @@ post: content: type: string example: This is a test reply - timestamp: - type: number toPid: type: number required: From 1b29dbb69d123c35d4fa954b8ee9aa63ded28136 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 13 Feb 2023 12:15:45 -0500 Subject: [PATCH 08/13] test: add dummy emailer hook in authentication test --- test/authentication.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/authentication.js b/test/authentication.js index b8889d95eb..3718271809 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -12,13 +12,22 @@ const db = require('./mocks/databasemock'); const user = require('../src/user'); const utils = require('../src/utils'); const meta = require('../src/meta'); +const plugins = require('../src/plugins'); const privileges = require('../src/privileges'); const helpers = require('./helpers'); describe('authentication', () => { const jar = request.jar(); let regularUid; + const dummyEmailerHook = async (data) => {}; + before((done) => { + // Attach an emailer hook so related requests do not error + plugins.hooks.register('authentication-test', { + hook: 'filter:email.send', + method: dummyEmailerHook, + }); + user.create({ username: 'regular', password: 'regularpwd', email: 'regular@nodebb.org' }, (err, uid) => { assert.ifError(err); regularUid = uid; @@ -27,6 +36,10 @@ describe('authentication', () => { }); }); + after(() => { + plugins.hooks.unregister('authentication-test', 'filter:email.send'); + }); + it('should allow login with email for uid 1', async () => { const oldValue = meta.config.allowLoginWith; meta.config.allowLoginWith = 'username-email'; From edd2fc38fc2d6f0ff7f344d11236190a44404f5d Mon Sep 17 00:00:00 2001 From: gasoved Date: Thu, 16 Feb 2023 04:38:51 +0300 Subject: [PATCH 09/13] fix: update main post timestamp when rescheduling --- src/topics/scheduled.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/topics/scheduled.js b/src/topics/scheduled.js index 3544e54945..a386de8869 100644 --- a/src/topics/scheduled.js +++ b/src/topics/scheduled.js @@ -60,6 +60,7 @@ Scheduled.pin = async function (tid, topicData) { }; Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) { + const mainPid = await topics.getTopicField(tid, 'mainPid'); await Promise.all([ db.sortedSetsAdd([ 'topics:scheduled', @@ -67,6 +68,7 @@ Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) { 'topics:tid', `cid:${cid}:uid:${uid}:tids`, ], timestamp, tid), + posts.setPostField(mainPid, 'timestamp', timestamp), shiftPostTimes(tid, timestamp), ]); return topics.updateLastPostTimeFromLastPid(tid); From 3bd9a8715409d9bda478202e8b62e3d396b6dfcc Mon Sep 17 00:00:00 2001 From: Eldor Date: Tue, 21 Feb 2023 15:56:00 +0200 Subject: [PATCH 10/13] fix: show error alert if password change fails --- public/src/client/account/edit/password.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/src/client/account/edit/password.js b/public/src/client/account/edit/password.js index abbe443e85..4ad1b47dbb 100644 --- a/public/src/client/account/edit/password.js +++ b/public/src/client/account/edit/password.js @@ -77,6 +77,7 @@ define('forum/account/edit/password', [ ajaxify.go('user/' + ajaxify.data.userslug + '/edit'); } }) + .catch(alerts.error) .finally(() => { btn.removeClass('disabled').find('i').addClass('hide'); currentPassword.val(''); From 8cf4a6f62eab74961f351f0cbf83e53722378e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 15 Nov 2022 09:29:14 -0500 Subject: [PATCH 11/13] fix: alert on page load --- public/src/admin/settings/email.js | 52 +++++++++++++++++++----------- src/views/admin/settings/email.tpl | 2 +- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/public/src/admin/settings/email.js b/public/src/admin/settings/email.js index e1598c2c0b..c1b3ee33b5 100644 --- a/public/src/admin/settings/email.js +++ b/public/src/admin/settings/email.js @@ -9,15 +9,23 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function configureEmailTester(); configureEmailEditor(); handleDigestHourChange(); - handleSmtpServiceChange(); - $(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange); - $(window).on('action:admin.settingsSaved', function () { - socket.emit('admin.user.restartJobs'); - }); - $('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange); + $(window).off('action:admin.settingsLoaded', onSettingsLoaded) + .on('action:admin.settingsLoaded', onSettingsLoaded); + $(window).off('action:admin.settingsSaved', onSettingsSaved) + .on('action:admin.settingsSaved', onSettingsSaved); }; + function onSettingsLoaded() { + handleDigestHourChange(); + handleSmtpServiceChange(); + } + + function onSettingsSaved() { + handleDigestHourChange(); + socket.emit('admin.user.restartJobs'); + } + function configureEmailTester() { $('button[data-action="email.test"]').off('click').on('click', function () { socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) { @@ -106,20 +114,26 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function } function handleSmtpServiceChange() { - const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp'; - $('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom); - - const enabledEl = document.getElementById('email:smtpTransport:enabled'); - if (enabledEl) { - if (!enabledEl.checked) { - enabledEl.closest('label').classList.toggle('is-checked', true); - enabledEl.checked = true; - alerts.alert({ - message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]', - timeout: 5000, - }); - } + function toggleCustomService() { + const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp'; + $('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom); } + toggleCustomService(); + $('[id="email:smtpTransport:service"]').change(function () { + toggleCustomService(); + + const enabledEl = document.getElementById('email:smtpTransport:enabled'); + if (enabledEl) { + if (!enabledEl.checked) { + $('label[for="email:smtpTransport:enabled"]').toggleClass('is-checked', true); + enabledEl.checked = true; + alerts.alert({ + message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]', + timeout: 5000, + }); + } + } + }); } return module; diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl index 4d8dcf27b1..86145d0c69 100644 --- a/src/views/admin/settings/email.tpl +++ b/src/views/admin/settings/email.tpl @@ -150,7 +150,7 @@ [[admin/settings/email:smtp-transport.gmail-warning2]]

-