mirror of
https://github.com/discourse/discourse.git
synced 2024-11-27 03:10:46 -06:00
928a6cd143
In most cases, deleting a user from outside the review UI will also delete any pending reviewables for that user. This was not working in some cases, e.g. for reviewables created due to "fast typer" violations. This was happening because UserDestroyer only automatically resolves flagged posts. After this change, in addition to existing checks, look for ReviewablePost where the post was created by the user and reject them if present.
185 lines
6.0 KiB
Ruby
185 lines
6.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Responsible for destroying a User record
|
|
class UserDestroyer
|
|
class PostsExistError < RuntimeError
|
|
end
|
|
|
|
def initialize(actor)
|
|
@actor = actor
|
|
raise Discourse::InvalidParameters.new("acting user is nil") unless @actor && @actor.is_a?(User)
|
|
@guardian = Guardian.new(actor)
|
|
end
|
|
|
|
# Returns false if the user failed to be deleted.
|
|
# Returns a frozen instance of the User if the delete succeeded.
|
|
def destroy(user, opts = {})
|
|
raise Discourse::InvalidParameters.new("user is nil") unless user && user.is_a?(User)
|
|
raise PostsExistError if !opts[:delete_posts] && user.posts.joins(:topic).count != 0
|
|
@guardian.ensure_can_delete_user!(user)
|
|
|
|
# default to using a transaction
|
|
opts[:transaction] = true if opts[:transaction] != false
|
|
|
|
prepare_for_destroy(user) if opts[:prepare_for_destroy] == true
|
|
|
|
result = nil
|
|
|
|
optional_transaction(open_transaction: opts[:transaction]) do
|
|
UserSecurityKey.where(user_id: user.id).delete_all
|
|
Bookmark.where(user_id: user.id).delete_all
|
|
Draft.where(user_id: user.id).delete_all
|
|
Reviewable.where(created_by_id: user.id).delete_all
|
|
|
|
category_topic_ids = Category.where("topic_id IS NOT NULL").pluck(:topic_id)
|
|
|
|
if opts[:delete_posts]
|
|
DiscoursePluginRegistry.user_destroyer_on_content_deletion_callbacks.each do |cb|
|
|
cb.call(user, @guardian, opts)
|
|
end
|
|
|
|
agree_with_flags(user) if opts[:delete_as_spammer]
|
|
block_external_urls(user) if opts[:block_urls]
|
|
delete_posts(user, category_topic_ids, opts)
|
|
end
|
|
|
|
user.post_actions.find_each { |post_action| post_action.remove_act!(Discourse.system_user) }
|
|
|
|
# Add info about the user to staff action logs
|
|
UserHistory.staff_action_records(
|
|
Discourse.system_user,
|
|
acting_user: user.username,
|
|
).update_all(
|
|
["details = CONCAT(details, ?)", "\nuser_id: #{user.id}\nusername: #{user.username}"],
|
|
)
|
|
|
|
# keep track of emails used
|
|
user_emails = user.user_emails.pluck(:email)
|
|
|
|
if result = user.destroy
|
|
if opts[:block_email]
|
|
user_emails.each do |email|
|
|
ScreenedEmail.block(email, ip_address: result.ip_address)&.record_match!
|
|
end
|
|
end
|
|
|
|
if opts[:block_ip] && result.ip_address
|
|
ScreenedIpAddress.watch(result.ip_address)&.record_match!
|
|
if result.registration_ip_address && result.ip_address != result.registration_ip_address
|
|
ScreenedIpAddress.watch(result.registration_ip_address)&.record_match!
|
|
end
|
|
end
|
|
|
|
Post.unscoped.where(user_id: result.id).update_all(user_id: nil)
|
|
|
|
# If this user created categories, fix those up:
|
|
Category
|
|
.where(user_id: result.id)
|
|
.each do |c|
|
|
c.user_id = Discourse::SYSTEM_USER_ID
|
|
c.save!
|
|
if topic = Topic.unscoped.find_by(id: c.topic_id)
|
|
topic.recover!
|
|
topic.user_id = Discourse::SYSTEM_USER_ID
|
|
topic.save!
|
|
end
|
|
end
|
|
|
|
Invite
|
|
.where(email: user_emails)
|
|
.each do |invite|
|
|
# invited_users will be removed by dependent destroy association when user is destroyed
|
|
invite.invited_groups.destroy_all
|
|
invite.topic_invites.destroy_all
|
|
invite.destroy
|
|
end
|
|
|
|
unless opts[:quiet]
|
|
if @actor == user
|
|
deleted_by = Discourse.system_user
|
|
opts[:context] = I18n.t("staff_action_logs.user_delete_self", url: opts[:context])
|
|
else
|
|
deleted_by = @actor
|
|
end
|
|
StaffActionLogger.new(deleted_by).log_user_deletion(user, opts.slice(:context))
|
|
if opts.slice(:context).blank?
|
|
Rails.logger.warn("User destroyed without context from: #{caller_locations(14, 1)[0]}")
|
|
end
|
|
end
|
|
MessageBus.publish "/logout/#{result.id}", result.id, user_ids: [result.id]
|
|
end
|
|
end
|
|
|
|
# After the user is deleted, remove the reviewable
|
|
if reviewable = ReviewableUser.pending.find_by(target: user)
|
|
reviewable.perform(@actor, :delete_user)
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
protected
|
|
|
|
def block_external_urls(user)
|
|
TopicLink
|
|
.where(user: user, internal: false)
|
|
.find_each do |link|
|
|
next if Oneboxer.engine(link.url) != Onebox::Engine::AllowlistedGenericOnebox
|
|
ScreenedUrl.watch(link.url, link.domain, ip_address: user.ip_address)&.record_match!
|
|
end
|
|
end
|
|
|
|
def agree_with_flags(user)
|
|
ReviewableFlaggedPost
|
|
.where(target_created_by: user)
|
|
.find_each do |reviewable|
|
|
if reviewable.actions_for(@guardian).has?(:agree_and_keep)
|
|
reviewable.perform(@actor, :agree_and_keep)
|
|
end
|
|
end
|
|
|
|
ReviewablePost
|
|
.where(target_created_by: user)
|
|
.find_each do |reviewable|
|
|
if reviewable.actions_for(@guardian).has?(:reject_and_delete)
|
|
reviewable.perform(@actor, :reject_and_delete)
|
|
end
|
|
end
|
|
end
|
|
|
|
def delete_posts(user, category_topic_ids, opts)
|
|
user.posts.find_each do |post|
|
|
if post.is_first_post? && category_topic_ids.include?(post.topic_id)
|
|
post.update!(user: Discourse.system_user)
|
|
else
|
|
PostDestroyer.new(@actor.staff? ? @actor : Discourse.system_user, post).destroy
|
|
end
|
|
|
|
if post.topic && post.is_first_post?
|
|
Topic.unscoped.where(id: post.topic_id).update_all(user_id: nil)
|
|
end
|
|
end
|
|
end
|
|
|
|
def prepare_for_destroy(user)
|
|
PostAction.where(user_id: user.id).delete_all
|
|
UserAction.where(
|
|
"user_id = :user_id OR target_user_id = :user_id OR acting_user_id = :user_id",
|
|
user_id: user.id,
|
|
).delete_all
|
|
PostTiming.where(user_id: user.id).delete_all
|
|
TopicViewItem.where(user_id: user.id).delete_all
|
|
TopicUser.where(user_id: user.id).delete_all
|
|
TopicAllowedUser.where(user_id: user.id).delete_all
|
|
Notification.where(user_id: user.id).delete_all
|
|
end
|
|
|
|
def optional_transaction(open_transaction: true)
|
|
if open_transaction
|
|
User.transaction { yield }
|
|
else
|
|
yield
|
|
end
|
|
end
|
|
end
|