diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 1c691bc5ac..628f21836c 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -108,6 +108,10 @@ paths: $ref: 'write/posts/pid/vote.yaml' /posts/{pid}/bookmark: $ref: 'write/posts/pid/bookmark.yaml' + /posts/{pid}/diffs: + $ref: 'write/posts/pid/diffs.yaml' + /posts/{pid}/diffs/{since}: + $ref: 'write/posts/pid/diffs/since.yaml' /admin/settings/{setting}: $ref: 'write/admin/settings/setting.yaml' /files/: diff --git a/public/openapi/write/posts/pid/diffs.yaml b/public/openapi/write/posts/pid/diffs.yaml new file mode 100644 index 0000000000..40d4201185 --- /dev/null +++ b/public/openapi/write/posts/pid/diffs.yaml @@ -0,0 +1,41 @@ +get: + tags: + - posts + summary: get post edit history + description: This operation retrieves a post's edit history + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + responses: + '200': + description: Post history successfully retrieved. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + timestamps: + type: array + items: + type: number + revisions: + type: array + items: + type: object + properties: + timestamp: + type: number + username: + type: string + editable: + type: boolean \ No newline at end of file diff --git a/public/openapi/write/posts/pid/diffs/since.yaml b/public/openapi/write/posts/pid/diffs/since.yaml new file mode 100644 index 0000000000..8db8c6f4ac --- /dev/null +++ b/public/openapi/write/posts/pid/diffs/since.yaml @@ -0,0 +1,65 @@ +get: + tags: + - posts + summary: get single post edit history + description: This operation retrieves a post's edit history + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + - in: path + name: since + schema: + type: number + required: true + description: a valid UNIX timestamp + example: 0 + responses: + '200': + description: Post history successfully retrieved. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + $ref: ../../../../components/schemas/PostObject.yaml#/PostObject +put: + tags: + - posts + summary: revert a post + description: This operation reverts a post to an earlier version. The revert process will append a new history item to the post's edit history. + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + - in: path + name: since + schema: + type: number + required: true + description: a valid UNIX timestamp + example: 0 + responses: + '200': + description: Post successfully reverted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} \ No newline at end of file diff --git a/public/src/client/topic/diffs.js b/public/src/client/topic/diffs.js index 007182ae8e..3b310f6d85 100644 --- a/public/src/client/topic/diffs.js +++ b/public/src/client/topic/diffs.js @@ -1,6 +1,6 @@ 'use strict'; -define('forum/topic/diffs', ['forum/topic/images'], function () { +define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) { var Diffs = {}; Diffs.open = function (pid) { @@ -10,11 +10,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; - socket.emit('posts.getDiffs', { pid: pid }, function (err, data) { - if (err) { - return app.alertError(err.message); - } - + api.get(`/posts/${pid}/diffs`, {}).then((data) => { app.parseAndTranslate('partials/modals/post_history', { diffs: data.revisions.map(function (revision) { var timestamp = parseInt(revision.timestamp, 10); @@ -56,7 +52,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { revertEl.prop('disabled', true); }); }); - }); + }).catch(app.alertError); }; Diffs.load = function (pid, since, postContainer) { @@ -64,11 +60,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { return; } - socket.emit('posts.showPostAt', { pid: pid, since: since }, function (err, data) { - if (err) { - return app.alertError(err.message); - } - + api.get(`/posts/${pid}/diffs/${since}`, {}).then((data) => { data.deleted = !!parseInt(data.deleted, 10); app.parseAndTranslate('partials/posts_list', 'posts', { @@ -76,7 +68,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { }, function (html) { postContainer.empty().append(html); }); - }); + }).catch(app.alertError); }; Diffs.restore = function (pid, since, modal) { @@ -84,14 +76,10 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { return; } - socket.emit('posts.restoreDiff', { pid: pid, since: since }, function (err) { - if (err) { - return app.alertError(err); - } - + api.put(`/posts/${pid}/diffs/${since}`, {}).then(() => { modal.modal('hide'); app.alertSuccess('[[topic:diffs.post-restored]]'); - }); + }).catch(app.alertError); }; return Diffs; diff --git a/src/api/posts.js b/src/api/posts.js index c01b377340..c1582765e2 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -4,6 +4,7 @@ const validator = require('validator'); const _ = require('lodash'); const utils = require('../utils'); +const user = require('../user'); const posts = require('../posts'); const topics = require('../topics'); const groups = require('../groups'); @@ -213,3 +214,61 @@ postsAPI.bookmark = async function (caller, data) { postsAPI.unbookmark = async function (caller, data) { return await apiHelpers.postCommand(caller, 'unbookmark', 'bookmarked', '', data); }; + +async function diffsPrivilegeCheck(pid, uid) { + const [deleted, privilegesData] = await Promise.all([ + posts.getPostField(pid, 'deleted'), + privileges.posts.get([pid], uid), + ]); + + const allowed = privilegesData[0]['posts:history'] && (deleted ? privilegesData[0]['posts:view_deleted'] : true); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } +} + +postsAPI.getDiffs = async (caller, data) => { + await diffsPrivilegeCheck(data.pid, caller.uid); + const timestamps = await posts.diffs.list(data.pid); + const post = await posts.getPostFields(data.pid, ['timestamp', 'uid']); + + const diffs = await posts.diffs.get(data.pid); + const uids = diffs.map(diff => diff.uid || null); + uids.push(post.uid); + let usernames = await user.getUsersFields(uids, ['username']); + usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null)); + + let canEdit = true; + try { + await user.isPrivilegedOrSelf(caller.uid, post.uid); + } catch (e) { + canEdit = false; + } + + timestamps.push(post.timestamp); + + return { + timestamps: timestamps, + revisions: timestamps.map((timestamp, idx) => ({ + timestamp: timestamp, + username: usernames[idx], + })), + editable: canEdit, + }; +}; + +postsAPI.loadDiff = async (caller, data) => { + await diffsPrivilegeCheck(data.pid, caller.uid); + return await posts.diffs.load(data.pid, data.since, caller.uid); +}; + +postsAPI.restoreDiff = async (caller, data) => { + const cid = await posts.getCidByPid(data.pid); + const canEdit = await privileges.categories.can('edit', cid, caller.uid); + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + const edit = await posts.diffs.restore(data.pid, data.since, caller.uid, apiHelpers.buildReqObject(caller)); + websockets.in('topic_' + edit.topic.tid).emit('event:post_edited', edit); +}; diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 7988439a1f..197e699c31 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -73,3 +73,16 @@ Posts.unbookmark = async (req, res) => { await api.posts.unbookmark(req, data); helpers.formatApiResponse(200, res); }; + +Posts.getDiffs = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.getDiffs(req, { ...req.params })); +}; + +Posts.loadDiff = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.loadDiff(req, { ...req.params })); +}; + +Posts.restoreDiff = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.restoreDiff(req, { ...req.params })); +}; + diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index 837f8def8c..c7a68e94c2 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -24,5 +24,9 @@ module.exports = function () { setupApiRoute(router, 'put', '/:pid/bookmark', [...middlewares, middleware.assert.post], controllers.write.posts.bookmark); setupApiRoute(router, 'delete', '/:pid/bookmark', [...middlewares, middleware.assert.post], controllers.write.posts.unbookmark); + setupApiRoute(router, 'get', '/:pid/diffs', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.getDiffs); + setupApiRoute(router, 'get', '/:pid/diffs/:since', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.loadDiff); + setupApiRoute(router, 'put', '/:pid/diffs/:since', [...middlewares, middleware.assert.post], controllers.write.posts.restoreDiff); + return router; }; diff --git a/src/socket.io/posts/diffs.js b/src/socket.io/posts/diffs.js index fb74f6a9ef..a1e2043243 100644 --- a/src/socket.io/posts/diffs.js +++ b/src/socket.io/posts/diffs.js @@ -1,67 +1,21 @@ 'use strict'; -const posts = require('../../posts'); -const user = require('../../user'); -const privileges = require('../../privileges'); -const apiHelpers = require('../../api/helpers'); +const api = require('../../api'); const websockets = require('..'); module.exports = function (SocketPosts) { SocketPosts.getDiffs = async function (socket, data) { - await privilegeCheck(data.pid, socket.uid); - const timestamps = await posts.diffs.list(data.pid); - const post = await posts.getPostFields(data.pid, ['timestamp', 'uid']); - - const diffs = await posts.diffs.get(data.pid); - const uids = diffs.map(diff => diff.uid || null); - uids.push(post.uid); - let usernames = await user.getUsersFields(uids, ['username']); - usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null)); - - let canEdit = true; - try { - await user.isPrivilegedOrSelf(socket.uid, post.uid); - } catch (e) { - canEdit = false; - } - - timestamps.push(post.timestamp); - - return { - timestamps: timestamps, - revisions: timestamps.map((timestamp, idx) => ({ - timestamp: timestamp, - username: usernames[idx], - })), - editable: canEdit, - }; + websockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/diffs'); + return await api.posts.getDiffs(socket, data); }; SocketPosts.showPostAt = async function (socket, data) { - await privilegeCheck(data.pid, socket.uid); - return await posts.diffs.load(data.pid, data.since, socket.uid); + websockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/diffs/:since'); + return await api.posts.loadDiff(socket, data); }; - async function privilegeCheck(pid, uid) { - const [deleted, privilegesData] = await Promise.all([ - posts.getPostField(pid, 'deleted'), - privileges.posts.get([pid], uid), - ]); - - const allowed = privilegesData[0]['posts:history'] && (deleted ? privilegesData[0]['posts:view_deleted'] : true); - if (!allowed) { - throw new Error('[[error:no-privileges]]'); - } - } - SocketPosts.restoreDiff = async function (socket, data) { - const cid = await posts.getCidByPid(data.pid); - const canEdit = await privileges.categories.can('edit', cid, socket.uid); - if (!canEdit) { - throw new Error('[[error:no-privileges]]'); - } - - const edit = await posts.diffs.restore(data.pid, data.since, socket.uid, apiHelpers.buildReqObject(socket)); - websockets.in('topic_' + edit.topic.tid).emit('event:post_edited', edit); + websockets.warnDeprecated(socket, 'PUT /api/v3/posts/:pid/diffs/:since'); + return await api.posts.restoreDiff(socket, data); }; };