mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:23:25 +08:00
FEATURE: Allow admins to permanently delete posts and topics (#14406)
Sometimes administrators want to permanently delete posts and topics from the database. To make sure that this is done for a good reasons, administrators can do this only after one minute has passed since the post was deleted or immediately if another administrator does it.
This commit is contained in:
parent
76c9de2d04
commit
c4843fc1c1
|
@ -595,9 +595,9 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
post.get("post_number") === 1 ? this.recoverTopic() : post.recover();
|
||||
},
|
||||
|
||||
deletePost(post) {
|
||||
deletePost(post, opts) {
|
||||
if (post.get("post_number") === 1) {
|
||||
return this.deleteTopic();
|
||||
return this.deleteTopic(opts);
|
||||
} else if (!post.can_delete) {
|
||||
return false;
|
||||
}
|
||||
|
@ -611,7 +611,7 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
ajax(`/posts/${post.id}/reply-ids.json`).then((replies) => {
|
||||
if (replies.length === 0) {
|
||||
return post
|
||||
.destroy(user)
|
||||
.destroy(user, opts)
|
||||
.then(refresh)
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
|
@ -630,7 +630,7 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
label: I18n.t("post.controls.delete_replies.just_the_post"),
|
||||
callback() {
|
||||
post
|
||||
.destroy(user)
|
||||
.destroy(user, opts)
|
||||
.then(refresh)
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
|
@ -685,7 +685,7 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
});
|
||||
} else {
|
||||
return post
|
||||
.destroy(user)
|
||||
.destroy(user, opts)
|
||||
.then(refresh)
|
||||
.catch((error) => {
|
||||
popupAjaxError(error);
|
||||
|
@ -694,6 +694,19 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
}
|
||||
},
|
||||
|
||||
permanentlyDeletePost(post) {
|
||||
return bootbox.confirm(
|
||||
I18n.t("post.controls.permanently_delete_confirmation"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
(result) => {
|
||||
if (result) {
|
||||
this.send("deletePost", post, { force_destroy: true });
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
editPost(post) {
|
||||
if (!this.currentUser) {
|
||||
return bootbox.alert(I18n.t("post.controls.edit_anonymous"));
|
||||
|
@ -1497,13 +1510,13 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
this.model.recover();
|
||||
},
|
||||
|
||||
deleteTopic() {
|
||||
deleteTopic(opts) {
|
||||
if (
|
||||
this.model.views > this.siteSettings.min_topic_views_for_delete_confirm
|
||||
) {
|
||||
this.deleteTopicModal();
|
||||
} else {
|
||||
this.model.destroy(this.currentUser);
|
||||
this.model.destroy(this.currentUser, opts);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ export function transformBasicPost(post) {
|
|||
created_at: post.created_at,
|
||||
updated_at: post.updated_at,
|
||||
canDelete: post.can_delete,
|
||||
canPermanentlyDelete: post.can_permanently_delete,
|
||||
showFlagDelete: false,
|
||||
canRecover: post.can_recover,
|
||||
canEdit: post.can_edit,
|
||||
|
@ -261,6 +262,7 @@ export default function transformPost(
|
|||
postAtts.canRecoverTopic = postAtts.isDeleted && details.can_recover;
|
||||
postAtts.canDeleteTopic = !postAtts.isDeleted && details.can_delete;
|
||||
postAtts.expandablePost = topic.expandable_first_post;
|
||||
postAtts.canPermanentlyDeleteTopic = details.can_permanently_delete;
|
||||
|
||||
// Show a "Flag to delete" message if not staff and you can't
|
||||
// otherwise delete it.
|
||||
|
|
|
@ -249,10 +249,10 @@ const Post = RestModel.extend({
|
|||
}
|
||||
},
|
||||
|
||||
destroy(deletedBy) {
|
||||
destroy(deletedBy, opts) {
|
||||
return this.setDeletedState(deletedBy).then(() => {
|
||||
return ajax("/posts/" + this.id, {
|
||||
data: { context: window.location.pathname },
|
||||
data: { context: window.location.pathname, ...opts },
|
||||
type: "DELETE",
|
||||
});
|
||||
});
|
||||
|
|
|
@ -433,9 +433,9 @@ const Topic = RestModel.extend({
|
|||
},
|
||||
|
||||
// Delete this topic
|
||||
destroy(deleted_by) {
|
||||
destroy(deleted_by, opts) {
|
||||
return ajax(`/t/${this.id}`, {
|
||||
data: { context: window.location.pathname },
|
||||
data: { context: window.location.pathname, ...opts },
|
||||
type: "DELETE",
|
||||
})
|
||||
.then(() => {
|
||||
|
|
|
@ -217,6 +217,7 @@
|
|||
showLogin=(route-action "showLogin")
|
||||
showRawEmail=(route-action "showRawEmail")
|
||||
deletePost=(action "deletePost")
|
||||
permanentlyDeletePost=(action "permanentlyDeletePost")
|
||||
recoverPost=(action "recoverPost")
|
||||
expandHidden=(action "expandHidden")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
|
|
|
@ -37,6 +37,15 @@ export function buildManageButtons(attrs, currentUser, siteSettings) {
|
|||
});
|
||||
}
|
||||
|
||||
if (attrs.canPermanentlyDelete || attrs.canPermanentlyDeleteTopic) {
|
||||
contents.push({
|
||||
icon: "trash-alt",
|
||||
className: "popup-menu-button permanently-delete",
|
||||
label: "post.controls.permanently_delete",
|
||||
action: "permanentlyDeletePost",
|
||||
});
|
||||
}
|
||||
|
||||
if (!attrs.isWhisper && currentUser.staff) {
|
||||
const buttonAtts = {
|
||||
action: "togglePostType",
|
||||
|
|
|
@ -649,6 +649,36 @@ discourseModule("Integration | Component | Widget | post", function (hooks) {
|
|||
},
|
||||
});
|
||||
|
||||
componentTest("permanently delete topic", {
|
||||
template: hbs`{{mount-widget widget="post" args=args permanentlyDeletePost=permanentlyDeletePost}}`,
|
||||
beforeEach() {
|
||||
this.set("args", { canManage: true, canPermanentlyDeleteTopic: true });
|
||||
this.set("permanentlyDeletePost", () => (this.deleted = true));
|
||||
},
|
||||
async test(assert) {
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(".post-admin-menu .permanently-delete");
|
||||
assert.ok(this.deleted);
|
||||
assert.ok(!exists(".post-admin-menu"), "also hides the menu");
|
||||
},
|
||||
});
|
||||
|
||||
componentTest("permanently delete post", {
|
||||
template: hbs`
|
||||
{{mount-widget widget="post" args=args permanentlyDeletePost=permanentlyDeletePost}}
|
||||
`,
|
||||
beforeEach() {
|
||||
this.set("args", { canManage: true, canPermanentlyDelete: true });
|
||||
this.set("permanentlyDeletePost", () => (this.deleted = true));
|
||||
},
|
||||
async test(assert) {
|
||||
await click(".post-menu-area .show-post-admin-menu");
|
||||
await click(".post-admin-menu .permanently-delete");
|
||||
assert.ok(this.deleted);
|
||||
assert.ok(!exists(".post-admin-menu"), "also hides the menu");
|
||||
},
|
||||
});
|
||||
|
||||
componentTest("toggle moderator post", {
|
||||
template: hbs`
|
||||
{{mount-widget widget="post" args=args togglePostType=togglePostType}}
|
||||
|
|
|
@ -303,14 +303,24 @@ class PostsController < ApplicationController
|
|||
|
||||
def destroy
|
||||
post = find_post_from_params
|
||||
guardian.ensure_can_delete!(post)
|
||||
|
||||
force_destroy = false
|
||||
if params[:force_destroy].present?
|
||||
if !guardian.can_permanently_delete?(post)
|
||||
return render_json_error post.cannot_permanently_delete_reason(current_user), status: 403
|
||||
end
|
||||
|
||||
force_destroy = true
|
||||
else
|
||||
guardian.ensure_can_delete!(post)
|
||||
end
|
||||
|
||||
unless guardian.can_moderate_topic?(post.topic)
|
||||
RateLimiter.new(current_user, "delete_post_per_min", SiteSetting.max_post_deletions_per_minute, 1.minute).performed!
|
||||
RateLimiter.new(current_user, "delete_post_per_day", SiteSetting.max_post_deletions_per_day, 1.day).performed!
|
||||
end
|
||||
|
||||
destroyer = PostDestroyer.new(current_user, post, context: params[:context])
|
||||
destroyer = PostDestroyer.new(current_user, post, context: params[:context], force_destroy: force_destroy)
|
||||
destroyer.destroy
|
||||
|
||||
render body: nil
|
||||
|
|
|
@ -606,11 +606,21 @@ class TopicsController < ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
topic = Topic.find_by(id: params[:id])
|
||||
guardian.ensure_can_delete!(topic)
|
||||
topic = Topic.with_deleted.find_by(id: params[:id])
|
||||
|
||||
first_post = topic.ordered_posts.first
|
||||
PostDestroyer.new(current_user, first_post, context: params[:context]).destroy
|
||||
force_destroy = false
|
||||
if params[:force_destroy].present?
|
||||
if !guardian.can_permanently_delete?(topic)
|
||||
return render_json_error topic.cannot_permanently_delete_reason(current_user), status: 403
|
||||
end
|
||||
|
||||
force_destroy = true
|
||||
else
|
||||
guardian.ensure_can_delete!(topic)
|
||||
end
|
||||
|
||||
first_post = topic.posts.with_deleted.order(:post_number).first
|
||||
PostDestroyer.new(current_user, first_post, context: params[:context], force_destroy: force_destroy).destroy
|
||||
|
||||
render body: nil
|
||||
rescue Discourse::InvalidAccess
|
||||
|
|
|
@ -920,6 +920,21 @@ class Category < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def cannot_delete_reason
|
||||
return I18n.t('category.cannot_delete.uncategorized') if self.uncategorized?
|
||||
return I18n.t('category.cannot_delete.has_subcategories') if self.has_children?
|
||||
|
||||
if self.topic_count != 0
|
||||
oldest_topic = self.topics.where.not(id: self.topic_id).order('created_at ASC').limit(1).first
|
||||
if oldest_topic
|
||||
I18n.t('category.cannot_delete.topic_exists', count: self.topic_count, topic_link: "<a href=\"#{oldest_topic.url}\">#{CGI.escapeHTML(oldest_topic.title)}</a>")
|
||||
else
|
||||
# This is a weird case, probably indicating a bug.
|
||||
I18n.t('category.cannot_delete.topic_exists_no_oldest', count: self.topic_count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def should_update_reviewables?
|
||||
|
|
|
@ -25,6 +25,9 @@ class Post < ActiveRecord::Base
|
|||
# Version 2 15-12-2017, introduces CommonMark and a huge number of onebox fixes
|
||||
BAKED_VERSION = 2
|
||||
|
||||
# Time between the delete and permanent delete of a post
|
||||
PERMANENT_DELETE_TIMER = 5.minutes
|
||||
|
||||
rate_limit
|
||||
rate_limit :limit_posts_per_day
|
||||
|
||||
|
@ -1090,6 +1093,13 @@ class Post < ActiveRecord::Base
|
|||
UrlHelper.cook_url(raw_url, secure: image_upload&.secure?, local: true) if raw_url
|
||||
end
|
||||
|
||||
def cannot_permanently_delete_reason(user)
|
||||
if self.deleted_by_id == user&.id && self.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago
|
||||
time_left = RateLimiter.time_left(Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i)
|
||||
I18n.t('post.cannot_permanently_delete.wait_or_different_admin', time_left: time_left)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_quote_into_arguments(quote)
|
||||
|
|
|
@ -1787,6 +1787,15 @@ class Topic < ActiveRecord::Base
|
|||
def apply_per_day_rate_limit_for(key, method_name)
|
||||
RateLimiter.new(user, "#{key}-per-day", SiteSetting.get(method_name), 1.day.to_i)
|
||||
end
|
||||
|
||||
def cannot_permanently_delete_reason(user)
|
||||
if self.posts_count > 1
|
||||
I18n.t('post.cannot_permanently_delete.many_posts')
|
||||
elsif self.deleted_by_id == user&.id && self.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago
|
||||
time_left = RateLimiter.time_left(Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i)
|
||||
I18n.t('post.cannot_permanently_delete.wait_or_different_admin', time_left: time_left)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
|
|
@ -71,10 +71,6 @@ class CategorySerializer < SiteCategorySerializer
|
|||
scope && scope.can_delete?(object)
|
||||
end
|
||||
|
||||
def cannot_delete_reason
|
||||
scope && scope.cannot_delete_category_reason(object)
|
||||
end
|
||||
|
||||
def include_cannot_delete_reason?
|
||||
!include_can_delete? && scope && scope.can_edit?(object)
|
||||
end
|
||||
|
|
|
@ -43,6 +43,7 @@ class PostSerializer < BasicPostSerializer
|
|||
:version,
|
||||
:can_edit,
|
||||
:can_delete,
|
||||
:can_permanently_delete,
|
||||
:can_recover,
|
||||
:can_wiki,
|
||||
:link_counts,
|
||||
|
@ -166,6 +167,14 @@ class PostSerializer < BasicPostSerializer
|
|||
scope.can_delete?(object)
|
||||
end
|
||||
|
||||
def can_permanently_delete
|
||||
true
|
||||
end
|
||||
|
||||
def include_can_permanently_delete?
|
||||
SiteSetting.can_permanently_delete && object.deleted_at
|
||||
end
|
||||
|
||||
def can_recover
|
||||
scope.can_recover_post?(object)
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ class TopicViewDetailsSerializer < ApplicationSerializer
|
|||
def self.can_attributes
|
||||
[:can_move_posts,
|
||||
:can_delete,
|
||||
:can_permanently_delete,
|
||||
:can_recover,
|
||||
:can_remove_allowed_users,
|
||||
:can_invite_to,
|
||||
|
@ -110,6 +111,10 @@ class TopicViewDetailsSerializer < ApplicationSerializer
|
|||
scope.can_delete?(object.topic)
|
||||
end
|
||||
|
||||
def include_can_permanently_delete?
|
||||
SiteSetting.can_permanently_delete && object.topic.deleted_at
|
||||
end
|
||||
|
||||
def include_can_recover?
|
||||
scope.can_recover_topic?(object.topic)
|
||||
end
|
||||
|
|
|
@ -3125,6 +3125,8 @@ en:
|
|||
other: "Yes, and all %{count} replies"
|
||||
just_the_post: "No, just this post"
|
||||
admin: "post admin actions"
|
||||
permanently_delete: "Permanently Delete"
|
||||
permanently_delete_confirmation: "Are you sure you permanently want to delete this post? You will not be able to recover it."
|
||||
wiki: "Make Wiki"
|
||||
unwiki: "Remove Wiki"
|
||||
convert_to_moderator: "Add Staff Color"
|
||||
|
|
|
@ -721,6 +721,9 @@ en:
|
|||
has_likes:
|
||||
one: "%{count} Like"
|
||||
other: "%{count} Likes"
|
||||
cannot_permanently_delete:
|
||||
many_posts: "You can not permanently delete this topic because there are other posts."
|
||||
wait_or_different_admin: "You must wait %{time_left} before permanently deleting this post or a different administrator must do it."
|
||||
|
||||
rate_limiter:
|
||||
slow_down: "You’ve performed this action too many times, please try again later."
|
||||
|
|
|
@ -1677,6 +1677,9 @@ security:
|
|||
disable_onebox_media_download_controls:
|
||||
default: false
|
||||
hidden: true
|
||||
can_permanently_delete:
|
||||
default: false
|
||||
hidden: true
|
||||
|
||||
onebox:
|
||||
post_onebox_maxlength:
|
||||
|
|
|
@ -172,6 +172,10 @@ class Guardian
|
|||
can_do?(:delete, obj)
|
||||
end
|
||||
|
||||
def can_permanently_delete?(obj)
|
||||
can_do?(:permanently_delete, obj)
|
||||
end
|
||||
|
||||
def can_moderate?(obj)
|
||||
obj && authenticated? && !is_silenced? && (
|
||||
is_staff? ||
|
||||
|
|
|
@ -38,23 +38,6 @@ module CategoryGuardian
|
|||
!category.has_children?
|
||||
end
|
||||
|
||||
def cannot_delete_category_reason(category)
|
||||
return I18n.t('category.cannot_delete.uncategorized') if category.uncategorized?
|
||||
return I18n.t('category.cannot_delete.has_subcategories') if category.has_children?
|
||||
|
||||
if category.topic_count != 0
|
||||
oldest_topic = category.topics.where.not(id: category.topic_id).order('created_at ASC').limit(1).first
|
||||
if oldest_topic
|
||||
return I18n.t('category.cannot_delete.topic_exists', count: category.topic_count, topic_link: "<a href=\"#{oldest_topic.url}\">#{CGI.escapeHTML(oldest_topic.title)}</a>")
|
||||
else
|
||||
# This is a weird case, probably indicating a bug.
|
||||
return I18n.t('category.cannot_delete.topic_exists_no_oldest', count: category.topic_count)
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def can_see_serialized_category?(category_id:, read_restricted: true)
|
||||
# Guard to ensure only a boolean is passed in
|
||||
read_restricted = true unless !!read_restricted == read_restricted
|
||||
|
|
|
@ -205,6 +205,16 @@ module PostGuardian
|
|||
false
|
||||
end
|
||||
|
||||
def can_permanently_delete_post?(post)
|
||||
return false if !SiteSetting.can_permanently_delete
|
||||
return false if !post
|
||||
return false if post.is_first_post?
|
||||
return false if !is_admin? || !can_edit_post?(post)
|
||||
return false if !post.deleted_at
|
||||
return false if post.deleted_by_id == @user.id && post.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago
|
||||
true
|
||||
end
|
||||
|
||||
def can_recover_post?(post)
|
||||
return false unless post
|
||||
|
||||
|
|
|
@ -153,6 +153,16 @@ module TopicGuardian
|
|||
!Discourse.static_doc_topic_ids.include?(topic.id)
|
||||
end
|
||||
|
||||
def can_permanently_delete_topic?(topic)
|
||||
return false if !SiteSetting.can_permanently_delete
|
||||
return false if !topic
|
||||
return false if topic.posts_count > 1
|
||||
return false if !is_admin? || !can_see_topic?(topic)
|
||||
return false if !topic.deleted_at
|
||||
return false if topic.deleted_by_id == @user.id && topic.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago
|
||||
true
|
||||
end
|
||||
|
||||
def can_toggle_topic_visibility?(topic)
|
||||
can_moderate?(topic) || can_perform_action_available_to_group_moderators?(topic)
|
||||
end
|
||||
|
|
|
@ -55,7 +55,7 @@ class PostDestroyer
|
|||
|
||||
def destroy
|
||||
payload = WebHook.generate_payload(:post, @post) if WebHook.active_web_hooks(:post).exists?
|
||||
topic = @post.topic
|
||||
topic = Topic.with_deleted.find_by(id: @post.topic_id)
|
||||
is_first_post = @post.is_first_post? && topic
|
||||
has_topic_web_hooks = is_first_post && WebHook.active_web_hooks(:topic).exists?
|
||||
|
||||
|
@ -79,7 +79,7 @@ class PostDestroyer
|
|||
Jobs.enqueue(:sync_topic_user_bookmarked, topic_id: topic.id) if topic
|
||||
|
||||
if is_first_post
|
||||
UserProfile.remove_featured_topic_from_all_profiles(@topic)
|
||||
UserProfile.remove_featured_topic_from_all_profiles(topic)
|
||||
UserActionManager.topic_destroyed(topic)
|
||||
DiscourseEvent.trigger(:topic_destroyed, topic, @user)
|
||||
WebHook.enqueue_topic_hooks(:topic_destroyed, topic, topic_payload) if has_topic_web_hooks
|
||||
|
|
|
@ -2,6 +2,18 @@
|
|||
|
||||
class RateLimiter
|
||||
|
||||
def self.time_left(available_in)
|
||||
if available_in <= 3
|
||||
I18n.t("rate_limiter.short_time")
|
||||
elsif available_in < 1.minute.to_i
|
||||
I18n.t("rate_limiter.seconds", count: available_in)
|
||||
elsif available_in < 1.hour.to_i
|
||||
I18n.t("rate_limiter.minutes", count: (available_in / 1.minute.to_i))
|
||||
else
|
||||
I18n.t("rate_limiter.hours", count: (available_in / 1.hour.to_i))
|
||||
end
|
||||
end
|
||||
|
||||
# A rate limit has been exceeded.
|
||||
class LimitExceeded < StandardError
|
||||
attr_reader :type, :available_in
|
||||
|
@ -12,16 +24,7 @@ class RateLimiter
|
|||
end
|
||||
|
||||
def time_left
|
||||
@time_left ||=
|
||||
if @available_in <= 3
|
||||
I18n.t("rate_limiter.short_time")
|
||||
elsif @available_in < 1.minute.to_i
|
||||
I18n.t("rate_limiter.seconds", count: @available_in)
|
||||
elsif @available_in < 1.hour.to_i
|
||||
I18n.t("rate_limiter.minutes", count: (@available_in / 1.minute.to_i))
|
||||
else
|
||||
I18n.t("rate_limiter.hours", count: (@available_in / 1.hour.to_i))
|
||||
end
|
||||
@time_left ||= RateLimiter.time_left(@available_in)
|
||||
end
|
||||
|
||||
def description
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CategoryGuardian do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
let(:guardian) { Guardian.new(admin) }
|
||||
fab!(:category) { Fabricate(:category) }
|
||||
|
||||
describe '#cannot_delete_category_reason' do
|
||||
describe 'when category is uncategorized' do
|
||||
it 'should return the reason' do
|
||||
category = Category.find(SiteSetting.uncategorized_category_id)
|
||||
|
||||
expect(guardian.cannot_delete_category_reason(category)).to eq(
|
||||
I18n.t('category.cannot_delete.uncategorized')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when category has subcategories' do
|
||||
it 'should return the right reason' do
|
||||
category.subcategories << Fabricate(:category)
|
||||
|
||||
expect(guardian.cannot_delete_category_reason(category)).to eq(
|
||||
I18n.t('category.cannot_delete.has_subcategories')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when category has topics' do
|
||||
it 'should return the right reason' do
|
||||
topic = Fabricate(:topic,
|
||||
title: '</a><script>alert(document.cookie);</script><a>',
|
||||
category: category
|
||||
)
|
||||
|
||||
category.reload
|
||||
|
||||
expect(guardian.cannot_delete_category_reason(category)).to eq(
|
||||
I18n.t('category.cannot_delete.topic_exists',
|
||||
count: 1,
|
||||
topic_link: "<a href=\"#{topic.url}\"></a><script>alert(document.cookie);</script><a></a>"
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1220,4 +1220,48 @@ describe Category do
|
|||
expect(Category.find_by_slug_path(["#{category.id}-category", "#{subcategory.id}-category"])).to eq(subcategory)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#cannot_delete_reason' do
|
||||
fab!(:admin) { Fabricate(:admin) }
|
||||
let(:guardian) { Guardian.new(admin) }
|
||||
fab!(:category) { Fabricate(:category) }
|
||||
|
||||
describe 'when category is uncategorized' do
|
||||
it 'should return the reason' do
|
||||
category = Category.find(SiteSetting.uncategorized_category_id)
|
||||
|
||||
expect(category.cannot_delete_reason).to eq(
|
||||
I18n.t('category.cannot_delete.uncategorized')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when category has subcategories' do
|
||||
it 'should return the right reason' do
|
||||
category.subcategories << Fabricate(:category)
|
||||
|
||||
expect(category.cannot_delete_reason).to eq(
|
||||
I18n.t('category.cannot_delete.has_subcategories')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when category has topics' do
|
||||
it 'should return the right reason' do
|
||||
topic = Fabricate(:topic,
|
||||
title: '</a><script>alert(document.cookie);</script><a>',
|
||||
category: category
|
||||
)
|
||||
|
||||
category.reload
|
||||
|
||||
expect(category.cannot_delete_reason).to eq(
|
||||
I18n.t('category.cannot_delete.topic_exists',
|
||||
count: 1,
|
||||
topic_link: "<a href=\"#{topic.url}\"></a><script>alert(document.cookie);</script><a></a>"
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -224,6 +224,65 @@ describe PostsController do
|
|||
|
||||
delete "/posts/#{post.id}.json"
|
||||
end
|
||||
|
||||
context "permanently destroy" do
|
||||
let!(:post) { Fabricate(:post, topic_id: topic.id, post_number: 3) }
|
||||
|
||||
before do
|
||||
SiteSetting.can_permanently_delete = true
|
||||
end
|
||||
|
||||
it "does not work for a post that was not deleted yet" do
|
||||
sign_in(admin)
|
||||
|
||||
delete "/posts/#{post.id}.json", params: { force_destroy: true }
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it "needs some time to pass to permanently delete a topic" do
|
||||
sign_in(admin)
|
||||
|
||||
delete "/posts/#{post.id}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(post.reload.deleted_by_id).to eq(admin.id)
|
||||
|
||||
delete "/posts/#{post.id}.json", params: { force_destroy: true }
|
||||
expect(response.status).to eq(403)
|
||||
|
||||
post.update!(deleted_at: 10.minutes.ago)
|
||||
|
||||
delete "/posts/#{post.id}.json", params: { force_destroy: true }
|
||||
expect(response.status).to eq(200)
|
||||
expect { post.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it "needs two users to permanently delete a topic" do
|
||||
sign_in(admin)
|
||||
|
||||
delete "/posts/#{post.id}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(post.reload.deleted_by_id).to eq(admin.id)
|
||||
|
||||
sign_in(Fabricate(:admin))
|
||||
|
||||
delete "/posts/#{post.id}.json", params: { force_destroy: true }
|
||||
expect(response.status).to eq(200)
|
||||
expect { post.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
it "moderators cannot permanently delete topics" do
|
||||
sign_in(admin)
|
||||
|
||||
delete "/posts/#{post.id}.json"
|
||||
expect(response.status).to eq(200)
|
||||
expect(post.reload.deleted_by_id).to eq(admin.id)
|
||||
|
||||
sign_in(moderator)
|
||||
|
||||
delete "/posts/#{post.id}.json", params: { force_destroy: true }
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user