diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index fda479175b..ae2e03501a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -47,6 +47,7 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.x type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch,enable=${{ github.event.repository.default_branch != github.ref }} - name: Build and push Docker images uses: docker/build-push-action@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 02db5f4bac..e846b96420 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +#### v3.1.2 (2023-05-12) + +##### Chores + +* incrementing version number - v3.1.1 (40250733) +* update changelog for v3.1.1 (ccd6f48c) +* incrementing version number - v3.1.0 (0cb386bd) +* incrementing version number - v3.0.1 (26f6ea49) +* incrementing version number - v3.0.0 (224e08cd) + +##### Bug Fixes + +* #11595, use default value (28740de7) + #### v3.1.1 (2023-05-11) ##### Chores diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 98833e1e92..ecb724b3c9 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -235,15 +235,14 @@ define('forum/chats', [ } }); mousetrap.bind('up', function (e) { - if (e.target === components.get('chat/input').get(0)) { + const inputEl = components.get('chat/input'); + if (e.target === inputEl.get(0) && !inputEl.val()) { // Retrieve message id from messages list const message = components.get('chat/messages').find('.chat-message[data-self="1"]').last(); if (!message.length) { return; } const lastMid = message.attr('data-mid'); - const inputEl = components.get('chat/input'); - messages.prepEdit(inputEl, lastMid, ajaxify.data.roomId); } }); diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index e3bccacdca..b17a8f772b 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -176,6 +176,11 @@ define('forum/chats/messages', [ autoCompleteEl.destroy(); } } + textarea.on('keyup', (e) => { + if (e.key === 'Escape') { + finishEdit(); + } + }); editEl.find('[data-action="cancel"]').on('click', finishEdit); editEl.find('[data-action="save"]').on('click', function () { diff --git a/public/src/sockets.js b/public/src/sockets.js index 6d3ccfbb80..acd3ba3dbe 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/meta/themes.js b/src/meta/themes.js index d757cfb9ec..6cce964832 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -89,9 +89,9 @@ Themes.set = async (data) => { switch (data.type) { case 'local': { const current = await Meta.configs.get('theme:id'); + const score = await db.sortedSetScore('plugins:active', current); await db.sortedSetRemove('plugins:active', current); - const numPlugins = await db.sortedSetCard('plugins:active'); - await db.sortedSetAdd('plugins:active', numPlugins, data.id); + await db.sortedSetAdd('plugins:active', score || 0, data.id); if (current !== data.id) { const pathToThemeJson = path.join(nconf.get('themes_path'), data.id, 'theme.json'); @@ -103,9 +103,9 @@ Themes.set = async (data) => { config = JSON.parse(config); const activePluginsConfig = nconf.get('plugins:active'); if (!activePluginsConfig) { + const score = await db.sortedSetScore('plugins:active', current); await db.sortedSetRemove('plugins:active', current); - const numPlugins = await db.sortedSetCard('plugins:active'); - await db.sortedSetAdd('plugins:active', numPlugins, data.id); + await db.sortedSetAdd('plugins:active', score || 0, 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 theme '${data.id}' to the list of active plugins before updating it in the ACP`); diff --git a/src/middleware/csrf.js b/src/middleware/csrf.js index f6af0c625b..be5b0761fe 100644 --- a/src/middleware/csrf.js +++ b/src/middleware/csrf.js @@ -5,12 +5,15 @@ 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.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, @@ -19,4 +22,5 @@ const { module.exports = { generateToken, csrfSynchronisedProtection, + isRequestValid, }; diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 963267ed9a..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); @@ -231,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]]')); } @@ -246,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/webserver.js b/src/webserver.js index c0a1c8e537..55e263486a 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -199,13 +199,17 @@ function setupHelmet(app) { } if (meta.config['hsts-enabled']) { options.hsts = { - maxAge: meta.config['hsts-maxage'], + maxAge: Math.max(0, meta.config['hsts-maxage']), includeSubDomains: !!meta.config['hsts-subdomains'], preload: !!meta.config['hsts-preload'], }; } - app.use(helmet(options)); + try { + app.use(helmet(options)); + } catch (err) { + winston.error(`[startup] unable to initialize helmet \n${err.stack}`); + } } diff --git a/test/helpers/index.js b/test/helpers/index.js index b79bf66e2d..4adcb1b0dd 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -96,7 +96,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)); @@ -107,6 +107,9 @@ helpers.connectSocketIO = function (res, callback) { Origin: nconf.get('url'), Cookie: cookie, }, + query: { + _csrf: csrf_token, + }, }); socket.on('connect', () => { diff --git a/test/socket.io.js b/test/socket.io.js index 726f6f71ad..4e2292d4e5 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);