feat: #9109, ability to delete a post's diffs

v1.18.x
gasoved 4 years ago committed by Julian Lam
parent a87416971b
commit eb642f40b9

@ -183,8 +183,10 @@
"diffs.current-revision": "current revision", "diffs.current-revision": "current revision",
"diffs.original-revision": "original revision", "diffs.original-revision": "original revision",
"diffs.restore": "Restore this 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.post-restored": "Post successfully restored to earlier revision",
"diffs.delete": "Delete this revision",
"diffs.deleted": "Revision deleted",
"timeago_later": "%1 later", "timeago_later": "%1 later",
"timeago_earlier": "%1 earlier", "timeago_earlier": "%1 earlier",

@ -1,6 +1,6 @@
'use strict'; '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 = {}; var Diffs = {};
Diffs.open = function (pid) { Diffs.open = function (pid) {
@ -8,6 +8,44 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) {
return; 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' }; var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' };
api.get(`/posts/${pid}/diffs`, {}).then((data) => { 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, numDiffs: data.timestamps.length,
editable: data.editable, editable: data.editable,
deletable: data.deletable,
}, function (html) { }, function (html) {
var modal = bootbox.dialog({ const modalExists = !!modal;
title: '[[topic:diffs.title]]', if (modalExists) {
message: html, modal.find('.modal-body').html(html);
size: 'large', } else {
}); modal = bootbox.dialog({
title: '[[topic:diffs.title]]',
message: html,
size: 'large',
});
}
if (!data.timestamps.length) { if (!data.timestamps.length) {
return; return;
@ -36,51 +80,35 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) {
var selectEl = modal.find('select'); var selectEl = modal.find('select');
var revertEl = modal.find('button[data-action="restore"]'); var revertEl = modal.find('button[data-action="restore"]');
var deleteEl = modal.find('button[data-action="delete"]');
var postContainer = modal.find('ul.posts-list'); var postContainer = modal.find('ul.posts-list');
selectEl.on('change', function () { selectEl.on('change', function () {
Diffs.load(pid, this.value, postContainer); Diffs.load(pid, this.value, postContainer);
revertEl.prop('disabled', data.timestamps.indexOf(this.value) === 0); revertEl.prop('disabled', data.timestamps.indexOf(this.value) === 0);
deleteEl.prop('disabled', data.timestamps.indexOf(this.value) === 0);
}); });
revertEl.on('click', function () { revertEl.on('click', function () {
Diffs.restore(pid, selectEl.val(), modal); Diffs.restore(pid, selectEl.val(), modal);
}); });
deleteEl.on('click', function () {
Diffs.delete(pid, selectEl.val(), modal);
});
modal.on('shown.bs.modal', function () { modal.on('shown.bs.modal', function () {
Diffs.load(pid, selectEl.val(), postContainer); Diffs.load(pid, selectEl.val(), postContainer);
revertEl.prop('disabled', true); 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) => { if (modalExists) {
data.deleted = !!parseInt(data.deleted, 10); modal.trigger('shown.bs.modal');
}
app.parseAndTranslate('partials/posts_list', 'posts', {
posts: [data],
}, function (html) {
postContainer.empty().append(html);
}); });
}).catch(app.alertError); }).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; return Diffs;
}); });

@ -260,9 +260,14 @@ postsAPI.getDiffs = async (caller, data) => {
let usernames = await user.getUsersFields(uids, ['username']); let usernames = await user.getUsersFields(uids, ['username']);
usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null)); 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; let canEdit = true;
try { try {
await user.isPrivilegedOrSelf(caller.uid, post.uid); if (!isModerator) {
await user.isPrivilegedOrSelf(caller.uid, post.uid);
}
} catch (e) { } catch (e) {
canEdit = false; canEdit = false;
} }
@ -276,6 +281,7 @@ postsAPI.getDiffs = async (caller, data) => {
username: usernames[idx], username: usernames[idx],
})), })),
editable: canEdit, editable: canEdit,
deletable: isModerator,
}; };
}; };

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const posts = require('../../posts'); const posts = require('../../posts');
const privileges = require('../../privileges');
const api = require('../../api'); const api = require('../../api');
const helpers = require('../helpers'); const helpers = require('../helpers');
@ -94,3 +95,19 @@ Posts.restoreDiff = async (req, res) => {
helpers.formatApiResponse(200, res, await api.posts.restoreDiff(req, { ...req.params })); 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);
};

@ -71,28 +71,79 @@ module.exports = function (Posts) {
}); });
}; };
async function postDiffLoad(pid, since, uid) { Diffs.delete = async function (pid, timestamp, uid) {
// Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` getValidatedTimestamp(timestamp);
since = parseInt(since, 10);
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]]'); 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([ const [post, diffs] = await Promise.all([
Posts.getPostSummaryByPids([pid], uid, { parse: false }), Posts.getPostSummaryByPids([pid], uid, { parse: false }),
Posts.diffs.get(pid, since), Posts.diffs.get(pid, since),
]); ]);
// Replace content with re-constructed content from that point in time // Replace content with re-constructed content from that point in time
post[0].content = diffs.reduce(function (content, currentDiff) { post[0].content = diffs.reduce(applyPatch, validator.unescape(post[0].content));
const result = diff.applyPatch(content, currentDiff.patch, {
fuzzFactor: 1,
});
return typeof result === 'string' ? result : content;
}, validator.unescape(post[0].content));
return post[0]; 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;
}
}; };

@ -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', [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, '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, '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; return router;
}; };

Loading…
Cancel
Save