Merge branch 'v2.x' into isekai-main

isekai-main
落雨楓 2 years ago
commit d38671a082

@ -13,13 +13,14 @@ on:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel # A workflow run is made up of one or more jobs that can run sequentially or in parallel
permissions: permissions:
contents: read contents: read
packages: write
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
@ -32,14 +33,15 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} registry: ghcr.io
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4
with: with:
images: nodebb/docker images: ghcr.io/${{ github.repository }}
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}

@ -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) #### v2.8.9 (2023-03-19)
##### Chores ##### Chores
@ -5,34 +48,6 @@
* up cron (73a50d17) * up cron (73a50d17)
* incrementing version number - v2.8.8 (b331b942) * incrementing version number - v2.8.8 (b331b942)
* update changelog for v2.8.8 (c03d5db7) * 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 ##### Bug Fixes
@ -55,33 +70,6 @@
* incrementing version number - v2.8.7 (3f8248d6) * incrementing version number - v2.8.7 (3f8248d6)
* update changelog for v2.8.7 (2ca38e7b) * 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 ##### Bug Fixes
@ -97,32 +85,6 @@
* incrementing version number - v2.8.6 (af6ce447) * incrementing version number - v2.8.6 (af6ce447)
* update changelog for v2.8.6 (f3306d03) * 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 ##### Documentation Changes
@ -151,31 +113,6 @@
* **i18n:** fallback strings for new resources: nodebb.error (8335f90a) * **i18n:** fallback strings for new resources: nodebb.error (8335f90a)
* incrementing version number - v2.8.5 (bff5ce2d) * incrementing version number - v2.8.5 (bff5ce2d)
* update changelog for v2.8.5 (24e58c28) * 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 ##### New Features
@ -201,30 +138,6 @@
* incrementing version number - v2.8.4 (a46b2bbc) * incrementing version number - v2.8.4 (a46b2bbc)
* update changelog for v2.8.4 (c13f0e21) * 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 ##### Bug Fixes
@ -236,29 +149,6 @@
* incrementing version number - v2.8.3 (c20b20a7) * incrementing version number - v2.8.3 (c20b20a7)
* update changelog for v2.8.3 (eb2841ee) * 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) #### v2.8.3 (2023-01-25)
@ -267,28 +157,6 @@
* remove extraneous lines from changelog (48c9f447) * remove extraneous lines from changelog (48c9f447)
* incrementing version number - v2.8.2 (050e43f8) * incrementing version number - v2.8.2 (050e43f8)
* update changelog for v2.8.2 (66aa3169) * 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 ##### Bug Fixes

@ -2,7 +2,7 @@
"name": "nodebb", "name": "nodebb",
"license": "GPL-3.0", "license": "GPL-3.0",
"description": "NodeBB Forum", "description": "NodeBB Forum",
"version": "2.8.10", "version": "2.8.17",
"homepage": "http://www.nodebb.org", "homepage": "http://www.nodebb.org",
"repository": { "repository": {
"type": "git", "type": "git",
@ -55,7 +55,7 @@
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
"cron": "2.3.0", "cron": "2.3.0",
"cropperjs": "1.5.13", "cropperjs": "1.5.13",
"csurf": "1.11.0", "csrf-sync": "4.0.1",
"daemon": "1.1.0", "daemon": "1.1.0",
"diff": "5.1.0", "diff": "5.1.0",
"esbuild": "0.16.10", "esbuild": "0.16.10",
@ -90,7 +90,7 @@
"@nodebb/bootswatch": "3.4.2", "@nodebb/bootswatch": "3.4.2",
"nconf": "0.12.0", "nconf": "0.12.0",
"nodebb-plugin-2factor": "5.1.2", "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-dbsearch": "5.1.5",
"nodebb-plugin-emoji": "4.0.6", "nodebb-plugin-emoji": "4.0.6",
"nodebb-plugin-emoji-android": "3.0.0", "nodebb-plugin-emoji-android": "3.0.0",

@ -24,7 +24,7 @@ define('forum/topic/images', [], function () {
if (!$this.parent().is('a')) { if (!$this.parent().is('a')) {
$this.wrap('<a href="' + src + '" ' + $this.wrap('<a href="' + src + '" ' +
(!srcExt && altExt ? ' download="' + altFilename + '" ' : '') + (!srcExt && altExt ? ' download="' + utils.escapeHTML(altFilename) + '" ' : '') +
' target="_blank" rel="noopener">'); ' target="_blank" rel="noopener">');
} }
}); });

@ -15,6 +15,9 @@ app = window.app || {};
reconnectionDelay: config.reconnectionDelay, reconnectionDelay: config.reconnectionDelay,
transports: config.socketioTransports, transports: config.socketioTransports,
path: config.relative_path + '/socket.io', path: config.relative_path + '/socket.io',
query: {
_csrf: config.csrf_token,
},
}; };
window.socket = io(config.websocketAddress, ioParams); window.socket = io(config.websocketAddress, ioParams);

@ -237,23 +237,26 @@ Analytics.getDailyStatsForSet = async function (set, day, numDays) {
set = `analytics:${set}`; set = `analytics:${set}`;
} }
const daysArr = [];
day = new Date(day); day = new Date(day);
// set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values // set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values
day.setDate(day.getDate() + 1); day.setDate(day.getDate() + 1);
day.setHours(0, 0, 0, 0); day.setHours(0, 0, 0, 0);
while (numDays > 0) { async function getHourlyStats(hour) {
/* eslint-disable no-await-in-loop */
const dayData = await Analytics.getHourlyStatsForSet( const dayData = await Analytics.getHourlyStatsForSet(
set, set,
day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1)), hour,
24 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; numDays -= 1;
} }
return daysArr;
return await Promise.all(hours.map(getHourlyStats));
}; };
Analytics.getUnwrittenPageviews = function () { Analytics.getUnwrittenPageviews = function () {

@ -95,11 +95,9 @@ module.exports = function (Categories) {
await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']); await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']);
await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']); await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']);
cache.del([ cache.del('categories:cid');
'categories:cid', await clearParentCategoryCache(parentCid);
`cid:${parentCid}:children`,
`cid:${parentCid}:children:all`,
]);
if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) { if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) {
category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid); category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid);
} }
@ -112,6 +110,22 @@ module.exports = function (Categories) {
return category; 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) { async function duplicateCategoriesChildren(parentCid, cid, uid) {
let children = await Categories.getChildren([cid], uid); let children = await Categories.getChildren([cid], uid);
if (!children.length) { if (!children.length) {

@ -128,12 +128,13 @@ async function getStats() {
} }
let results = await Promise.all([ let results = await Promise.all([
getStatsForSet('ip:recent', 'uniqueIPCount'), getStatsFromAnalytics('uniquevisitors', 'uniqueIPCount'),
getStatsFromAnalytics('logins', 'loginCount'), getStatsFromAnalytics('logins', 'loginCount'),
getStatsForSet('users:joindate', 'userCount'), getStatsForSet('users:joindate', 'userCount'),
getStatsForSet('posts:pid', 'postCount'), getStatsForSet('posts:pid', 'postCount'),
getStatsForSet('topics:tid', 'topicCount'), getStatsForSet('topics:tid', 'topicCount'),
]); ]);
results[0].name = '[[admin/dashboard:unique-visitors]]'; results[0].name = '[[admin/dashboard:unique-visitors]]';
results[1].name = '[[admin/dashboard:logins]]'; results[1].name = '[[admin/dashboard:logins]]';

@ -9,6 +9,7 @@ const categories = require('../categories');
const plugins = require('../plugins'); const plugins = require('../plugins');
const translator = require('../translator'); const translator = require('../translator');
const languages = require('../languages'); const languages = require('../languages');
const { generateToken } = require('../middleware/csrf');
const apiController = module.exports; const apiController = module.exports;
@ -64,7 +65,7 @@ apiController.loadConfig = async function (req) {
'cache-buster': meta.config['cache-buster'] || '', 'cache-buster': meta.config['cache-buster'] || '',
topicPostSort: meta.config.topicPostSort || 'oldest_to_newest', topicPostSort: meta.config.topicPostSort || 'oldest_to_newest',
categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest', 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'), searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles', searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles',
bootswatchSkin: meta.config.bootswatchSkin || '', bootswatchSkin: meta.config.bootswatchSkin || '',

@ -383,7 +383,7 @@ authenticationController.onSuccessfulLogin = async function (req, uid) {
}), }),
user.auth.addSession(uid, req.sessionID), user.auth.addSession(uid, req.sessionID),
user.updateLastOnlineTime(uid), user.updateLastOnlineTime(uid),
user.updateOnlineUsers(uid), user.onUserOnline(uid, Date.now()),
analytics.increment('logins'), analytics.increment('logins'),
db.incrObjectFieldBy('global', 'loginCount', 1), db.incrObjectFieldBy('global', 'loginCount', 1),
]); ]);

@ -108,7 +108,7 @@ Themes.set = async (data) => {
await db.sortedSetAdd('plugins:active', numPlugins, data.id); await db.sortedSetAdd('plugins:active', numPlugins, data.id);
} else if (!activePluginsConfig.includes(data.id)) { } else if (!activePluginsConfig.includes(data.id)) {
// This prevents changing theme when configuration doesn't include it, but allows it otherwise // 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]]'); throw new Error('[[error:theme-not-set-in-configuration]]');
} }

@ -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,
};

@ -2,11 +2,11 @@
const async = require('async'); const async = require('async');
const path = require('path'); const path = require('path');
const csrf = require('csurf');
const validator = require('validator'); const validator = require('validator');
const nconf = require('nconf'); const nconf = require('nconf');
const toobusy = require('toobusy-js'); const toobusy = require('toobusy-js');
const util = require('util'); const util = require('util');
const { csrfSynchronisedProtection } = require('./csrf');
const plugins = require('../plugins'); const plugins = require('../plugins');
const meta = require('../meta'); const meta = require('../meta');
@ -34,7 +34,7 @@ middleware.regexes = {
timestampedUpload: /^\d+-.+$/, timestampedUpload: /^\d+-.+$/,
}; };
const csrfMiddleware = csrf(); const csrfMiddleware = csrfSynchronisedProtection;
middleware.applyCSRF = function (req, res, next) { middleware.applyCSRF = function (req, res, next) {
if (req.uid >= 0) { if (req.uid >= 0) {
@ -102,11 +102,20 @@ middleware.pluginHooks = helpers.try(async (req, res, next) => {
}); });
middleware.validateFiles = function validateFiles(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]]'])); 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) { middleware.prepareAPI = function prepareAPI(req, res, next) {

@ -10,6 +10,7 @@ const meta = require('../meta');
const controllers = require('../controllers'); const controllers = require('../controllers');
const helpers = require('../controllers/helpers'); const helpers = require('../controllers/helpers');
const plugins = require('../plugins'); const plugins = require('../plugins');
const { generateToken } = require('../middleware/csrf');
let loginStrategies = []; let loginStrategies = [];
@ -108,7 +109,7 @@ Auth.reloadRoutes = async function (params) {
}; };
if (strategy.checkState !== false) { if (strategy.checkState !== false) {
req.session.ssoState = req.csrfToken && req.csrfToken(); req.session.ssoState = generateToken(req, true);
opts.state = req.session.ssoState; opts.state = req.session.ssoState;
} }

@ -34,13 +34,25 @@ Sockets.init = async function (server) {
} }
} }
io.use(authorize);
io.on('connection', onConnection); io.on('connection', onConnection);
const opts = { const opts = {
transports: nconf.get('socket.io:transports') || ['polling', 'websocket'], transports: nconf.get('socket.io:transports') || ['polling', 'websocket'],
cookie: false, 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. * 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) { 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; socket.request.ip = socket.ip;
logger.io_one(socket, socket.uid); logger.io_one(socket, socket.uid);
@ -112,48 +128,49 @@ async function onMessage(socket, payload) {
return winston.warn('[socket.io] Empty 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 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 () {}; const callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {};
if (!eventName) { try {
return winston.warn('[socket.io] Empty method name'); 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}]]` });
}
const parts = eventName.split('.'); if (typeof eventName !== 'string') {
const namespace = parts[0]; eventName = typeof eventName;
const methodToCall = parts.reduce((prev, cur) => { const escapedName = validator.escape(eventName);
if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) { return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
return prev[cur];
} }
return null;
}, Namespaces);
if (!methodToCall || typeof methodToCall !== 'function') { const parts = eventName.split('.');
if (process.env.NODE_ENV === 'development') { const namespace = parts[0];
winston.warn(`[socket.io] Unrecognized message: ${eventName}`); 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 = socket.previousEvents || [];
socket.previousEvents.push(eventName); socket.previousEvents.push(eventName);
if (socket.previousEvents.length > 20) { if (socket.previousEvents.length > 20) {
socket.previousEvents.shift(); socket.previousEvents.shift();
} }
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) { if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`); winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
return socket.disconnect(); return socket.disconnect();
} }
try {
await checkMaintenance(socket); await checkMaintenance(socket);
await validateSession(socket, '[[error:revalidate-failure]]'); 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))); const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err)));
async function authorize(socket, callback) { async function authorize(request, callback) {
const { request } = socket;
if (!request) { if (!request) {
return callback(new Error('[[error:not-authorized]]')); return callback(new Error('[[error:not-authorized]]'));
} }
@ -245,15 +260,13 @@ async function authorize(socket, callback) {
}); });
const sessionData = await getSessionAsync(sessionId); const sessionData = await getSessionAsync(sessionId);
request.session = sessionData;
let uid = 0;
if (sessionData && sessionData.passport && sessionData.passport.user) { if (sessionData && sessionData.passport && sessionData.passport.user) {
request.session = sessionData; uid = parseInt(sessionData.passport.user, 10);
socket.uid = parseInt(sessionData.passport.user, 10);
} else {
socket.uid = 0;
} }
request.uid = socket.uid; request.uid = uid;
callback(); callback(null, uid);
} }
Sockets.in = function (room) { Sockets.in = function (room) {

@ -232,10 +232,15 @@ module.exports = function (Topics) {
if (!tids.length) { if (!tids.length) {
return; return;
} }
let topicsTags = await Topics.getTopicsTags(tids);
topicsTags = topicsTags.map(
topicTags => topicTags.filter(topicTag => topicTag && topicTag !== tag)
);
await db.deleteObjectFields( await db.setObjectBulk(
tids.map(tid => `topic:${tid}`), tids.map((tid, index) => ([
['tags'], `topic:${tid}`, { tags: topicsTags[index].join(',') },
]))
); );
}); });
} }

@ -40,6 +40,10 @@ Interstitials.email = async (data) => {
issuePasswordChallenge: !!data.userData.uid && hasPassword, issuePasswordChallenge: !!data.userData.uid && hasPassword,
}, },
callback: async (userData, formData) => { callback: async (userData, formData) => {
if (formData.email) {
formData.email = String(formData.email).trim();
}
// Validate and send email confirmation // Validate and send email confirmation
if (userData.uid) { if (userData.uid) {
const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10); const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10);

@ -27,9 +27,13 @@ module.exports = function (User) {
if (now - parseInt(userOnlineTime, 10) < 300000) { if (now - parseInt(userOnlineTime, 10) < 300000) {
return; return;
} }
await db.sortedSetAdd('users:online', now, uid); await User.onUserOnline(uid, now);
topics.pushUnreadCount(uid); 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) { User.isOnline = async function (uid) {

@ -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'); const io = require('socket.io-client');
let cookies = res.headers['set-cookie']; let cookies = res.headers['set-cookie'];
cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c)); cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c));
@ -106,6 +106,9 @@ helpers.connectSocketIO = function (res, callback) {
Origin: nconf.get('url'), Origin: nconf.get('url'),
Cookie: cookie, Cookie: cookie,
}, },
query: {
_csrf: csrf_token,
},
}); });
socket.on('connect', () => { socket.on('connect', () => {
@ -121,7 +124,6 @@ helpers.uploadFile = function (uploadEndPoint, filePath, body, jar, csrf_token,
let formData = { let formData = {
files: [ files: [
fs.createReadStream(filePath), fs.createReadStream(filePath),
fs.createReadStream(filePath), // see https://github.com/request/request/issues/2445
], ],
}; };
formData = utils.merge(formData, body); formData = utils.merge(formData, body);

@ -73,7 +73,7 @@ describe('socket.io', () => {
}, (err, res) => { }, (err, res) => {
assert.ifError(err); assert.ifError(err);
helpers.connectSocketIO(res, (err, _io) => { helpers.connectSocketIO(res, body.csrf_token, (err, _io) => {
io = _io; io = _io;
assert.ifError(err); assert.ifError(err);
@ -110,8 +110,7 @@ describe('socket.io', () => {
it('should return error for invalid eventName type', (done) => { it('should return error for invalid eventName type', (done) => {
const eventName = ['topics.loadMoreTags']; const eventName = ['topics.loadMoreTags'];
io.emit(eventName, (err) => { io.emit(eventName, (err) => {
const eventAsString = String(eventName); assert.strictEqual(err.message, `[[error:invalid-event, object]]`);
assert.strictEqual(err.message, `[[error:invalid-event, ${eventAsString}]]`);
done(); done();
}); });
}); });

@ -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) => { it('should delete tag', (done) => {
topics.deleteTag('javascript', (err) => { topics.deleteTag('javascript', (err) => {
assert.ifError(err); assert.ifError(err);

@ -87,8 +87,7 @@ describe('Upload Controllers', () => {
const oldValue = meta.config.allowedFileExtensions; const oldValue = meta.config.allowedFileExtensions;
meta.config.allowedFileExtensions = 'png,jpg,bmp,html'; meta.config.allowedFileExtensions = 'png,jpg,bmp,html';
require('../src/middleware/uploads').clearCache(); 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 + 1;
const times = (meta.config.uploadRateLimitThreshold / 2) + 1;
async.timesSeries(times, (i, next) => { 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) => { 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) { if (i + 1 >= times) {
@ -522,7 +521,7 @@ describe('Upload Controllers', () => {
it('should return files with no post associated with them', async () => { it('should return files with no post associated with them', async () => {
const orphans = await posts.uploads.getOrphans(); const orphans = await posts.uploads.getOrphans();
assert.strictEqual(orphans.length, 2); assert.strictEqual(orphans.length, 1);
orphans.forEach((relPath) => { orphans.forEach((relPath) => {
assert(relPath.startsWith('files/')); assert(relPath.startsWith('files/'));
assert(relPath.endsWith('test.png')); assert(relPath.endsWith('test.png'));
@ -553,7 +552,7 @@ describe('Upload Controllers', () => {
await posts.uploads.cleanOrphans(); await posts.uploads.cleanOrphans();
const orphans = await posts.uploads.getOrphans(); 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 () => { 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(); await posts.uploads.cleanOrphans();
const orphans = await posts.uploads.getOrphans(); 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 () => { it('should delete orphans older than the configured number of days', async () => {

Loading…
Cancel
Save