Merge commit '64ac483dddb65ad6cba06a2e2a43a07a361546f1' into v1.15.x

This commit is contained in:
Misty (Bot)
2020-12-03 19:27:45 +00:00
9 changed files with 259 additions and 30 deletions

View File

@@ -1,3 +1,126 @@
#### v1.15.4 (2020-12-02)
##### Chores
* up persona (dde3171c)
* fallbacks for nodebb.error (82ca3760)
* fallbacks for nodebb.topic (5b269bc5)
* remove test code (07fe959c)
* incrementing version number - v1.15.3 (d1ae08fa)
* update changelog for v1.15.3 (cf157c9b)
##### New Features
* #9005, use timestamp in profile/cover images (5f0f476b)
* #8983, update pin tooltip in topic (954dc5b7)
* option to allow auto-joining of groups (optionally skip the "request membership" step) (685f3c6a)
* user notification settings for group.leave event (c1a7968d)
* add defaults for composer help (0cba2691)
* #8900, postQueue setting for category (1eb5fabd)
* #8960, update view count after merge (14bb0a44)
* use correct code (557f0f56)
* #8989, convert widget nav to dropdown (4c650aee)
* add handler for 501 api response (007a3258)
* add translation key for pin icon label with expiry (12b3aa0d)
* add pinExpiry and pinExpiryISO to topic data (ad8e7700)
* add cancel button to pin expiration modal (e1432caf)
##### Bug Fixes
* #9032, fix login redirect for sso plugins (6f68f4d2)
* #8962, dont show null for purged targets (86b7f8a5)
* selector on hooks page (3488daa1)
* notification on group.leave incorrectly showing "Guest has left X group" (f7558c60)
* #9019, add missing lang strings (b46d2f93)
* #9018 (e45b5cba)
* #9015, add default value for dailyDigestFreq (0f1fc10f)
* spec (cfb7b113)
* #8997, don't send notifications if uids already in group (f7c738de)
* #9002 ban templates not user friendly (4317cdea)
* #9010, show rest of info even if clusterMonitor priv is not granted (202dcef4)
* #9007 revoke old sessions after adding (d46740f8)
* guests dont always have sid (70073653)
* allow guests to see their replies immediately (a4fe4d3c)
* privs headers (92d1b8a6)
* pwd reset test (f25000cb)
* #8991, logout on password reset, dont verify email if password expired (5080f357)
* don't show topic search if no search privilege (8adbf54a)
* #8998, allow guests to use write api to post/reply (3cd0c9a4)
* guest handles to user displayname as well (5a137a0d)
* timestamp in queue, add post queue strings (546f58bf)
* #8992, set email:confirmed for first admin user (7f5efc3e)
* typo in upgrade script, closes #8990 (80f0750b)
* #8984, post-queue ux (1269103f)
* order (9ab4fb41)
* #8982, copy color on tag rename, dont copy if target exists (d3c04afb)
* tests (b596e948)
* api test (77a6dbac)
* remove dupe (cbbda451)
* csv test (3de692cd)
* spec, remove old tests (4afdf8bc)
* #8969, export csv to file (6e6a7a8f)
* spec for /tag (88e5cda5)
* #8980, fix lang string (f4d217d8)
* #8979 (bf171adc)
* #8971, disallow flags of privileged users (mods, gmods, admins) (1e7cf1cb)
* #8974, with password login for approval queue (dadb2527)
* #8974, dont show wrong message on register queue (fdca8b16)
* #8973, fix timestamp on ban modal (5c3deb4b)
* #8968, don't show topic search if search is not enabled (c8554b78)
* flicker on tooltips if server call takes long time (4c7374ea)
* missing select/clear all checkbox added to category privileges template (#8967) (a56a6577)
* use package.name for theme.id (#8965) (ba3981e2)
* winston usages (b8cafefc)
* #8957 (414caac0)
* dont go back after delete account actions (7e6427bc)
* error message (47a19d67)
* #8954, clear purged replies and toPids (#8959) (5bb5ec46)
* #8955, popstate to purged topic should go to homepage (39dae0aa)
* 'already-deleting' error on subsequent account content deletions (21d6225c)
* #8949, faster upgrade script (93863bb3)
* **deps:**
* update dependency nodebb-theme-vanilla to v11.3.8 (#9031) (18707940)
* update dependency nodebb-theme-slick to v1.3.5 (#9030) (4085f3e6)
* update dependency nodebb-theme-persona to v10.2.98 (#9029) (f7d60c43)
* update dependency nodebb-theme-persona to v10.2.97 (42b23a3b)
* update dependency nodebb-plugin-composer-default to v6.4.10 (#9025) (43bbfb67)
* update dependency nodebb-theme-vanilla to v11.3.7 (#9024) (3f597a55)
* update dependency nodebb-plugin-composer-default to v6.4.9 (#9023) (110186b9)
* update dependency nodebb-theme-slick to v1.3.4 (#9022) (8dc1437e)
* update dependency nodebb-theme-persona to v10.2.96 (#9021) (2c9cd286)
* update dependency nodebb-plugin-composer-default to v6.4.8 (#9017) (1f5f2e1d)
* update dependency nodebb-plugin-markdown to v8.12.3 (9004319e)
* update dependency validator to v13.5.1 (7b39cf4b)
* update dependency nodebb-theme-persona to v10.2.95 (#9001) (4ddab380)
* update dependency nodebb-theme-persona to v10.2.94 (#9000) (877d8554)
* update dependency nodebb-theme-persona to v10.2.93 (#8999) (c44d9d2f)
* update dependency nodebb-theme-persona to v10.2.92 (#8995) (346b91eb)
* update dependency nodebb-theme-vanilla to v11.3.6 (#8987) (6c980db1)
* update dependency nodebb-theme-persona to v10.2.91 (#8986) (8258536a)
* update dependency autoprefixer to v10.0.4 (#8985) (fad2d342)
* update dependency nodebb-plugin-markdown to v8.12.2 (f5714452)
* update dependency nodebb-theme-persona to v10.2.90 (5664807d)
* update dependency nodebb-theme-vanilla to v11.3.5 (19fe2493)
* update dependency nodebb-theme-persona to v10.2.89 (ad60bc06)
* update dependency autoprefixer to v10.0.3 (b2f0d38f)
* update dependency benchpressjs to v2.3.0 (6c316be4)
* **openapi:**
* spec for c1a7968d23f0809e7012edfccf49b193749998ec (69864b87)
* spec for 685f3c6aa6173383d6c31b87ed51cf8ed0ca44ce (1bb75e76)
* **acp:**
* #9008 undefined link for "no users browsing" state on dashboard (54dc449f)
* #9009 no-users-browsing untranslated on dashboard (286243cd)
* **spec:**
* from 6e6a7a8f8a9a75500ba1f336cabc882234212f88 (acb57666)
* breaking tests (88a60473)
* broken test due to canFlag addition (1b1205a9)
##### Refactors
* remove old hack (73746bb4)
* add TopicObjectSlim common schema (22715d54)
* pin/lock threadTools to use topicCommand, rewrote topicCommand to match categoryCommand signature (15c6f32c)
#### v1.15.3 (2020-11-26)
##### Chores

View File

@@ -100,10 +100,10 @@
"nodebb-plugin-markdown": "8.12.3",
"nodebb-plugin-mentions": "2.13.5",
"nodebb-plugin-soundpack-default": "1.0.0",
"nodebb-plugin-spam-be-gone": "0.7.6",
"nodebb-plugin-spam-be-gone": "0.7.7",
"nodebb-rewards-essentials": "0.1.4",
"nodebb-theme-lavender": "5.0.14",
"nodebb-theme-persona": "10.2.98",
"nodebb-theme-persona": "10.2.99",
"nodebb-theme-slick": "1.3.5",
"nodebb-theme-vanilla": "11.3.8",
"nodebb-widget-essentials": "4.1.2",

View File

@@ -145,12 +145,18 @@ ajaxify = window.ajaxify || {};
app.alertError('[[global:please_log_in]]');
app.previousUrl = url;
window.location.href = config.relative_path + '/login';
} else if ((status === 302 || status === 308) && typeof data.responseJSON === 'string') {
ajaxifyTimer = undefined;
if (data.responseJSON.startsWith('http://') || data.responseJSON.startsWith('https://')) {
window.location.href = data.responseJSON;
} else {
ajaxify.go(data.responseJSON.slice(1), callback, quiet);
} else if (status === 302 || status === 308) {
if (data.responseJSON && data.responseJSON.external) {
// this is used by sso plugins to redirect to the auth route
// cant use ajaxify.go for /auth/sso routes
window.location.href = data.responseJSON.external;
} else if (typeof data.responseJSON === 'string') {
ajaxifyTimer = undefined;
if (data.responseJSON.startsWith('http://') || data.responseJSON.startsWith('https://')) {
window.location.href = data.responseJSON;
} else {
ajaxify.go(data.responseJSON.slice(1), callback, quiet);
}
}
}
} else if (textStatus !== 'abort') {
@@ -194,8 +200,9 @@ ajaxify = window.ajaxify || {};
// Allow translation strings in title on ajaxify (#5927)
title = translator.unescape(title);
translator.translate(title, function (translated) {
var data = { title: title };
$(window).trigger('action:ajaxify.updateTitle', data);
translator.translate(data.title, function (translated) {
window.document.title = $('<div></div>').html(translated).text();
});
});

View File

@@ -106,14 +106,14 @@ define('pictureCropper', ['cropper'], function (Cropper) {
return;
}
cropperModal.find('#upload-progress-bar').css('width', '100%');
cropperModal.find('#upload-progress-bar').css('width', '0%');
cropperModal.find('#upload-progress-box').show().removeClass('hide');
var socketData = {};
socketData[data.paramName] = data.paramValue;
socketData.imageData = imageData;
socket.emit(data.socketMethod, socketData, function (err, imageData) {
socketUpload({
data: data,
imageData: imageData,
progressBarEl: cropperModal.find('#upload-progress-bar'),
}, function (err, result) {
if (err) {
cropperModal.find('#upload-progress-box').hide();
cropperModal.find('.upload-btn').removeClass('disabled');
@@ -121,7 +121,7 @@ define('pictureCropper', ['cropper'], function (Cropper) {
return app.alertError(err.message);
}
callback(imageData.url);
callback(result.url);
cropperModal.modal('hide');
});
});
@@ -143,6 +143,36 @@ define('pictureCropper', ['cropper'], function (Cropper) {
});
};
function socketUpload(params, callback) {
var socketData = {};
socketData[params.data.paramName] = params.data.paramValue;
socketData.method = params.data.socketMethod;
socketData.size = params.imageData.length;
socketData.progress = 0;
var chunkSize = 100000;
function doUpload() {
var chunk = params.imageData.slice(socketData.progress, socketData.progress + chunkSize);
socket.emit('uploads.upload', {
chunk: chunk,
params: socketData,
}, function (err, result) {
if (err) {
return app.alertError(err);
}
if (socketData.progress + chunkSize < socketData.size) {
socketData.progress += chunk.length;
params.progressBarEl.css('width', (socketData.progress / socketData.size * 100).toFixed(2) + '%');
return setTimeout(doUpload, 100);
}
params.progressBarEl.css('width', '100%');
callback(null, result);
});
}
doUpload();
}
function checkCORS(cropperTool, data) {
var imageData;
try {
@@ -177,7 +207,7 @@ define('pictureCropper', ['cropper'], function (Cropper) {
var file = fileInput[0].files[0];
var fileSize = data.hasOwnProperty('fileSize') && data.fileSize !== undefined ? parseInt(data.fileSize, 10) : false;
if (fileSize && file.size > fileSize * 1024) {
return app.alertError('[[error:file-too-big, ' + fileSize + ']]');
return showAlert('error', '[[error:file-too-big, ' + fileSize + ']]');
}
if (file.name.endsWith('.gif')) {

View File

@@ -145,11 +145,20 @@ helpers.notAllowed = async function (req, res, error) {
};
helpers.redirect = function (res, url, permanent) {
if (res.locals.isAPI) {
res.set('X-Redirect', encodeURI(url)).status(200).json(encodeURI(url));
let redirectUrl;
// this is used by sso plugins to redirect to the auth route
if (url.hasOwnProperty('external')) {
redirectUrl = url.external;
url.external = encodeURI(url.external);
} else {
const redirectUrl = url.startsWith('http://') || url.startsWith('https://') ?
url : relative_path + url;
redirectUrl = url;
url = encodeURI(url);
}
if (res.locals.isAPI) {
res.set('X-Redirect', encodeURI(redirectUrl)).status(200).json(url);
} else {
redirectUrl = redirectUrl.startsWith('http://') || redirectUrl.startsWith('https://') ?
redirectUrl : relative_path + redirectUrl;
res.redirect(permanent ? 308 : 307, encodeURI(redirectUrl));
}
};

View File

@@ -125,7 +125,7 @@ Controllers.login = async function (req, res) {
data.allowLocalLogin = hasLoginPrivilege || parseInt(req.query.local, 10) === 1;
if (!data.allowLocalLogin && !data.allowRegistration && data.alternate_logins && data.authentication.length === 1) {
return helpers.redirect(res, data.authentication[0].url);
return helpers.redirect(res, { external: data.authentication[0].url });
}
if (req.loggedIn) {

View File

@@ -5,12 +5,10 @@
* payload and throw an error otherwise.
*/
const fs = require('fs');
const fsPromises = fs.promises;
const path = require('path');
const nconf = require('nconf');
const file = require('../file');
const user = require('../user');
const groups = require('../groups');
const topics = require('../topics');
@@ -64,13 +62,12 @@ Assert.path = helpers.try(async (req, res, next) => {
const pathToFile = path.join(nconf.get('upload_path'), req.body.path);
res.locals.cleanedPath = pathToFile;
// Guard against path traversal
if (!pathToFile.startsWith(nconf.get('upload_path'))) {
return controllerHelpers.formatApiResponse(403, res, new Error('[[error:invalid-path]]'));
}
try {
await fsPromises.access(pathToFile, fs.constants.F_OK);
} catch (e) {
if (!await file.exists(pathToFile)) {
return controllerHelpers.formatApiResponse(404, res, new Error('[[error:invalid-path]]'));
}

View File

@@ -69,6 +69,15 @@ function onConnection(socket) {
socket.on('*', function (payload) {
onMessage(socket, payload);
});
socket.on('disconnect', function () {
onDisconnect(socket);
});
}
function onDisconnect(socket) {
require('./uploads').clear(socket.id);
plugins.hooks.fire('action:sockets.disconnect', { socket: socket });
}
function onConnect(socket) {
@@ -82,6 +91,7 @@ function onConnect(socket) {
socket.join('sess_' + socket.request.signedCookies[nconf.get('sessionKey')]);
Sockets.server.sockets.sockets[socket.id].emit('checkSession', socket.uid);
Sockets.server.sockets.sockets[socket.id].emit('setHostname', os.hostname());
plugins.hooks.fire('action:sockets.connect', { socket: socket });
}
async function onMessage(socket, payload) {
@@ -148,7 +158,8 @@ async function onMessage(socket, payload) {
function requireModules() {
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist', 'flags',
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist',
'flags', 'uploads',
];
modules.forEach(function (module) {

52
src/socket.io/uploads.js Normal file
View File

@@ -0,0 +1,52 @@
'use strict';
const socketUser = require('./user');
const socketGroup = require('./groups');
const image = require('../image');
const meta = require('../meta');
const inProgress = {};
const uploads = module.exports;
uploads.upload = async function (socket, data) {
const methodToFunc = {
'user.uploadCroppedPicture': socketUser.uploadCroppedPicture,
'user.updateCover': socketUser.updateCover,
'groups.cover.update': socketGroup.cover.update,
};
if (!socket.uid || !data || !data.chunk || !data.params || !data.params.method || !methodToFunc[data.params.method]) {
throw new Error('[[error:invalid-data]]');
}
inProgress[socket.id] = inProgress[socket.id] || {};
const socketUploads = inProgress[socket.id];
const method = data.params.method;
socketUploads[method] = socketUploads[method] || { imageData: '' };
socketUploads[method].imageData += data.chunk;
try {
const maxSize = data.params.method === 'user.uploadCroppedPicture' ?
meta.config.maximumProfileImageSize : meta.config.maximumCoverImageSize;
const size = image.sizeFromBase64(socketUploads[method].imageData);
if (size > maxSize * 1024) {
throw new Error('[[error:file-too-big, ' + maxSize + ']]');
}
if (socketUploads[method].imageData.length < data.params.size) {
return;
}
data.params.imageData = socketUploads[method].imageData;
const result = await methodToFunc[data.params.method](socket, data.params);
delete socketUploads[method];
return result;
} catch (err) {
delete inProgress[socket.id];
throw err;
}
};
uploads.clear = function (sid) {
delete inProgress[sid];
};