From eb642f40b9b29fa4da404455e8746fabaffffac3 Mon Sep 17 00:00:00 2001 From: gasoved Date: Thu, 28 Jan 2021 21:42:10 +0300 Subject: [PATCH] feat: #9109, ability to delete a post's diffs --- public/language/en-GB/topic.json | 4 +- public/src/client/topic/diffs.js | 94 +++++++++++++++++++++----------- src/api/posts.js | 8 ++- src/controllers/write/posts.js | 17 ++++++ src/posts/diffs.js | 73 +++++++++++++++++++++---- src/routes/write/posts.js | 1 + 6 files changed, 151 insertions(+), 46 deletions(-) diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index ac92291087..509f238589 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -183,8 +183,10 @@ "diffs.current-revision": "current revision", "diffs.original-revision": "original revision", "diffs.restore": "Restore this revision", - "diffs.restore-description": "A new revision will be appended to this post's edit history.", + "diffs.restore-description": "A new revision will be appended to this post's edit history after restoring.", "diffs.post-restored": "Post successfully restored to earlier revision", + "diffs.delete": "Delete this revision", + "diffs.deleted": "Revision deleted", "timeago_later": "%1 later", "timeago_earlier": "%1 earlier", diff --git a/public/src/client/topic/diffs.js b/public/src/client/topic/diffs.js index 3b310f6d85..801b818dd7 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', ['api', 'forum/topic/images'], function (api) { +define('forum/topic/diffs', ['api', 'bootbox', 'forum/topic/images'], function (api, bootbox) { var Diffs = {}; Diffs.open = function (pid) { @@ -8,6 +8,44 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) { return; } + openModal(pid); + }; + + Diffs.load = function (pid, since, postContainer) { + if (!config.enablePostHistory) { + return; + } + + api.get(`/posts/${pid}/diffs/${since}`, {}).then((data) => { + data.deleted = !!parseInt(data.deleted, 10); + + app.parseAndTranslate('partials/posts_list', 'posts', { + posts: [data], + }, function (html) { + postContainer.empty().append(html); + }); + }).catch(app.alertError); + }; + + Diffs.restore = function (pid, since, modal) { + if (!config.enablePostHistory) { + return; + } + + api.put(`/posts/${pid}/diffs/${since}`, {}).then(() => { + modal.modal('hide'); + app.alertSuccess('[[topic:diffs.post-restored]]'); + }).catch(app.alertError); + }; + + Diffs.delete = function (pid, timestamp, modal) { + api.del(`/posts/${pid}/diffs/${timestamp}`).then(() => { + openModal(pid, modal); + app.alertSuccess('[[topic:diffs.deleted]]'); + }).catch(app.alertError); + }; + + function openModal(pid, modal) { var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; api.get(`/posts/${pid}/diffs`, {}).then((data) => { @@ -23,12 +61,18 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) { }), numDiffs: data.timestamps.length, editable: data.editable, + deletable: data.deletable, }, function (html) { - var modal = bootbox.dialog({ - title: '[[topic:diffs.title]]', - message: html, - size: 'large', - }); + const modalExists = !!modal; + if (modalExists) { + modal.find('.modal-body').html(html); + } else { + modal = bootbox.dialog({ + title: '[[topic:diffs.title]]', + message: html, + size: 'large', + }); + } if (!data.timestamps.length) { return; @@ -36,51 +80,35 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) { var selectEl = modal.find('select'); var revertEl = modal.find('button[data-action="restore"]'); + var deleteEl = modal.find('button[data-action="delete"]'); var postContainer = modal.find('ul.posts-list'); selectEl.on('change', function () { Diffs.load(pid, this.value, postContainer); revertEl.prop('disabled', data.timestamps.indexOf(this.value) === 0); + deleteEl.prop('disabled', data.timestamps.indexOf(this.value) === 0); }); revertEl.on('click', function () { Diffs.restore(pid, selectEl.val(), modal); }); + deleteEl.on('click', function () { + Diffs.delete(pid, selectEl.val(), modal); + }); + modal.on('shown.bs.modal', function () { Diffs.load(pid, selectEl.val(), postContainer); revertEl.prop('disabled', true); + deleteEl.prop('disabled', true); }); - }); - }).catch(app.alertError); - }; - - Diffs.load = function (pid, since, postContainer) { - if (!config.enablePostHistory) { - return; - } - api.get(`/posts/${pid}/diffs/${since}`, {}).then((data) => { - data.deleted = !!parseInt(data.deleted, 10); - - app.parseAndTranslate('partials/posts_list', 'posts', { - posts: [data], - }, function (html) { - postContainer.empty().append(html); + if (modalExists) { + modal.trigger('shown.bs.modal'); + } }); }).catch(app.alertError); - }; - - Diffs.restore = function (pid, since, modal) { - if (!config.enablePostHistory) { - return; - } - - 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 301df87a2e..40026d8a23 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -260,9 +260,14 @@ postsAPI.getDiffs = async (caller, data) => { let usernames = await user.getUsersFields(uids, ['username']); usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null)); + const cid = await posts.getCidByPid(data.pid); + const isModerator = await privileges.users.isModerator(cid, caller.uid); + let canEdit = true; try { - await user.isPrivilegedOrSelf(caller.uid, post.uid); + if (!isModerator) { + await user.isPrivilegedOrSelf(caller.uid, post.uid); + } } catch (e) { canEdit = false; } @@ -276,6 +281,7 @@ postsAPI.getDiffs = async (caller, data) => { username: usernames[idx], })), editable: canEdit, + deletable: isModerator, }; }; diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 8cd094848c..fff042e32d 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -1,6 +1,7 @@ 'use strict'; const posts = require('../../posts'); +const privileges = require('../../privileges'); const api = require('../../api'); const helpers = require('../helpers'); @@ -94,3 +95,19 @@ Posts.restoreDiff = async (req, res) => { helpers.formatApiResponse(200, res, await api.posts.restoreDiff(req, { ...req.params })); }; +Posts.deleteDiff = async (req, res) => { + if (!parseInt(req.params.pid, 10)) { + throw new Error('[[error:invalid-data]]'); + } + + const cid = await posts.getCidByPid(req.params.pid); + const isModerator = privileges.users.isModerator(cid, req.uid); + + if (!isModerator) { + return helpers.formatApiResponse(403, res, new Error('[[error:no-privileges]]')); + } + + await posts.diffs.delete(req.params.pid, req.params.timestamp, req.uid); + + helpers.formatApiResponse(200, res); +}; diff --git a/src/posts/diffs.js b/src/posts/diffs.js index 79bdd5223a..2ca11b016b 100644 --- a/src/posts/diffs.js +++ b/src/posts/diffs.js @@ -71,28 +71,79 @@ module.exports = function (Posts) { }); }; - async function postDiffLoad(pid, since, uid) { - // Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` - since = parseInt(since, 10); + Diffs.delete = async function (pid, timestamp, uid) { + getValidatedTimestamp(timestamp); - if (isNaN(since) || since > Date.now()) { + const [post, diffs, timestamps] = await Promise.all([ + Posts.getPostSummaryByPids([pid], uid, { parse: false }), + Diffs.get(pid), + Diffs.list(pid), + ]); + + const timestampIndex = timestamps.indexOf(timestamp); + const lastTimestampIndex = timestamps.length - 1; + + if (timestamp === String(post[0].timestamp)) { + return Promise.all([ + db.delete(`diff:${pid}.${timestamps[lastTimestampIndex]}`), + db.listRemoveAll(`post:${pid}:diffs`, timestamps[lastTimestampIndex]), + ]); + } + if (timestampIndex === 0 || timestampIndex === -1) { throw new Error('[[error:invalid-data]]'); } + const postContent = validator.unescape(post[0].content); + const versionContents = {}; + for (let i = 0, content = postContent; i < timestamps.length; ++i) { + versionContents[timestamps[i]] = applyPatch(content, diffs[i]); + content = versionContents[timestamps[i]]; + } + + /* eslint-disable no-await-in-loop */ + for (let i = lastTimestampIndex; i >= timestampIndex; --i) { + const newContentIndex = i === timestampIndex ? i - 2 : i - 1; + const timestampToUpdate = newContentIndex + 1; + const newContent = newContentIndex < 0 ? postContent : versionContents[timestamps[newContentIndex]]; + const patch = diff.createPatch('', newContent, versionContents[timestamps[i]]); + await db.setObject('diff:' + pid + '.' + timestamps[timestampToUpdate], { patch }); + } + + return Promise.all([ + db.delete(`diff:${pid}.${timestamp}`), + db.listRemoveAll(`post:${pid}:diffs`, timestamp), + ]); + }; + + async function postDiffLoad(pid, since, uid) { + // Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` + since = getValidatedTimestamp(since); + const [post, diffs] = await Promise.all([ Posts.getPostSummaryByPids([pid], uid, { parse: false }), Posts.diffs.get(pid, since), ]); // Replace content with re-constructed content from that point in time - post[0].content = diffs.reduce(function (content, currentDiff) { - const result = diff.applyPatch(content, currentDiff.patch, { - fuzzFactor: 1, - }); - - return typeof result === 'string' ? result : content; - }, validator.unescape(post[0].content)); + post[0].content = diffs.reduce(applyPatch, validator.unescape(post[0].content)); return post[0]; } + + function getValidatedTimestamp(timestamp) { + timestamp = parseInt(timestamp, 10); + + if (isNaN(timestamp) || timestamp > Date.now()) { + throw new Error('[[error:invalid-data]]'); + } + + return timestamp; + } + + function applyPatch(content, aDiff) { + const result = diff.applyPatch(content, aDiff.patch, { + fuzzFactor: 1, + }); + return typeof result === 'string' ? result : content; + } }; diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index d842270e82..56853bc3de 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -29,6 +29,7 @@ module.exports = function () { 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); + setupApiRoute(router, 'delete', '/:pid/diffs/:timestamp', [...middlewares, middleware.assert.post], controllers.write.posts.deleteDiff); return router; };