mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 11:05:54 +01:00 
			
		
		
		
	feat: #9109, ability to delete a post's diffs
This commit is contained in:
		| @@ -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,51 +8,7 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) { | |||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; | 		openModal(pid); | ||||||
|  |  | ||||||
| 		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); |  | ||||||
|  |  | ||||||
| 					return { |  | ||||||
| 						username: revision.username, |  | ||||||
| 						timestamp: timestamp, |  | ||||||
| 						pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts), |  | ||||||
| 					}; |  | ||||||
| 				}), |  | ||||||
| 				numDiffs: data.timestamps.length, |  | ||||||
| 				editable: data.editable, |  | ||||||
| 			}, function (html) { |  | ||||||
| 				var modal = bootbox.dialog({ |  | ||||||
| 					title: '[[topic:diffs.title]]', |  | ||||||
| 					message: html, |  | ||||||
| 					size: 'large', |  | ||||||
| 				}); |  | ||||||
|  |  | ||||||
| 				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) === 0); |  | ||||||
| 				}); |  | ||||||
|  |  | ||||||
| 				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); |  | ||||||
| 				}); |  | ||||||
| 			}); |  | ||||||
| 		}).catch(app.alertError); |  | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	Diffs.load = function (pid, since, postContainer) { | 	Diffs.load = function (pid, since, postContainer) { | ||||||
| @@ -82,5 +38,77 @@ define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) { | |||||||
| 		}).catch(app.alertError); | 		}).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) => { | ||||||
|  | 			app.parseAndTranslate('partials/modals/post_history', { | ||||||
|  | 				diffs: data.revisions.map(function (revision) { | ||||||
|  | 					var timestamp = parseInt(revision.timestamp, 10); | ||||||
|  |  | ||||||
|  | 					return { | ||||||
|  | 						username: revision.username, | ||||||
|  | 						timestamp: timestamp, | ||||||
|  | 						pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts), | ||||||
|  | 					}; | ||||||
|  | 				}), | ||||||
|  | 				numDiffs: data.timestamps.length, | ||||||
|  | 				editable: data.editable, | ||||||
|  | 				deletable: data.deletable, | ||||||
|  | 			}, function (html) { | ||||||
|  | 				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; | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				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); | ||||||
|  | 				}); | ||||||
|  |  | ||||||
|  | 				if (modalExists) { | ||||||
|  | 					modal.trigger('shown.bs.modal'); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		}).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 { | ||||||
|  | 		if (!isModerator) { | ||||||
| 			await user.isPrivilegedOrSelf(caller.uid, post.uid); | 			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; | ||||||
| }; | }; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user