diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index 650c9eaa5f..77f8f130af 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -4,6 +4,7 @@ const validator = require('validator'); const api = require('../../api'); const topics = require('../../topics'); +const privileges = require('../../privileges'); const helpers = require('../helpers'); const uploadsController = require('../uploads'); @@ -89,6 +90,10 @@ Topics.deleteTags = async (req, res) => { helpers.formatApiResponse(200, res); }; +Topics.getThumbs = async (req, res) => { + helpers.formatApiResponse(200, res, await topics.thumbs.get(req.params.tid)); +}; + Topics.addThumb = async (req, res) => { // req.params.tid could be either a tid (pushing a new thumb to an existing topic) or a post UUID (a new topic being composed) const id = req.params.tid; @@ -98,23 +103,34 @@ Topics.addThumb = async (req, res) => { if (!isUUID && (isNaN(parseInt(id, 10)) || !await topics.exists(req.params.tid))) { return helpers.formatApiResponse(404, res, new Error('[[error:no-topic]]')); } + + // While drafts are not protected, tids are + if (!isUUID && !await privileges.topics.canEdit(req.params.tid, req.user.uid)) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + /** * todo test: * - uuid * - tid * - number but not tid * - random garbage + * - wrong caller uid (unpriv) */ const files = await uploadsController.uploadThumb(req, res); // response is handled here, fix this? // Add uploaded files to topic zset await Promise.all(files.map(async (fileObj) => { - await topics.thumbs.associate(id, fileObj.path, isUUID); + await topics.thumbs.associate(id, fileObj.path); })); }; Topics.deleteThumb = async (req, res) => { + if (!await privileges.topics.canEdit(req.params.tid, req.user.uid)) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + await topics.thumbs.delete(req.params.tid, req.body.path); helpers.formatApiResponse(200, res, await topics.thumbs.get(req.params.tid)); }; diff --git a/src/middleware/assert.js b/src/middleware/assert.js index ce45e5d1c2..80fd009e7a 100644 --- a/src/middleware/assert.js +++ b/src/middleware/assert.js @@ -58,6 +58,11 @@ Assert.path = helpers.try(async (req, res, next) => { req.body.path = new URL(req.body.path).pathname; } + // Strip upload_url if found + if (req.body.path.startsWith(nconf.get('upload_url'))) { + req.body.path = req.body.path.slice(nconf.get('upload_url').length); + } + const pathToFile = path.join(nconf.get('upload_path'), req.body.path); res.locals.cleanedPath = pathToFile; diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index c7feb4b318..f72f8bef24 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -34,6 +34,7 @@ module.exports = function () { setupApiRoute(router, 'put', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.addTags); setupApiRoute(router, 'delete', '/:tid/tags', [...middlewares, middleware.assert.topic], controllers.write.topics.deleteTags); + setupApiRoute(router, 'get', '/:tid/thumbs', [], controllers.write.topics.getThumbs); setupApiRoute(router, 'post', '/:tid/thumbs', [multipartMiddleware, middleware.validateFiles, ...middlewares], controllers.write.topics.addThumb); setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.assert.topic, middleware.assert.path], controllers.write.topics.deleteThumb); diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index ed81e04e89..af06b1ace8 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -1,12 +1,13 @@ 'use strict'; -var nconf = require('nconf'); -var path = require('path'); +const nconf = require('nconf'); +const path = require('path'); +const validator = require('validator'); const db = require('../database'); -var file = require('../file'); -var plugins = require('../plugins'); +const file = require('../file'); +const plugins = require('../plugins'); const Thumbs = {}; module.exports = Thumbs; @@ -25,7 +26,7 @@ Thumbs.get = async function (tids) { singular = true; } - const sets = tids.map(tid => `topic:${tid}:thumbs`); + const sets = tids.map(tid => `${validator.isUUID(String(tid)) ? 'draft' : 'topic'}:${tid}:thumbs`); const thumbs = await db.getSortedSetsMembers(sets); let response = thumbs.map(thumbSet => thumbSet.map(thumb => ({ url: path.join(nconf.get('upload_url'), thumb), @@ -35,8 +36,9 @@ Thumbs.get = async function (tids) { return singular ? response.pop() : response; }; -Thumbs.associate = async function (id, path, isDraft) { - // Associates a newly uploaded file as a thumb to the passed-in tid +Thumbs.associate = async function (id, path) { + // Associates a newly uploaded file as a thumb to the passed-in draft or topic + const isDraft = validator.isUUID(String(id)); const set = `${isDraft ? 'draft' : 'topic'}:${id}:thumbs`; const numThumbs = await db.sortedSetCard(set); path = path.replace(nconf.get('upload_path'), ''); @@ -47,7 +49,7 @@ Thumbs.commit = async function (uuid, tid) { // Converts the draft thumb zset to the topic zset (combines thumbs if applicable) const set = `draft:${uuid}:thumbs`; const thumbs = await db.getSortedSetRange(set, 0, -1); - await Promise.all(thumbs.map(async path => await Thumbs.associate(tid, path, false))); + await Promise.all(thumbs.map(async path => await Thumbs.associate(tid, path))); await db.delete(set); };