diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a901a4ee46..5cf00383ef 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -13,13 +13,14 @@ on: # A workflow run is made up of one or more jobs that can run sequentially or in parallel permissions: contents: read + packages: write jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -32,14 +33,15 @@ jobs: - name: Login to Docker Hub uses: docker/login-action@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Docker meta id: meta uses: docker/metadata-action@v4 with: - images: nodebb/docker + images: ghcr.io/${{ github.repository }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6feab404d2..b99f4b090f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,46 @@ +#### v2.8.11 (2023-04-11) + +##### Chores + +* incrementing version number - v2.8.10 (5b7c3671) +* update changelog for v2.8.10 (188ec62f) + +##### Continuous Integration + +* publish to ghcr instead of docker hub (c2756728) + +##### Documentation Changes + +* update readme with new screenshot and updated copy for Harmony (67055006) + +##### Bug Fixes + +* don't crash on objects with toString property (4d2d7689) +* fire action:user.online on user login (7397873d) + +##### Tests + +* update socket.io test (e0b20658) + +#### v2.8.10 (2023-03-27) + +##### Chores + +* up composer-default (e9a8e195) +* incrementing version number - v2.8.9 (57f14e41) +* update changelog for v2.8.9 (18b2150e) + +##### Bug Fixes + +* #11403, remove loader.js crash counter logic (830f142b) +* don't crash if event name is not a string (37b48b82) +* closes #11173, move cache clear code (c2961ad4) + +##### Other Changes + +* fix arrow (1aff9cad) +* whitespace (894f392b) + #### v2.8.9 (2023-03-19) ##### Chores @@ -5,34 +48,6 @@ * 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 @@ -55,33 +70,6 @@ * 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 @@ -97,32 +85,6 @@ * 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 @@ -151,31 +113,6 @@ * **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 @@ -201,30 +138,6 @@ * incrementing version number - v2.8.4 (a46b2bbc) * update changelog for v2.8.4 (c13f0e21) -* 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 @@ -236,29 +149,6 @@ * incrementing version number - v2.8.3 (c20b20a7) * update changelog for v2.8.3 (eb2841ee) -* 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) #### v2.8.3 (2023-01-25) @@ -267,28 +157,6 @@ * remove extraneous lines from changelog (48c9f447) * incrementing version number - v2.8.2 (050e43f8) * update changelog for v2.8.2 (66aa3169) -* 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 diff --git a/install/package.json b/install/package.json index 44521d8d9d..0710065462 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.10", + "version": "2.8.17", "homepage": "http://www.nodebb.org", "repository": { "type": "git", @@ -55,7 +55,7 @@ "cookie-parser": "1.4.6", "cron": "2.3.0", "cropperjs": "1.5.13", - "csurf": "1.11.0", + "csrf-sync": "4.0.1", "daemon": "1.1.0", "diff": "5.1.0", "esbuild": "0.16.10", @@ -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.5", + "nodebb-plugin-composer-default": "9.2.6", "nodebb-plugin-dbsearch": "5.1.5", "nodebb-plugin-emoji": "4.0.6", "nodebb-plugin-emoji-android": "3.0.0", diff --git a/public/src/client/topic/images.js b/public/src/client/topic/images.js index a04b22a9e9..7506790fff 100644 --- a/public/src/client/topic/images.js +++ b/public/src/client/topic/images.js @@ -24,7 +24,7 @@ define('forum/topic/images', [], function () { if (!$this.parent().is('a')) { $this.wrap(''); } }); diff --git a/public/src/sockets.js b/public/src/sockets.js index 2927c21619..d12f32c332 100644 --- a/public/src/sockets.js +++ b/public/src/sockets.js @@ -15,6 +15,9 @@ app = window.app || {}; reconnectionDelay: config.reconnectionDelay, transports: config.socketioTransports, path: config.relative_path + '/socket.io', + query: { + _csrf: config.csrf_token, + }, }; window.socket = io(config.websocketAddress, ioParams); diff --git a/src/analytics.js b/src/analytics.js index 03be429637..73a2f164b8 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -237,23 +237,26 @@ Analytics.getDailyStatsForSet = async function (set, day, numDays) { set = `analytics:${set}`; } - const daysArr = []; day = new Date(day); // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values day.setDate(day.getDate() + 1); day.setHours(0, 0, 0, 0); - while (numDays > 0) { - /* eslint-disable no-await-in-loop */ + async function getHourlyStats(hour) { const dayData = await Analytics.getHourlyStatsForSet( set, - day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1)), + hour, 24 ); - daysArr.push(dayData.reduce((cur, next) => cur + next)); + return dayData.reduce((cur, next) => cur + next); + } + const hours = []; + while (numDays > 0) { + hours.push(day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1))); numDays -= 1; } - return daysArr; + + return await Promise.all(hours.map(getHourlyStats)); }; Analytics.getUnwrittenPageviews = function () { diff --git a/src/categories/create.js b/src/categories/create.js index ce3f01f22e..c2f7fb7968 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -95,11 +95,9 @@ module.exports = function (Categories) { await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']); await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']); - cache.del([ - 'categories:cid', - `cid:${parentCid}:children`, - `cid:${parentCid}:children:all`, - ]); + cache.del('categories:cid'); + await clearParentCategoryCache(parentCid); + if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid); } @@ -112,6 +110,22 @@ module.exports = function (Categories) { return category; }; + async function clearParentCategoryCache(parentCid) { + while (parseInt(parentCid, 10) >= 0) { + cache.del([ + `cid:${parentCid}:children`, + `cid:${parentCid}:children:all`, + ]); + + if (parseInt(parentCid, 10) === 0) { + return; + } + // clear all the way to root + // eslint-disable-next-line no-await-in-loop + parentCid = await Categories.getCategoryField(parentCid, 'parentCid'); + } + } + async function duplicateCategoriesChildren(parentCid, cid, uid) { let children = await Categories.getChildren([cid], uid); if (!children.length) { diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index ac9f4fc130..193476f1af 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -128,12 +128,13 @@ async function getStats() { } let results = await Promise.all([ - getStatsForSet('ip:recent', 'uniqueIPCount'), + getStatsFromAnalytics('uniquevisitors', 'uniqueIPCount'), getStatsFromAnalytics('logins', 'loginCount'), getStatsForSet('users:joindate', 'userCount'), getStatsForSet('posts:pid', 'postCount'), getStatsForSet('topics:tid', 'topicCount'), ]); + results[0].name = '[[admin/dashboard:unique-visitors]]'; results[1].name = '[[admin/dashboard:logins]]'; diff --git a/src/controllers/api.js b/src/controllers/api.js index 7474f6e7a0..5db3b2d9b0 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -9,6 +9,7 @@ const categories = require('../categories'); const plugins = require('../plugins'); const translator = require('../translator'); const languages = require('../languages'); +const { generateToken } = require('../middleware/csrf'); const apiController = module.exports; @@ -64,7 +65,7 @@ apiController.loadConfig = async function (req) { 'cache-buster': meta.config['cache-buster'] || '', topicPostSort: meta.config.topicPostSort || 'oldest_to_newest', categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest', - csrf_token: req.uid >= 0 && req.csrfToken && req.csrfToken(), + csrf_token: req.uid >= 0 ? generateToken(req) : undefined, searchEnabled: plugins.hooks.hasListeners('filter:search.query'), searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles', bootswatchSkin: meta.config.bootswatchSkin || '', diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 82dc6b1660..bb6e6bf49f 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -383,7 +383,7 @@ authenticationController.onSuccessfulLogin = async function (req, uid) { }), user.auth.addSession(uid, req.sessionID), user.updateLastOnlineTime(uid), - user.updateOnlineUsers(uid), + user.onUserOnline(uid, Date.now()), analytics.increment('logins'), db.incrObjectFieldBy('global', 'loginCount', 1), ]); diff --git a/src/meta/themes.js b/src/meta/themes.js index d4d3f58f45..0af75640c9 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -108,7 +108,7 @@ Themes.set = async (data) => { await db.sortedSetAdd('plugins:active', numPlugins, data.id); } else if (!activePluginsConfig.includes(data.id)) { // This prevents changing theme when configuration doesn't include it, but allows it otherwise - winston.error('When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP'); + winston.error(`When defining active plugins in configuration, changing themes requires adding the theme '${data.id}' to the list of active plugins before updating it in the ACP`); throw new Error('[[error:theme-not-set-in-configuration]]'); } diff --git a/src/middleware/csrf.js b/src/middleware/csrf.js new file mode 100644 index 0000000000..be5b0761fe --- /dev/null +++ b/src/middleware/csrf.js @@ -0,0 +1,26 @@ +'use strict'; + +const { csrfSync } = require('csrf-sync'); + +const { + generateToken, + csrfSynchronisedProtection, + isRequestValid, +} = csrfSync({ + getTokenFromRequest: (req) => { + if (req.headers['x-csrf-token']) { + return req.headers['x-csrf-token']; + } else if (req.body && req.body.csrf_token) { + return req.body.csrf_token; + } else if (req.query) { + return req.query._csrf; + } + }, + size: 64, +}); + +module.exports = { + generateToken, + csrfSynchronisedProtection, + isRequestValid, +}; diff --git a/src/middleware/index.js b/src/middleware/index.js index 96bd3da398..d98064a1d5 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -2,11 +2,11 @@ const async = require('async'); const path = require('path'); -const csrf = require('csurf'); const validator = require('validator'); const nconf = require('nconf'); const toobusy = require('toobusy-js'); const util = require('util'); +const { csrfSynchronisedProtection } = require('./csrf'); const plugins = require('../plugins'); const meta = require('../meta'); @@ -34,7 +34,7 @@ middleware.regexes = { timestampedUpload: /^\d+-.+$/, }; -const csrfMiddleware = csrf(); +const csrfMiddleware = csrfSynchronisedProtection; middleware.applyCSRF = function (req, res, next) { if (req.uid >= 0) { @@ -102,11 +102,20 @@ middleware.pluginHooks = helpers.try(async (req, res, next) => { }); middleware.validateFiles = function validateFiles(req, res, next) { - if (!Array.isArray(req.files.files) || !req.files.files.length) { + if (!req.files.files) { return next(new Error(['[[error:invalid-files]]'])); } - next(); + if (Array.isArray(req.files.files) && req.files.files.length) { + return next(); + } + + if (typeof req.files.files === 'object') { + req.files.files = [req.files.files]; + return next(); + } + + return next(new Error(['[[error:invalid-files]]'])); }; middleware.prepareAPI = function prepareAPI(req, res, next) { diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 934fdec80e..62c1e15363 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -10,6 +10,7 @@ const meta = require('../meta'); const controllers = require('../controllers'); const helpers = require('../controllers/helpers'); const plugins = require('../plugins'); +const { generateToken } = require('../middleware/csrf'); let loginStrategies = []; @@ -108,7 +109,7 @@ Auth.reloadRoutes = async function (params) { }; if (strategy.checkState !== false) { - req.session.ssoState = req.csrfToken && req.csrfToken(); + req.session.ssoState = generateToken(req, true); opts.state = req.session.ssoState; } diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 30e4e23581..d457afefbc 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -34,13 +34,25 @@ Sockets.init = async function (server) { } } - io.use(authorize); - io.on('connection', onConnection); const opts = { transports: nconf.get('socket.io:transports') || ['polling', 'websocket'], cookie: false, + allowRequest: (req, callback) => { + authorize(req, (err) => { + if (err) { + return callback(err); + } + const csrf = require('../middleware/csrf'); + const isValid = csrf.isRequestValid({ + session: req.session || {}, + query: req._query, + headers: req.headers, + }); + callback(null, isValid); + }); + }, }; /* * Restrict socket.io listener to cookie domain. If none is set, infer based on url. @@ -62,7 +74,11 @@ Sockets.init = async function (server) { }; function onConnection(socket) { - socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0]; + socket.uid = socket.request.uid; + socket.ip = ( + socket.request.headers['x-forwarded-for'] || + socket.request.connection.remoteAddress || '' + ).split(',')[0]; socket.request.ip = socket.ip; logger.io_one(socket, socket.uid); @@ -112,48 +128,49 @@ async function onMessage(socket, payload) { return winston.warn('[socket.io] Empty payload'); } - const eventName = payload.data[0]; + let eventName = payload.data[0]; const params = typeof payload.data[1] === 'function' ? {} : payload.data[1]; const callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {}; - if (!eventName) { - return winston.warn('[socket.io] Empty method name'); - } - - if (typeof eventName !== 'string') { - const escapedName = validator.escape(String(eventName)); - return callback({ message: `[[error:invalid-event, ${escapedName}]]` }); - } + try { + if (!eventName) { + return winston.warn('[socket.io] Empty method name'); + } - const parts = eventName.split('.'); - const namespace = parts[0]; - const methodToCall = parts.reduce((prev, cur) => { - if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) { - return prev[cur]; + if (typeof eventName !== 'string') { + eventName = typeof eventName; + const escapedName = validator.escape(eventName); + return callback({ message: `[[error:invalid-event, ${escapedName}]]` }); } - return null; - }, Namespaces); - if (!methodToCall || typeof methodToCall !== 'function') { - if (process.env.NODE_ENV === 'development') { - winston.warn(`[socket.io] Unrecognized message: ${eventName}`); + const parts = eventName.split('.'); + const namespace = parts[0]; + const methodToCall = parts.reduce((prev, cur) => { + if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) { + return prev[cur]; + } + return null; + }, Namespaces); + + if (!methodToCall || typeof methodToCall !== 'function') { + if (process.env.NODE_ENV === 'development') { + winston.warn(`[socket.io] Unrecognized message: ${eventName}`); + } + const escapedName = validator.escape(String(eventName)); + return callback({ message: `[[error:invalid-event, ${escapedName}]]` }); } - const escapedName = validator.escape(String(eventName)); - return callback({ message: `[[error:invalid-event, ${escapedName}]]` }); - } - socket.previousEvents = socket.previousEvents || []; - socket.previousEvents.push(eventName); - if (socket.previousEvents.length > 20) { - socket.previousEvents.shift(); - } + socket.previousEvents = socket.previousEvents || []; + socket.previousEvents.push(eventName); + if (socket.previousEvents.length > 20) { + socket.previousEvents.shift(); + } - if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { - winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`); - return socket.disconnect(); - } + if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { + winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`); + return socket.disconnect(); + } - try { await checkMaintenance(socket); await validateSession(socket, '[[error:revalidate-failure]]'); @@ -230,9 +247,7 @@ async function validateSession(socket, errorMsg) { const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err))); -async function authorize(socket, callback) { - const { request } = socket; - +async function authorize(request, callback) { if (!request) { return callback(new Error('[[error:not-authorized]]')); } @@ -245,15 +260,13 @@ async function authorize(socket, callback) { }); const sessionData = await getSessionAsync(sessionId); - + request.session = sessionData; + let uid = 0; if (sessionData && sessionData.passport && sessionData.passport.user) { - request.session = sessionData; - socket.uid = parseInt(sessionData.passport.user, 10); - } else { - socket.uid = 0; + uid = parseInt(sessionData.passport.user, 10); } - request.uid = socket.uid; - callback(); + request.uid = uid; + callback(null, uid); } Sockets.in = function (room) { diff --git a/src/topics/tags.js b/src/topics/tags.js index 314b5cfd84..3b17027672 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -232,10 +232,15 @@ module.exports = function (Topics) { if (!tids.length) { return; } + let topicsTags = await Topics.getTopicsTags(tids); + topicsTags = topicsTags.map( + topicTags => topicTags.filter(topicTag => topicTag && topicTag !== tag) + ); - await db.deleteObjectFields( - tids.map(tid => `topic:${tid}`), - ['tags'], + await db.setObjectBulk( + tids.map((tid, index) => ([ + `topic:${tid}`, { tags: topicsTags[index].join(',') }, + ])) ); }); } diff --git a/src/user/interstitials.js b/src/user/interstitials.js index aa70e8098f..bebf12b12b 100644 --- a/src/user/interstitials.js +++ b/src/user/interstitials.js @@ -40,6 +40,10 @@ Interstitials.email = async (data) => { issuePasswordChallenge: !!data.userData.uid && hasPassword, }, callback: async (userData, formData) => { + if (formData.email) { + formData.email = String(formData.email).trim(); + } + // Validate and send email confirmation if (userData.uid) { const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10); diff --git a/src/user/online.js b/src/user/online.js index ffba4c9a94..b7c6b9d45a 100644 --- a/src/user/online.js +++ b/src/user/online.js @@ -27,9 +27,13 @@ module.exports = function (User) { if (now - parseInt(userOnlineTime, 10) < 300000) { return; } - await db.sortedSetAdd('users:online', now, uid); + await User.onUserOnline(uid, now); topics.pushUnreadCount(uid); - plugins.hooks.fire('action:user.online', { uid: uid, timestamp: now }); + }; + + User.onUserOnline = async (uid, timestamp) => { + await db.sortedSetAdd('users:online', timestamp, uid); + plugins.hooks.fire('action:user.online', { uid, timestamp }); }; User.isOnline = async function (uid) { diff --git a/test/helpers/index.js b/test/helpers/index.js index 6ca11e9252..4b536db004 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -95,7 +95,7 @@ helpers.logoutUser = function (jar, callback) { }); }; -helpers.connectSocketIO = function (res, callback) { +helpers.connectSocketIO = function (res, csrf_token, callback) { const io = require('socket.io-client'); let cookies = res.headers['set-cookie']; cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c)); @@ -106,6 +106,9 @@ helpers.connectSocketIO = function (res, callback) { Origin: nconf.get('url'), Cookie: cookie, }, + query: { + _csrf: csrf_token, + }, }); socket.on('connect', () => { @@ -121,7 +124,6 @@ helpers.uploadFile = function (uploadEndPoint, filePath, body, jar, csrf_token, let formData = { files: [ fs.createReadStream(filePath), - fs.createReadStream(filePath), // see https://github.com/request/request/issues/2445 ], }; formData = utils.merge(formData, body); diff --git a/test/socket.io.js b/test/socket.io.js index 110258a0de..33377e8bb3 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -73,7 +73,7 @@ describe('socket.io', () => { }, (err, res) => { assert.ifError(err); - helpers.connectSocketIO(res, (err, _io) => { + helpers.connectSocketIO(res, body.csrf_token, (err, _io) => { io = _io; assert.ifError(err); @@ -110,8 +110,7 @@ describe('socket.io', () => { it('should return error for invalid eventName type', (done) => { const eventName = ['topics.loadMoreTags']; io.emit(eventName, (err) => { - const eventAsString = String(eventName); - assert.strictEqual(err.message, `[[error:invalid-event, ${eventAsString}]]`); + assert.strictEqual(err.message, `[[error:invalid-event, object]]`); done(); }); }); diff --git a/test/topics.js b/test/topics.js index 44587639bc..f27ed80ad4 100644 --- a/test/topics.js +++ b/test/topics.js @@ -1932,6 +1932,14 @@ describe('Topic\'s', () => { }); }); + it('should only delete one tag from topic', async () => { + const result1 = await topics.post({ uid: adminUid, tags: ['deleteme1', 'deleteme2', 'deleteme3'], title: 'topic tagged with plugins', content: 'topic 1 content', cid: topic.categoryId }); + await topics.deleteTag('deleteme2'); + const topicData = await topics.getTopicData(result1.topicData.tid); + const tags = topicData.tags.map(t => t.value); + assert.deepStrictEqual(tags, ['deleteme1', 'deleteme3']); + }); + it('should delete tag', (done) => { topics.deleteTag('javascript', (err) => { assert.ifError(err); diff --git a/test/uploads.js b/test/uploads.js index 5be0818b5e..1eecfe9f1f 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -87,8 +87,7 @@ describe('Upload Controllers', () => { const oldValue = meta.config.allowedFileExtensions; meta.config.allowedFileExtensions = 'png,jpg,bmp,html'; require('../src/middleware/uploads').clearCache(); - // why / 2? see: helpers.uploadFile for a weird quirk where we actually upload 2 files per upload in our tests. - const times = (meta.config.uploadRateLimitThreshold / 2) + 1; + const times = meta.config.uploadRateLimitThreshold + 1; async.timesSeries(times, (i, next) => { helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/503.html'), {}, jar, csrf_token, (err, res, body) => { if (i + 1 >= times) { @@ -522,7 +521,7 @@ describe('Upload Controllers', () => { it('should return files with no post associated with them', async () => { const orphans = await posts.uploads.getOrphans(); - assert.strictEqual(orphans.length, 2); + assert.strictEqual(orphans.length, 1); orphans.forEach((relPath) => { assert(relPath.startsWith('files/')); assert(relPath.endsWith('test.png')); @@ -553,7 +552,7 @@ describe('Upload Controllers', () => { await posts.uploads.cleanOrphans(); const orphans = await posts.uploads.getOrphans(); - assert.strictEqual(orphans.length, 2); + assert.strictEqual(orphans.length, 1); }); it('should not touch orphans if they are newer than the configured expiry', async () => { @@ -561,7 +560,7 @@ describe('Upload Controllers', () => { await posts.uploads.cleanOrphans(); const orphans = await posts.uploads.getOrphans(); - assert.strictEqual(orphans.length, 2); + assert.strictEqual(orphans.length, 1); }); it('should delete orphans older than the configured number of days', async () => {