diff --git a/CHANGELOG.md b/CHANGELOG.md index b053f14578..6feab404d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,149 @@ +#### v2.8.9 (2023-03-19) + +##### Chores + +* up cron (73a50d17) +* incrementing version number - v2.8.8 (b331b942) +* update changelog for v2.8.8 (c03d5db7) +* incrementing version number - v2.8.7 (3f8248d6) +* incrementing version number - v2.8.6 (af6ce447) +* incrementing version number - v2.8.5 (bff5ce2d) +* 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) + +##### Bug Fixes + +* thumb remove on windows, closes #11357 (767c1d1f) +* #11357 clear cache on thumb remove (a3a38e4b) +* closes #11352, try/catch rss feeds (cfd50272) +* closes #11343, don't crash if tags array is empty (56427e4f) + +##### Code Style Changes + +* more fixes (93aa43f7) + +##### Tests + +* openapi for thumbs (9e685e65) + +#### v2.8.8 (2023-03-09) + +##### Chores + +* incrementing version number - v2.8.7 (3f8248d6) +* update changelog for v2.8.7 (2ca38e7b) +* incrementing version number - v2.8.6 (af6ce447) +* incrementing version number - v2.8.5 (bff5ce2d) +* 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) + +##### Bug Fixes + +* stop topic navigation hotkeys from firing if in a mousetrap-enabled form element (22fc8fe3) +* stop topic navigation hotkeys from firing if in a mousetrap-enabled form element (17d0b40e) +* tag filtering when changing filter to watched topics (1545223e) +* get cid from pid instead of passing in (f054a4f4) +* closes #11331, allow 0 length content if set to 0 in acp (8c762d32) + +#### v2.8.7 (2023-03-01) + +##### Chores + +* incrementing version number - v2.8.6 (af6ce447) +* update changelog for v2.8.6 (f3306d03) +* incrementing version number - v2.8.5 (bff5ce2d) +* 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) + +##### Documentation Changes + +* update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying (40e7b86d) + +##### Bug Fixes + +* display 25 topics on category feed (79155109) +* object destructuring overwriting type parameter (ec58700f) +* alert on page load (8cf4a6f6) +* show error alert if password change fails (3bd9a871) +* update main post timestamp when rescheduling (edd2fc38) +* show admins/globalmods if content is purged (326b9268) +* email expiry timestamps (e335d0f6) +* #11259, clean old emails when updating via admin (#11260) (845c8013) +* #11257, onSuccessfulLogin called with improper uid (7a5bcc21) + +##### Tests + +* add dummy emailer hook in authentication test (1b29dbb6) + #### v2.8.6 (2023-02-03) ##### Chores diff --git a/install/package.json b/install/package.json index 91ed420492..44521d8d9d 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.6", + "version": "2.8.10", "homepage": "http://www.nodebb.org", "repository": { "type": "git", @@ -53,7 +53,7 @@ "connect-pg-simple": "8.0.0", "connect-redis": "6.1.3", "cookie-parser": "1.4.6", - "cron": "2.1.0", + "cron": "2.3.0", "cropperjs": "1.5.13", "csurf": "1.11.0", "daemon": "1.1.0", @@ -90,7 +90,7 @@ "@nodebb/bootswatch": "3.4.2", "nconf": "0.12.0", "nodebb-plugin-2factor": "5.1.2", - "nodebb-plugin-composer-default": "9.2.4", + "nodebb-plugin-composer-default": "9.2.5", "nodebb-plugin-dbsearch": "5.1.5", "nodebb-plugin-emoji": "4.0.6", "nodebb-plugin-emoji-android": "3.0.0", diff --git a/loader.js b/loader.js index 1d86d3a37e..9a1f0cdbad 100644 --- a/loader.js +++ b/loader.js @@ -30,9 +30,7 @@ const output = logrotate({ file: outputLogFilePath, size: '1m', keep: 3, compres const silent = nconf.get('silent') === 'false' ? false : nconf.get('silent') !== false; let numProcs; const workers = []; -const Loader = { - timesStarted: 0, -}; +const Loader = {}; const appPath = path.join(__dirname, 'app.js'); Loader.init = function () { @@ -57,21 +55,6 @@ Loader.displayStartupMessages = function () { Loader.addWorkerEvents = function (worker) { worker.on('exit', (code, signal) => { - if (code !== 0) { - if (Loader.timesStarted < numProcs * 3) { - Loader.timesStarted += 1; - if (Loader.crashTimer) { - clearTimeout(Loader.crashTimer); - } - Loader.crashTimer = setTimeout(() => { - Loader.timesStarted = 0; - }, 10000); - } else { - console.log(`${numProcs * 3} restarts in 10 seconds, most likely an error on startup. Halting.`); - process.exit(); - } - } - console.log(`[cluster] Child Process (${worker.pid}) has exited (code: ${code}, signal: ${signal})`); if (!(worker.suicide || code === 0)) { console.log('[cluster] Spinning up another process...'); diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml index 64de19aeb8..ad337379f7 100644 --- a/public/openapi/components/schemas/TopicObject.yaml +++ b/public/openapi/components/schemas/TopicObject.yaml @@ -265,6 +265,9 @@ TopicObjectSlim: name: type: string description: The topic thumbnail filename + path: + type: string + description: Path to topic thumbnail without upload_url prefix url: type: string description: Relative path to the topic thumbnail 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: diff --git a/public/openapi/write/topics/tid/thumbs.yaml b/public/openapi/write/topics/tid/thumbs.yaml index a51858aeb3..075bc29ed2 100644 --- a/public/openapi/write/topics/tid/thumbs.yaml +++ b/public/openapi/write/topics/tid/thumbs.yaml @@ -31,6 +31,8 @@ get: type: string name: type: string + path: + type: string url: type: string description: Path to a topic thumbnail @@ -155,6 +157,8 @@ delete: type: string name: type: string + path: + type: string url: type: string description: Path to a topic thumbnail \ No newline at end of file diff --git a/public/openapi/write/topics/tid/thumbs/order.yaml b/public/openapi/write/topics/tid/thumbs/order.yaml index 8e9a12fef1..2fac638a23 100644 --- a/public/openapi/write/topics/tid/thumbs/order.yaml +++ b/public/openapi/write/topics/tid/thumbs/order.yaml @@ -38,4 +38,4 @@ put: $ref: ../../../../components/schemas/Status.yaml#/Status response: type: object - properties: {} \ No newline at end of file + properties: {} 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/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(''); diff --git a/public/src/client/topic.js b/public/src/client/topic.js index 961fcd74c3..8f0bf52f25 100644 --- a/public/src/client/topic.js +++ b/public/src/client/topic.js @@ -88,7 +88,11 @@ define('forum/topic', [ }); } - mousetrap.bind('j', () => { + mousetrap.bind('j', (e) => { + if (e.target.classList.contains('mousetrap')) { + return; + } + const index = navigator.getIndex(); const count = navigator.getCount(); if (index === count) { @@ -98,7 +102,11 @@ define('forum/topic', [ navigator.scrollToIndex(index, true, 0); }); - mousetrap.bind('k', () => { + mousetrap.bind('k', (e) => { + if (e.target.classList.contains('mousetrap')) { + return; + } + const index = navigator.getIndex(); if (index === 1) { return; diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index e9b5602145..c9d57854b8 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -41,7 +41,7 @@ define('forum/topic/postTools', [ const pid = postEl.attr('data-pid'); const index = parseInt(postEl.attr('data-index'), 10); - socket.emit('posts.loadPostTools', { pid: pid, cid: ajaxify.data.cid }, async (err, data) => { + socket.emit('posts.loadPostTools', { pid: pid }, async (err, data) => { if (err) { return alerts.error(err); } diff --git a/src/api/users.js b/src/api/users.js index e0382c95cd..bbb02a43bb 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -443,6 +443,10 @@ usersAPI.changePicture = async (caller, data) => { }; usersAPI.generateExport = async (caller, { uid, type }) => { + const validTypes = ['profile', 'posts', 'uploads']; + if (!validTypes.includes(type)) { + throw new Error('[[error:invalid-data]]'); + } const count = await db.incrObjectField('locks', `export:${uid}${type}`); if (count > 1) { throw new Error('[[error:already-exporting]]'); diff --git a/src/cli/index.js b/src/cli/index.js index 5eb9f4924c..3657549e3d 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -32,12 +32,6 @@ try { if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) { const e = new TypeError(`Incorrect dependency version: ${packageName}`); e.code = 'DEP_WRONG_VERSION'; - // delete the module from require cache so it doesn't break rest of the upgrade - // https://github.com/NodeBB/NodeBB/issues/11173 - const resolvedModule = require.resolve(packageName); - if (require.cache[resolvedModule]) { - delete require.cache[resolvedModule]; - } throw e; } }; @@ -57,6 +51,16 @@ try { packageInstall.preserveExtraneousPlugins(); packageInstall.installAll(); + // delete the module from require cache so it doesn't break rest of the upgrade + // https://github.com/NodeBB/NodeBB/issues/11173 + const packages = ['nconf', 'async', 'commander', 'chalk', 'lodash', 'lru-cache']; + packages.forEach((packageName) => { + const resolvedModule = require.resolve(packageName); + if (require.cache[resolvedModule]) { + delete require.cache[resolvedModule]; + } + }); + const chalk = require('chalk'); console.log(`${chalk.green('OK')}\n`); } else { 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); diff --git a/src/database/mongo/sorted/union.js b/src/database/mongo/sorted/union.js index ea4ad4d8e1..f518fab79f 100644 --- a/src/database/mongo/sorted/union.js +++ b/src/database/mongo/sorted/union.js @@ -26,7 +26,7 @@ module.exports = function (module) { async function getSortedSetUnion(params) { if (!Array.isArray(params.sets) || !params.sets.length) { - return; + return []; } let limit = params.stop - params.start + 1; if (limit <= 0) { diff --git a/src/database/postgres/sorted/union.js b/src/database/postgres/sorted/union.js index a8fcbd0d0d..9d671ceb0f 100644 --- a/src/database/postgres/sorted/union.js +++ b/src/database/postgres/sorted/union.js @@ -32,6 +32,9 @@ SELECT COUNT(DISTINCT z."value") c async function getSortedSetUnion(params) { const { sets } = params; + if (!sets || !sets.length) { + return []; + } const start = params.hasOwnProperty('start') ? params.start : 0; const stop = params.hasOwnProperty('stop') ? params.stop : -1; let weights = params.weights || []; diff --git a/src/routes/feeds.js b/src/routes/feeds.js index fbd8de7072..c4a1c2d9f2 100644 --- a/src/routes/feeds.js +++ b/src/routes/feeds.js @@ -9,11 +9,12 @@ const topics = require('../topics'); const user = require('../user'); const categories = require('../categories'); const meta = require('../meta'); -const helpers = require('../controllers/helpers'); +const controllerHelpers = require('../controllers/helpers'); const privileges = require('../privileges'); const db = require('../database'); const utils = require('../utils'); const controllers404 = require('../controllers/404'); +const routeHelpers = require('./helpers'); const terms = { daily: 'day', @@ -23,18 +24,18 @@ const terms = { }; module.exports = function (app, middleware) { - app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic); - app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory); - app.get('/topics.rss', middleware.maintenanceMode, generateForTopics); - app.get('/recent.rss', middleware.maintenanceMode, generateForRecent); - app.get('/top.rss', middleware.maintenanceMode, generateForTop); - app.get('/top/:term.rss', middleware.maintenanceMode, generateForTop); - app.get('/popular.rss', middleware.maintenanceMode, generateForPopular); - app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular); - app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts); - app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, generateForCategoryRecentPosts); - app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, generateForUserTopics); - app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag); + app.get('/topic/:topic_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopic)); + app.get('/category/:category_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategory)); + app.get('/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopics)); + app.get('/recent.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecent)); + app.get('/top.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop)); + app.get('/top/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop)); + app.get('/popular.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular)); + app.get('/popular/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular)); + app.get('/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecentPosts)); + app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategoryRecentPosts)); + app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForUserTopics)); + app.get('/tags/:tag.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTag)); }; async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) { @@ -46,16 +47,16 @@ async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) { } if (uid <= 0 || !token) { - return helpers.notAllowed(req, res); + return controllerHelpers.notAllowed(req, res); } const userToken = await db.getObjectField(`user:${uid}`, 'rss_token'); if (userToken !== token) { await user.auth.logAttempt(uid, req.ip); - return helpers.notAllowed(req, res); + return controllerHelpers.notAllowed(req, res); } const userPrivileges = await privileges.categories.get(cid, uid); if (!userPrivileges.read) { - return helpers.notAllowed(req, res); + return controllerHelpers.notAllowed(req, res); } return true; } @@ -127,7 +128,7 @@ async function generateForCategory(req, res, next) { db.getSortedSetRevIntersect({ sets: ['topics:tid', `cid:${cid}:tids:lastposttime`], start: 0, - stop: 25, + stop: 24, weights: [1, 0], }), ]); @@ -230,7 +231,7 @@ async function generateSorted(options, req, res, next) { const { cid } = req.query; if (cid) { if (!await privileges.categories.can('topics:read', cid, uid)) { - return helpers.notAllowed(req, res); + return controllerHelpers.notAllowed(req, res); } params.cids = [cid]; } diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 60c7a8cd27..30e4e23581 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -120,7 +120,12 @@ async function onMessage(socket, payload) { return winston.warn('[socket.io] Empty method name'); } - const parts = eventName.toString().split('.'); + if (typeof eventName !== 'string') { + const escapedName = validator.escape(String(eventName)); + return callback({ message: `[[error:invalid-event, ${escapedName}]]` }); + } + + const parts = eventName.split('.'); const namespace = parts[0]; const methodToCall = parts.reduce((prev, cur) => { if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) { diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index db6e432fa6..4ff69e7fc0 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -14,15 +14,15 @@ const utils = require('../../utils'); module.exports = function (SocketPosts) { SocketPosts.loadPostTools = async function (socket, data) { - if (!data || !data.pid || !data.cid) { + if (!data || !data.pid) { throw new Error('[[error:invalid-data]]'); } - + const cid = await posts.getCidByPid(data.pid); const results = await utils.promiseParallel({ posts: posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid', 'ip', 'flagId']), isAdmin: user.isAdministrator(socket.uid), isGlobalMod: user.isGlobalModerator(socket.uid), - isModerator: user.isModerator(socket.uid, data.cid), + isModerator: user.isModerator(socket.uid, cid), canEdit: privileges.posts.canEdit(data.pid, socket.uid), canDelete: privileges.posts.canDelete(data.pid, socket.uid), canPurge: privileges.posts.canPurge(data.pid, socket.uid), diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index 20a869056c..95c0097116 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -74,6 +74,6 @@ module.exports = function (SocketUser) { await user.isAdminOrSelf(socket.uid, data.uid); - api.users.generateExport(socket, { type, ...data }); + api.users.generateExport(socket, { type, uid: data.uid }); } }; diff --git a/src/topics/create.js b/src/topics/create.js index a6d63d53dd..8aacce42ca 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -82,9 +82,8 @@ module.exports = function (Topics) { data.title = String(data.title).trim(); data.tags = data.tags || []; - if (data.content) { - data.content = utils.rtrim(data.content); - } + data.content = String(data.content || '').trimEnd(); + Topics.checkTitle(data.title); await Topics.validateTags(data.tags, data.cid, uid); data.tags = await Topics.filterTags(data.tags, data.cid); @@ -167,9 +166,8 @@ module.exports = function (Topics) { data.cid = topicData.cid; await guestHandleValid(data); - if (data.content) { - data.content = utils.rtrim(data.content); - } + data.content = String(data.content || '').trimEnd(); + if (!data.fromQueue) { await user.isReadyToPost(uid, data.cid); Topics.checkContent(data.content); 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); diff --git a/src/topics/sorted.js b/src/topics/sorted.js index af2cd344cb..7899a2f8d0 100644 --- a/src/topics/sorted.js +++ b/src/topics/sorted.js @@ -174,7 +174,7 @@ module.exports = function (Topics) { } tids = await privileges.topics.filterTids('topics:read', tids, uid); - let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid']); + let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid', 'tags']); const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean); async function getIgnoredCids() { @@ -192,11 +192,13 @@ module.exports = function (Topics) { topicData = filtered; const cids = params.cids && params.cids.map(String); + const { tags } = params; tids = topicData.filter(t => ( t && t.cid && !isCidIgnored[t.cid] && - (!cids || cids.includes(String(t.cid))) + (!cids || cids.includes(String(t.cid))) && + (!tags.length || tags.every(tag => t.tags.find(topicTag => topicTag.value === tag))) )).map(t => t.tid); const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { tids: tids, params: params }); diff --git a/src/topics/tags.js b/src/topics/tags.js index 655c18345f..314b5cfd84 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -287,7 +287,7 @@ module.exports = function (Topics) { } Topics.getTagData = async function (tags) { - if (!tags.length) { + if (!tags || !tags.length) { return []; } tags.forEach((tag) => { diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index f2bacf74d3..60db184339 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -52,6 +52,7 @@ Thumbs.get = async function (tids) { const name = path.basename(thumb); return hasTimestampPrefix.test(name) ? name.slice(14) : name; })(), + path: thumb, url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, thumb), }))); @@ -151,6 +152,9 @@ Thumbs.delete = async function (id, relativePaths) { Promise.all(toRemove.map(async relativePath => posts.uploads.dissociate(mainPid, relativePath.slice(1)))), ]); } + if (toRemove.length) { + cache.del(set); + } }; Thumbs.deleteAll = async (id) => { diff --git a/src/user/admin.js b/src/user/admin.js index c92035fc47..b29adcf86f 100644 --- a/src/user/admin.js +++ b/src/user/admin.js @@ -70,7 +70,9 @@ module.exports = function (User) { let line = ''; usersData.forEach((user, index) => { - line += `${fields.map(field => user[field]).join(',')}`; + line += `${fields + .map(field => (isFinite(user[field]) ? `'${user[field]}'` : user[field])) + .join(',')}`; if (showIps) { userIPs = ips[index] ? ips[index].join(',') : ''; line += `,"${userIPs}"\n`; 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({ 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]]

-