From 58b3d608fc90fd148d32bbf3d9c98180a9384f6b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 15 Jun 2020 11:02:56 -0400 Subject: [PATCH] feat: allow post diffs to be restored, #8406 --- public/language/en-GB/topic.json | 3 ++ public/src/client/topic/diffs.js | 31 ++++++++++++++++--- src/posts/diffs.js | 52 +++++++++++++++++++------------- src/posts/edit.js | 7 ++++- src/socket.io/posts/diffs.js | 19 +++++++++++- 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index 06232945cb..f61d3d8a87 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -161,6 +161,9 @@ "diffs.no-revisions-description": "This post has %1 revisions.", "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.post-restored": "Post successfully restored to earlier revision", "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 7f1e8ae115..2eccd21191 100644 --- a/public/src/client/topic/diffs.js +++ b/public/src/client/topic/diffs.js @@ -10,13 +10,13 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'], var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; - socket.emit('posts.getDiffs', { pid: pid }, function (err, timestamps) { + socket.emit('posts.getDiffs', { pid: pid }, function (err, data) { if (err) { return app.alertError(err.message); } Benchpress.parse('partials/modals/post_history', { - diffs: timestamps.map(function (timestamp) { + diffs: data.timestamps.map(function (timestamp) { timestamp = parseInt(timestamp, 10); return { @@ -24,7 +24,8 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'], pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts), }; }), - numDiffs: timestamps.length, + numDiffs: data.timestamps.length, + editable: data.editable, }, function (html) { translator.translate(html, function (html) { var modal = bootbox.dialog({ @@ -33,19 +34,26 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'], size: 'large', }); - if (!timestamps.length) { + if (!data.timestamps.length) { return; } var selectEl = modal.find('select'); + var revertEl = modal.find('button[data-action="restore"]'); 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) === -1); + }); + + revertEl.on('click', function () { + Diffs.restore(pid, selectEl.val(), modal); }); modal.on('shown.bs.modal', function () { Diffs.load(pid, selectEl.val(), postContainer); + revertEl.prop('disabled', true); }); }); }); @@ -72,5 +80,20 @@ define('forum/topic/diffs', ['forum/topic/images', 'benchpress', 'translator'], }); }; + Diffs.restore = function (pid, since, modal) { + if (!config.enablePostHistory) { + return; + } + + socket.emit('posts.restoreDiff', { pid: pid, since: since }, function (err) { + if (err) { + return app.alertError(err); + } + + modal.modal('hide'); + app.alertSuccess('[[topic:diffs.post-restored]]'); + }); + }; + return Diffs; }); diff --git a/src/posts/diffs.js b/src/posts/diffs.js index 135139d4d6..54999c41b4 100644 --- a/src/posts/diffs.js +++ b/src/posts/diffs.js @@ -33,12 +33,14 @@ module.exports = function (Posts) { return await db.getListRange('post:' + pid + ':diffs', 0, -1); }; - Diffs.save = async function (pid, oldContent, newContent) { + Diffs.save = async function (data) { + const { pid, uid, oldContent, newContent } = data; const now = Date.now(); const patch = diff.createPatch('', newContent, oldContent); await Promise.all([ db.listPrepend('post:' + pid + ':diffs', now), db.setObject('diff:' + pid + '.' + now, { + uid: uid, pid: pid, patch: patch, }), @@ -46,6 +48,31 @@ module.exports = function (Posts) { }; Diffs.load = async function (pid, since, uid) { + const post = await postDiffLoad(pid, since, uid); + + // Clear editor data (as it is outdated for this content) + delete post.edited; + post.editor = null; + + post.content = String(post.content || ''); + + const result = await plugins.fireHook('filter:parse.post', { postData: post }); + result.postData.content = translator.escape(result.postData.content); + return result.postData; + }; + + Diffs.restore = async function (pid, since, uid, req) { + const post = await postDiffLoad(pid, since, uid); + + return await Posts.edit({ + uid: uid, + pid: pid, + content: post.content, + req: req, + }); + }; + + 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); @@ -57,33 +84,16 @@ module.exports = function (Posts) { Posts.getPostSummaryByPids([pid], uid, { parse: false }), Posts.diffs.get(pid, since), ]); - const data = { - post: post, - diffs: diffs, - }; - postDiffLoad(data); - const result = await plugins.fireHook('filter:parse.post', { postData: data.post }); - result.postData.content = translator.escape(result.postData.content); - return result.postData; - }; - - function postDiffLoad(data) { - data.post = data.post[0]; - data.post.content = validator.unescape(data.post.content); // Replace content with re-constructed content from that point in time - data.post.content = data.diffs.reduce(function (content, currentDiff) { + post[0].content = diffs.reduce(function (content, currentDiff) { const result = diff.applyPatch(content, currentDiff.patch, { fuzzFactor: 1, }); return typeof result === 'string' ? result : content; - }, data.post.content); - - // Clear editor data (as it is outdated for this content) - delete data.post.edited; - data.post.editor = null; + }, validator.unescape(post[0].content)); - data.post.content = String(data.post.content || ''); + return post[0]; } }; diff --git a/src/posts/edit.js b/src/posts/edit.js index b85f54e68b..67d0b898e5 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -46,7 +46,12 @@ module.exports = function (Posts) { await Posts.setPostFields(data.pid, postData); if (meta.config.enablePostHistory === 1) { - await Posts.diffs.save(data.pid, oldContent, data.content); + await Posts.diffs.save({ + pid: data.pid, + uid: data.uid, + oldContent: oldContent, + newContent: data.content, + }); } await Posts.uploads.sync(data.pid); diff --git a/src/socket.io/posts/diffs.js b/src/socket.io/posts/diffs.js index 6f4ae21c63..837ea4b653 100644 --- a/src/socket.io/posts/diffs.js +++ b/src/socket.io/posts/diffs.js @@ -2,13 +2,19 @@ const posts = require('../../posts'); const privileges = require('../../privileges'); +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 cid = await posts.getCidByPid(data.pid); + const canEdit = await privileges.categories.can('edit', cid, socket.uid); timestamps.unshift(Date.now()); - return timestamps; + return { + timestamps: timestamps, + editable: canEdit, + }; }; SocketPosts.showPostAt = async function (socket, data) { @@ -27,4 +33,15 @@ module.exports = function (SocketPosts) { 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, websockets.reqFromSocket(socket)); + websockets.in('topic_' + edit.topic.tid).emit('event:post_edited', edit); + }; };