diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index fa4834da55d0..60ff8dde38a0 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -790,6 +790,42 @@ paths: response: type: object properties: {} + /posts/{pid}: + put: + tags: + - posts + summary: Edit a post + description: This operation edits a post + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + content: + type: string + description: New post content + title: + type: string + description: Topic title, only accepted for main posts + required: + - content + example: + content: 'New post content' + title: 'New title' + responses: + '200': + description: Post successfully edited + content: + application/json: + schema: + type: object + properties: + status: + $ref: '#/components/schemas/Status' + response: + $ref: components/schemas/PostsObject.yaml#/PostsObject components: schemas: Status: diff --git a/src/controllers/write/index.js b/src/controllers/write/index.js index 693c49d2327a..b3dec622389e 100644 --- a/src/controllers/write/index.js +++ b/src/controllers/write/index.js @@ -6,3 +6,4 @@ Write.users = require('./users'); Write.groups = require('./groups'); Write.categories = require('./categories'); Write.topics = require('./topics'); +Write.posts = require('./posts'); diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js new file mode 100644 index 000000000000..a1481784d707 --- /dev/null +++ b/src/controllers/write/posts.js @@ -0,0 +1,81 @@ +'use strict'; + +const validator = require('validator'); +const _ = require('lodash'); + +const meta = require('../../meta'); +const groups = require('../../groups'); +const posts = require('../../posts'); +const events = require('../../events'); +const utils = require('../../utils'); + +const helpers = require('../helpers'); +const sockets = require('../../socket.io'); + +const Posts = module.exports; + +Posts.edit = async (req, res) => { + if (meta.config.minimumPostLength !== 0 && !req.body.content) { + throw new Error('[[error:invalid-data]]'); + } + + // Trim and remove HTML (latter for composers that send in HTML, like redactor) + var contentLen = utils.stripHTMLTags(req.body.content).trim().length; + + if (req.body.title && req.body.title.length < meta.config.minimumTitleLength) { + throw new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]'); + } else if (req.body.title && req.body.title.length > meta.config.maximumTitleLength) { + throw new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]'); + } else if (meta.config.minimumPostLength !== 0 && contentLen < meta.config.minimumPostLength) { + throw new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]'); + } else if (contentLen > meta.config.maximumPostLength) { + throw new Error('[[error:content-too-long, ' + meta.config.maximumPostLength + ']]'); + } + + // Payload construction + var payload = { + req, + uid: req.user.uid, + pid: req.params.pid, + content: req.body.content, + options: {}, + }; + ['handle', 'title'].forEach((prop) => { + if (req.body.hasOwnProperty(prop)) { + payload[prop] = req.body[prop]; + } + }); + ['topic_thumb', 'tags'].forEach((prop) => { + if (req.body.hasOwnProperty(prop)) { + payload.options[prop] = req.body[prop]; + } + }); + + const editResult = await posts.edit(payload); + helpers.formatApiResponse(200, res, await posts.getPostSummaryByPids([editResult.pid], req.user.uid, {})); + + if (editResult.topic.renamed) { + await events.log({ + type: 'topic-rename', + uid: req.user.uid, + ip: req.ip, + tid: editResult.topic.tid, + oldTitle: validator.escape(String(editResult.topic.oldTitle)), + newTitle: validator.escape(String(editResult.topic.title)), + }); + } + + if (!editResult.post.deleted) { + sockets.in('topic_' + editResult.topic.tid).emit('event:post_edited', editResult); + } + + const memberData = await groups.getMembersOfGroups([ + 'administrators', + 'Global Moderators', + 'cid:' + editResult.topic.cid + ':privileges:moderate', + 'cid:' + editResult.topic.cid + ':privileges:groups:moderate', + ]); + + const uids = _.uniq(_.flatten(memberData).concat(req.user.uid.toString())); + uids.forEach(uid => sockets.in('uid_' + uid).emit('event:post_edited', editResult)); +}; diff --git a/src/routes/write/index.js b/src/routes/write/index.js index 46cc85ca40a9..00be701dd182 100644 --- a/src/routes/write/index.js +++ b/src/routes/write/index.js @@ -24,7 +24,7 @@ Write.reload = (params) => { router.use('/api/v1/groups', require('./groups')()); router.use('/api/v1/categories', require('./categories')()); router.use('/api/v1/topics', require('./topics')()); - // router.use('/api/v1/posts', require('./posts')()); + router.use('/api/v1/posts', require('./posts')()); // router.use('/api/v1/util', require('./util')()); router.get('/api/v1/ping', function (req, res) { diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js new file mode 100644 index 000000000000..2cbe1a688ce2 --- /dev/null +++ b/src/routes/write/posts.js @@ -0,0 +1,94 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const setupApiRoute = routeHelpers.setupApiRoute; + +module.exports = function () { + const middlewares = [middleware.authenticate]; + + setupApiRoute(router, '/:pid', middleware, [...middlewares, middleware.checkRequired.bind(null, ['content'])], 'put', controllers.write.posts.edit); + + // app.route('/:pid') + // .put(apiMiddleware.requireUser, function(req, res) { + // if (!utils.checkRequired(['content'], req, res)) { + // return false; + // } + + // var payload = { + // uid: req.user.uid, + // pid: req.params.pid, + // content: req.body.content, + // options: {} + // }; + + // if (req.body.handle) { payload.handle = req.body.handle; } + // if (req.body.title) { payload.title = req.body.title; } + // if (req.body.topic_thumb) { payload.options.topic_thumb = req.body.topic_thumb; } + // if (req.body.tags) { payload.options.tags = req.body.tags; } + + // posts.edit(payload, function(err) { + // errorHandler.handle(err, res); + // }) + // }) + // .delete(apiMiddleware.requireUser, apiMiddleware.validatePid, function(req, res) { + // posts.purge(req.params.pid, req.user.uid, function(err) { + // errorHandler.handle(err, res); + // }); + // }); + + // app.route('/:pid/state') + // .put(apiMiddleware.requireUser, apiMiddleware.validatePid, function (req, res) { + // posts.restore(req.params.pid, req.user.uid, function (err) { + // errorHandler.handle(err, res); + // }); + // }) + // .delete(apiMiddleware.requireUser, apiMiddleware.validatePid, function (req, res) { + // posts.delete(req.params.pid, req.user.uid, function (err) { + // errorHandler.handle(err, res); + // }); + // }); + + // app.route('/:pid/vote') + // .post(apiMiddleware.requireUser, function(req, res) { + // if (!utils.checkRequired(['delta'], req, res)) { + // return false; + // } + + // if (req.body.delta > 0) { + // posts.upvote(req.params.pid, req.user.uid, function(err, data) { + // errorHandler.handle(err, res, data); + // }) + // } else if (req.body.delta < 0) { + // posts.downvote(req.params.pid, req.user.uid, function(err, data) { + // errorHandler.handle(err, res, data); + // }) + // } else { + // posts.unvote(req.params.pid, req.user.uid, function(err, data) { + // errorHandler.handle(err, res, data); + // }) + // } + // }) + // .delete(apiMiddleware.requireUser, function(req, res) { + // posts.unvote(req.params.pid, req.user.uid, function(err, data) { + // errorHandler.handle(err, res, data); + // }) + // }); + + // app.route('/:pid/bookmark') + // .post(apiMiddleware.requireUser, function(req, res) { + // posts.bookmark(req.params.pid, req.user.uid, function (err) { + // errorHandler.handle(err, res); + // }); + // }) + // .delete(apiMiddleware.requireUser, apiMiddleware.validatePid, function (req, res) { + // posts.unbookmark(req.params.pid, req.user.uid, function (err) { + // errorHandler.handle(err, res); + // }); + // }); + + return router; +}; diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js index c4dfa1365ab0..1e71aaff356d 100644 --- a/src/socket.io/posts/edit.js +++ b/src/socket.io/posts/edit.js @@ -12,6 +12,8 @@ const websockets = require('../index'); module.exports = function (SocketPosts) { SocketPosts.edit = async function (socket, data) { + websockets.warnDeprecated(socket, 'PUT /api/v1/posts/:pid'); + if (!socket.uid) { throw new Error('[[error:not-logged-in]]'); } else if (!data || !data.pid || (meta.config.minimumPostLength !== 0 && !data.content)) {