Merge branch 'heads/v2.8.10' into isekai-main

isekai-main
落雨楓 2 years ago
commit 5221956e2b

@ -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) #### v2.8.6 (2023-02-03)
##### Chores ##### Chores

@ -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.6", "version": "2.8.10",
"homepage": "http://www.nodebb.org", "homepage": "http://www.nodebb.org",
"repository": { "repository": {
"type": "git", "type": "git",
@ -53,7 +53,7 @@
"connect-pg-simple": "8.0.0", "connect-pg-simple": "8.0.0",
"connect-redis": "6.1.3", "connect-redis": "6.1.3",
"cookie-parser": "1.4.6", "cookie-parser": "1.4.6",
"cron": "2.1.0", "cron": "2.3.0",
"cropperjs": "1.5.13", "cropperjs": "1.5.13",
"csurf": "1.11.0", "csurf": "1.11.0",
"daemon": "1.1.0", "daemon": "1.1.0",
@ -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.4", "nodebb-plugin-composer-default": "9.2.5",
"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",

@ -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; const silent = nconf.get('silent') === 'false' ? false : nconf.get('silent') !== false;
let numProcs; let numProcs;
const workers = []; const workers = [];
const Loader = { const Loader = {};
timesStarted: 0,
};
const appPath = path.join(__dirname, 'app.js'); const appPath = path.join(__dirname, 'app.js');
Loader.init = function () { Loader.init = function () {
@ -57,21 +55,6 @@ Loader.displayStartupMessages = function () {
Loader.addWorkerEvents = function (worker) { Loader.addWorkerEvents = function (worker) {
worker.on('exit', (code, signal) => { 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})`); console.log(`[cluster] Child Process (${worker.pid}) has exited (code: ${code}, signal: ${signal})`);
if (!(worker.suicide || code === 0)) { if (!(worker.suicide || code === 0)) {
console.log('[cluster] Spinning up another process...'); console.log('[cluster] Spinning up another process...');

@ -265,6 +265,9 @@ TopicObjectSlim:
name: name:
type: string type: string
description: The topic thumbnail filename description: The topic thumbnail filename
path:
type: string
description: Path to topic thumbnail without upload_url prefix
url: url:
type: string type: string
description: Relative path to the topic thumbnail description: Relative path to the topic thumbnail

@ -19,6 +19,15 @@ post:
content: content:
type: string type: string
example: This is the test topic's content 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: tags:
type: array type: array
items: items:

@ -46,8 +46,6 @@ post:
content: content:
type: string type: string
example: This is a test reply example: This is a test reply
timestamp:
type: number
toPid: toPid:
type: number type: number
required: required:

@ -31,6 +31,8 @@ get:
type: string type: string
name: name:
type: string type: string
path:
type: string
url: url:
type: string type: string
description: Path to a topic thumbnail description: Path to a topic thumbnail
@ -155,6 +157,8 @@ delete:
type: string type: string
name: name:
type: string type: string
path:
type: string
url: url:
type: string type: string
description: Path to a topic thumbnail description: Path to a topic thumbnail

@ -38,4 +38,4 @@ put:
$ref: ../../../../components/schemas/Status.yaml#/Status $ref: ../../../../components/schemas/Status.yaml#/Status
response: response:
type: object type: object
properties: {} properties: {}

@ -9,15 +9,23 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
configureEmailTester(); configureEmailTester();
configureEmailEditor(); configureEmailEditor();
handleDigestHourChange(); handleDigestHourChange();
handleSmtpServiceChange();
$(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange); $(window).off('action:admin.settingsLoaded', onSettingsLoaded)
$(window).on('action:admin.settingsSaved', function () { .on('action:admin.settingsLoaded', onSettingsLoaded);
socket.emit('admin.user.restartJobs'); $(window).off('action:admin.settingsSaved', onSettingsSaved)
}); .on('action:admin.settingsSaved', onSettingsSaved);
$('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange);
}; };
function onSettingsLoaded() {
handleDigestHourChange();
handleSmtpServiceChange();
}
function onSettingsSaved() {
handleDigestHourChange();
socket.emit('admin.user.restartJobs');
}
function configureEmailTester() { function configureEmailTester() {
$('button[data-action="email.test"]').off('click').on('click', function () { $('button[data-action="email.test"]').off('click').on('click', function () {
socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) { 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() { function handleSmtpServiceChange() {
const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp'; function toggleCustomService() {
$('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom); 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,
});
}
} }
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; return module;

@ -77,6 +77,7 @@ define('forum/account/edit/password', [
ajaxify.go('user/' + ajaxify.data.userslug + '/edit'); ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
} }
}) })
.catch(alerts.error)
.finally(() => { .finally(() => {
btn.removeClass('disabled').find('i').addClass('hide'); btn.removeClass('disabled').find('i').addClass('hide');
currentPassword.val(''); currentPassword.val('');

@ -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 index = navigator.getIndex();
const count = navigator.getCount(); const count = navigator.getCount();
if (index === count) { if (index === count) {
@ -98,7 +102,11 @@ define('forum/topic', [
navigator.scrollToIndex(index, true, 0); navigator.scrollToIndex(index, true, 0);
}); });
mousetrap.bind('k', () => { mousetrap.bind('k', (e) => {
if (e.target.classList.contains('mousetrap')) {
return;
}
const index = navigator.getIndex(); const index = navigator.getIndex();
if (index === 1) { if (index === 1) {
return; return;

@ -41,7 +41,7 @@ define('forum/topic/postTools', [
const pid = postEl.attr('data-pid'); const pid = postEl.attr('data-pid');
const index = parseInt(postEl.attr('data-index'), 10); 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) { if (err) {
return alerts.error(err); return alerts.error(err);
} }

@ -443,6 +443,10 @@ usersAPI.changePicture = async (caller, data) => {
}; };
usersAPI.generateExport = async (caller, { uid, type }) => { 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}`); const count = await db.incrObjectField('locks', `export:${uid}${type}`);
if (count > 1) { if (count > 1) {
throw new Error('[[error:already-exporting]]'); throw new Error('[[error:already-exporting]]');

@ -32,12 +32,6 @@ try {
if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) { if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) {
const e = new TypeError(`Incorrect dependency version: ${packageName}`); const e = new TypeError(`Incorrect dependency version: ${packageName}`);
e.code = 'DEP_WRONG_VERSION'; 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; throw e;
} }
}; };
@ -57,6 +51,16 @@ try {
packageInstall.preserveExtraneousPlugins(); packageInstall.preserveExtraneousPlugins();
packageInstall.installAll(); 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'); const chalk = require('chalk');
console.log(`${chalk.green('OK')}\n`); console.log(`${chalk.green('OK')}\n`);
} else { } else {

@ -132,11 +132,11 @@ modsController.flags.detail = async function (req, res, next) {
uids = _.uniq(admins.concat(uids)); uids = _.uniq(admins.concat(uids));
} else if (flagData.type === 'post') { } else if (flagData.type === 'post') {
const cid = await posts.getCidByPid(flagData.targetId); const cid = await posts.getCidByPid(flagData.targetId);
if (!cid) { uids = _.uniq(admins.concat(globalMods));
return []; 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); const userData = await user.getUsersData(uids);
return userData.filter(u => u && u.userslug); return userData.filter(u => u && u.userslug);

@ -26,7 +26,7 @@ module.exports = function (module) {
async function getSortedSetUnion(params) { async function getSortedSetUnion(params) {
if (!Array.isArray(params.sets) || !params.sets.length) { if (!Array.isArray(params.sets) || !params.sets.length) {
return; return [];
} }
let limit = params.stop - params.start + 1; let limit = params.stop - params.start + 1;
if (limit <= 0) { if (limit <= 0) {

@ -32,6 +32,9 @@ SELECT COUNT(DISTINCT z."value") c
async function getSortedSetUnion(params) { async function getSortedSetUnion(params) {
const { sets } = params; const { sets } = params;
if (!sets || !sets.length) {
return [];
}
const start = params.hasOwnProperty('start') ? params.start : 0; const start = params.hasOwnProperty('start') ? params.start : 0;
const stop = params.hasOwnProperty('stop') ? params.stop : -1; const stop = params.hasOwnProperty('stop') ? params.stop : -1;
let weights = params.weights || []; let weights = params.weights || [];

@ -9,11 +9,12 @@ const topics = require('../topics');
const user = require('../user'); const user = require('../user');
const categories = require('../categories'); const categories = require('../categories');
const meta = require('../meta'); const meta = require('../meta');
const helpers = require('../controllers/helpers'); const controllerHelpers = require('../controllers/helpers');
const privileges = require('../privileges'); const privileges = require('../privileges');
const db = require('../database'); const db = require('../database');
const utils = require('../utils'); const utils = require('../utils');
const controllers404 = require('../controllers/404'); const controllers404 = require('../controllers/404');
const routeHelpers = require('./helpers');
const terms = { const terms = {
daily: 'day', daily: 'day',
@ -23,18 +24,18 @@ const terms = {
}; };
module.exports = function (app, middleware) { module.exports = function (app, middleware) {
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic); app.get('/topic/:topic_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopic));
app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory); app.get('/category/:category_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategory));
app.get('/topics.rss', middleware.maintenanceMode, generateForTopics); app.get('/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopics));
app.get('/recent.rss', middleware.maintenanceMode, generateForRecent); app.get('/recent.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecent));
app.get('/top.rss', middleware.maintenanceMode, generateForTop); app.get('/top.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop));
app.get('/top/:term.rss', middleware.maintenanceMode, generateForTop); app.get('/top/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop));
app.get('/popular.rss', middleware.maintenanceMode, generateForPopular); app.get('/popular.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular));
app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular); app.get('/popular/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular));
app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts); app.get('/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecentPosts));
app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, generateForCategoryRecentPosts); app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategoryRecentPosts));
app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, generateForUserTopics); app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForUserTopics));
app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag); app.get('/tags/:tag.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTag));
}; };
async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) { async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) {
@ -46,16 +47,16 @@ async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) {
} }
if (uid <= 0 || !token) { if (uid <= 0 || !token) {
return helpers.notAllowed(req, res); return controllerHelpers.notAllowed(req, res);
} }
const userToken = await db.getObjectField(`user:${uid}`, 'rss_token'); const userToken = await db.getObjectField(`user:${uid}`, 'rss_token');
if (userToken !== token) { if (userToken !== token) {
await user.auth.logAttempt(uid, req.ip); 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); const userPrivileges = await privileges.categories.get(cid, uid);
if (!userPrivileges.read) { if (!userPrivileges.read) {
return helpers.notAllowed(req, res); return controllerHelpers.notAllowed(req, res);
} }
return true; return true;
} }
@ -127,7 +128,7 @@ async function generateForCategory(req, res, next) {
db.getSortedSetRevIntersect({ db.getSortedSetRevIntersect({
sets: ['topics:tid', `cid:${cid}:tids:lastposttime`], sets: ['topics:tid', `cid:${cid}:tids:lastposttime`],
start: 0, start: 0,
stop: 25, stop: 24,
weights: [1, 0], weights: [1, 0],
}), }),
]); ]);
@ -230,7 +231,7 @@ async function generateSorted(options, req, res, next) {
const { cid } = req.query; const { cid } = req.query;
if (cid) { if (cid) {
if (!await privileges.categories.can('topics:read', cid, uid)) { if (!await privileges.categories.can('topics:read', cid, uid)) {
return helpers.notAllowed(req, res); return controllerHelpers.notAllowed(req, res);
} }
params.cids = [cid]; params.cids = [cid];
} }

@ -120,7 +120,12 @@ async function onMessage(socket, payload) {
return winston.warn('[socket.io] Empty method name'); 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 namespace = parts[0];
const methodToCall = parts.reduce((prev, cur) => { const methodToCall = parts.reduce((prev, cur) => {
if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) { if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {

@ -14,15 +14,15 @@ const utils = require('../../utils');
module.exports = function (SocketPosts) { module.exports = function (SocketPosts) {
SocketPosts.loadPostTools = async function (socket, data) { SocketPosts.loadPostTools = async function (socket, data) {
if (!data || !data.pid || !data.cid) { if (!data || !data.pid) {
throw new Error('[[error:invalid-data]]'); throw new Error('[[error:invalid-data]]');
} }
const cid = await posts.getCidByPid(data.pid);
const results = await utils.promiseParallel({ const results = await utils.promiseParallel({
posts: posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid', 'ip', 'flagId']), posts: posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid', 'ip', 'flagId']),
isAdmin: user.isAdministrator(socket.uid), isAdmin: user.isAdministrator(socket.uid),
isGlobalMod: user.isGlobalModerator(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), canEdit: privileges.posts.canEdit(data.pid, socket.uid),
canDelete: privileges.posts.canDelete(data.pid, socket.uid), canDelete: privileges.posts.canDelete(data.pid, socket.uid),
canPurge: privileges.posts.canPurge(data.pid, socket.uid), canPurge: privileges.posts.canPurge(data.pid, socket.uid),

@ -74,6 +74,6 @@ module.exports = function (SocketUser) {
await user.isAdminOrSelf(socket.uid, data.uid); await user.isAdminOrSelf(socket.uid, data.uid);
api.users.generateExport(socket, { type, ...data }); api.users.generateExport(socket, { type, uid: data.uid });
} }
}; };

@ -82,9 +82,8 @@ module.exports = function (Topics) {
data.title = String(data.title).trim(); data.title = String(data.title).trim();
data.tags = data.tags || []; data.tags = data.tags || [];
if (data.content) { data.content = String(data.content || '').trimEnd();
data.content = utils.rtrim(data.content);
}
Topics.checkTitle(data.title); Topics.checkTitle(data.title);
await Topics.validateTags(data.tags, data.cid, uid); await Topics.validateTags(data.tags, data.cid, uid);
data.tags = await Topics.filterTags(data.tags, data.cid); data.tags = await Topics.filterTags(data.tags, data.cid);
@ -167,9 +166,8 @@ module.exports = function (Topics) {
data.cid = topicData.cid; data.cid = topicData.cid;
await guestHandleValid(data); await guestHandleValid(data);
if (data.content) { data.content = String(data.content || '').trimEnd();
data.content = utils.rtrim(data.content);
}
if (!data.fromQueue) { if (!data.fromQueue) {
await user.isReadyToPost(uid, data.cid); await user.isReadyToPost(uid, data.cid);
Topics.checkContent(data.content); Topics.checkContent(data.content);

@ -60,6 +60,7 @@ Scheduled.pin = async function (tid, topicData) {
}; };
Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) { Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
const mainPid = await topics.getTopicField(tid, 'mainPid');
await Promise.all([ await Promise.all([
db.sortedSetsAdd([ db.sortedSetsAdd([
'topics:scheduled', 'topics:scheduled',
@ -67,6 +68,7 @@ Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
'topics:tid', 'topics:tid',
`cid:${cid}:uid:${uid}:tids`, `cid:${cid}:uid:${uid}:tids`,
], timestamp, tid), ], timestamp, tid),
posts.setPostField(mainPid, 'timestamp', timestamp),
shiftPostTimes(tid, timestamp), shiftPostTimes(tid, timestamp),
]); ]);
return topics.updateLastPostTimeFromLastPid(tid); return topics.updateLastPostTimeFromLastPid(tid);

@ -174,7 +174,7 @@ module.exports = function (Topics) {
} }
tids = await privileges.topics.filterTids('topics:read', tids, uid); 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); const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean);
async function getIgnoredCids() { async function getIgnoredCids() {
@ -192,11 +192,13 @@ module.exports = function (Topics) {
topicData = filtered; topicData = filtered;
const cids = params.cids && params.cids.map(String); const cids = params.cids && params.cids.map(String);
const { tags } = params;
tids = topicData.filter(t => ( tids = topicData.filter(t => (
t && t &&
t.cid && t.cid &&
!isCidIgnored[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); )).map(t => t.tid);
const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { tids: tids, params: params }); const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { tids: tids, params: params });

@ -287,7 +287,7 @@ module.exports = function (Topics) {
} }
Topics.getTagData = async function (tags) { Topics.getTagData = async function (tags) {
if (!tags.length) { if (!tags || !tags.length) {
return []; return [];
} }
tags.forEach((tag) => { tags.forEach((tag) => {

@ -52,6 +52,7 @@ Thumbs.get = async function (tids) {
const name = path.basename(thumb); const name = path.basename(thumb);
return hasTimestampPrefix.test(name) ? name.slice(14) : name; return hasTimestampPrefix.test(name) ? name.slice(14) : name;
})(), })(),
path: thumb,
url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, 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)))), Promise.all(toRemove.map(async relativePath => posts.uploads.dissociate(mainPid, relativePath.slice(1)))),
]); ]);
} }
if (toRemove.length) {
cache.del(set);
}
}; };
Thumbs.deleteAll = async (id) => { Thumbs.deleteAll = async (id) => {

@ -70,7 +70,9 @@ module.exports = function (User) {
let line = ''; let line = '';
usersData.forEach((user, index) => { 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) { if (showIps) {
userIPs = ips[index] ? ips[index].join(',') : ''; userIPs = ips[index] ? ips[index].join(',') : '';
line += `,"${userIPs}"\n`; line += `,"${userIPs}"\n`;

@ -134,13 +134,13 @@ UserEmail.sendValidationEmail = async function (uid, options) {
await UserEmail.expireValidation(uid); await UserEmail.expireValidation(uid);
await db.set(`confirm:byUid:${uid}`, confirm_code); 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}`, { await db.setObject(`confirm:${confirm_code}`, {
email: options.email.toLowerCase(), email: options.email.toLowerCase(),
uid: uid, 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}`); winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`);
events.log({ events.log({

@ -150,7 +150,7 @@
[[admin/settings/email:smtp-transport.gmail-warning2]] [[admin/settings/email:smtp-transport.gmail-warning2]]
</p> </p>
</div> </div>
<div class="form-group well" id="email:smtpTransport:custom-service" style="display: none"> <div class="form-group well" id="email:smtpTransport:custom-service">
<h5>Custom Service</h5> <h5>Custom Service</h5>
<label for="email:smtpTransport:host">[[admin/settings/email:smtp-transport.host]]</label> <label for="email:smtpTransport:host">[[admin/settings/email:smtp-transport.host]]</label>

@ -3,7 +3,7 @@
<div class="alert alert-info">[[modules:thumbs.modal.no-thumbs]]</div> <div class="alert alert-info">[[modules:thumbs.modal.no-thumbs]]</div>
{{{ end }}} {{{ end }}}
{{{ each thumbs }}} {{{ each thumbs }}}
<div class="media" data-id="{./id}" data-path="{./url}"> <div class="media" data-id="{./id}" data-path="{./path}">
<div class="media-left"> <div class="media-left">
<img class="media-object" src="{./url}" alt="" /> <img class="media-object" src="{./url}" alt="" />
</div> </div>

@ -12,13 +12,22 @@ const db = require('./mocks/databasemock');
const user = require('../src/user'); const user = require('../src/user');
const utils = require('../src/utils'); const utils = require('../src/utils');
const meta = require('../src/meta'); const meta = require('../src/meta');
const plugins = require('../src/plugins');
const privileges = require('../src/privileges'); const privileges = require('../src/privileges');
const helpers = require('./helpers'); const helpers = require('./helpers');
describe('authentication', () => { describe('authentication', () => {
const jar = request.jar(); const jar = request.jar();
let regularUid; let regularUid;
const dummyEmailerHook = async (data) => {};
before((done) => { before((done) => {
// Attach an emailer hook so related requests do not error
plugins.hooks.register('authentication-test', {
hook: 'filter:email.send',
method: dummyEmailerHook,
});
user.create({ username: 'regular', password: 'regularpwd', email: 'regular@nodebb.org' }, (err, uid) => { user.create({ username: 'regular', password: 'regularpwd', email: 'regular@nodebb.org' }, (err, uid) => {
assert.ifError(err); assert.ifError(err);
regularUid = uid; regularUid = uid;
@ -27,6 +36,10 @@ describe('authentication', () => {
}); });
}); });
after(() => {
plugins.hooks.unregister('authentication-test', 'filter:email.send');
});
it('should allow login with email for uid 1', async () => { it('should allow login with email for uid 1', async () => {
const oldValue = meta.config.allowLoginWith; const oldValue = meta.config.allowLoginWith;
meta.config.allowLoginWith = 'username-email'; meta.config.allowLoginWith = 'username-email';

@ -996,6 +996,11 @@ describe('Sorted Set methods', () => {
done(); done();
}); });
}); });
it('should return empty array if sets is empty', async () => {
const result = await db.getSortedSetRevUnion({ sets: [], start: 0, stop: -1 });
assert.deepStrictEqual(result, []);
});
}); });
describe('sortedSetIncrBy()', () => { describe('sortedSetIncrBy()', () => {

@ -107,6 +107,15 @@ 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}]]`);
done();
});
});
it('should get installed themes', (done) => { it('should get installed themes', (done) => {
const themes = ['nodebb-theme-lavender', 'nodebb-theme-persona', 'nodebb-theme-vanilla']; const themes = ['nodebb-theme-lavender', 'nodebb-theme-persona', 'nodebb-theme-vanilla'];
io.emit('admin.themes.getInstalled', (err, data) => { io.emit('admin.themes.getInstalled', (err, data) => {

@ -82,6 +82,7 @@ describe('Topic thumbs', () => {
assert.deepStrictEqual(thumbs, [{ assert.deepStrictEqual(thumbs, [{
id: topicObj.topicData.tid, id: topicObj.topicData.tid,
name: 'test.png', name: 'test.png',
path: `${relativeThumbPaths[0]}`,
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`,
}]); }]);
}); });
@ -92,6 +93,7 @@ describe('Topic thumbs', () => {
[{ [{
id: topicObj.topicData.tid, id: topicObj.topicData.tid,
name: 'test.png', name: 'test.png',
path: `${relativeThumbPaths[0]}`,
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`,
}], }],
[], [],
@ -200,16 +202,19 @@ describe('Topic thumbs', () => {
{ {
id: tid, id: tid,
name: 'test.png', name: 'test.png',
path: relativeThumbPaths[0],
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`,
}, },
{ {
id: tid, id: tid,
name: 'example.org', name: 'example.org',
path: 'https://example.org',
url: 'https://example.org', url: 'https://example.org',
}, },
{ {
id: tid, id: tid,
name: 'test2.png', name: 'test2.png',
path: relativeThumbPaths[1],
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[1]}`, url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[1]}`,
}, },
]); ]);

Loading…
Cancel
Save