discourse/lib/guardian/topic_guardian.rb
Bianca Nenciu fb3c610f09
PERF: Optimize topic query for many categories (#20743)
Sites with many categories and many of them in muted by default (see
`default_categories_muted`) reported bad performance when requesting
the homepage as an anonymous user. This was the case because of the
long query that iterated over topics and categories trying to remove
those from the muted categories.
2023-03-22 23:31:33 +02:00

400 lines
13 KiB
Ruby

# frozen_string_literal: true
#mixin for all guardian methods dealing with topic permissions
module TopicGuardian
def can_remove_allowed_users?(topic, target_user = nil)
is_staff? || (topic.user == @user && @user.has_trust_level?(TrustLevel[2])) ||
(
topic.allowed_users.count > 1 && topic.user != target_user &&
!!(target_user && user == target_user)
)
end
def can_review_topic?(topic)
return false if anonymous? || topic.nil?
return true if is_staff?
is_category_group_moderator?(topic.category)
end
def can_moderate_topic?(topic)
return false if anonymous? || topic.nil?
return true if is_staff?
can_perform_action_available_to_group_moderators?(topic)
end
def can_create_shared_draft?
SiteSetting.shared_drafts_enabled? && can_see_shared_draft?
end
def can_see_shared_draft?
@user.has_trust_level_or_staff?(SiteSetting.shared_drafts_min_trust_level)
end
def can_create_whisper?
@user.whisperer?
end
def can_see_whispers?(_topic = nil)
@user.whisperer?
end
def can_publish_topic?(topic, category)
can_see_shared_draft? && can_see?(topic) && can_create_topic_on_category?(category)
end
# Creating Methods
def can_create_topic?(parent)
is_staff? ||
(
user && user.trust_level >= SiteSetting.min_trust_to_create_topic.to_i &&
can_create_post?(parent) && Category.topic_create_allowed(self).limit(1).count == 1
)
end
def can_create_topic_on_category?(category)
# allow for category to be a number as well
category_id = Category === category ? category.id : category
can_create_topic?(nil) &&
(!category || Category.topic_create_allowed(self).where(id: category_id).count == 1)
end
def can_move_topic_to_category?(category)
category =
(
if Category === category
category
else
Category.find(category || SiteSetting.uncategorized_category_id)
end
)
is_staff? || (can_create_topic_on_category?(category) && !category.require_topic_approval?)
end
def can_create_post_on_topic?(topic)
# No users can create posts on deleted topics
return false if topic.blank?
return false if topic.trashed?
return true if is_admin?
trusted =
(authenticated? && user.has_trust_level?(TrustLevel[4])) || is_moderator? ||
can_perform_action_available_to_group_moderators?(topic)
(!(topic.closed? || topic.archived?) || trusted) && can_create_post?(topic)
end
# Editing Method
def can_edit_topic?(topic)
return false if Discourse.static_doc_topic_ids.include?(topic.id) && !is_admin?
return false unless can_see?(topic)
first_post = topic.first_post
return false if first_post&.locked? && !is_staff?
return true if is_admin?
return true if is_moderator? && can_create_post?(topic)
return true if is_category_group_moderator?(topic.category)
# can't edit topics in secured categories where you don't have permission to create topics
# except for a tiny edge case where the topic is uncategorized and you are trying
# to fix it but uncategorized is disabled
if (
SiteSetting.allow_uncategorized_topics ||
topic.category_id != SiteSetting.uncategorized_category_id
)
return false if !can_create_topic_on_category?(topic.category)
end
# Editing a shared draft.
if (
!topic.archived && !topic.private_message? &&
topic.category_id == SiteSetting.shared_drafts_category.to_i &&
can_see_category?(topic.category) && can_see_shared_draft? && can_create_post?(topic)
)
return true
end
# TL4 users can edit archived topics, but can not edit private messages
if (
SiteSetting.trusted_users_can_edit_others? && topic.archived && !topic.private_message? &&
user.has_trust_level?(TrustLevel[4]) && can_create_post?(topic)
)
return true
end
# TL3 users can not edit archived topics and private messages
if (
SiteSetting.trusted_users_can_edit_others? && !topic.archived && !topic.private_message? &&
user.has_trust_level?(TrustLevel[3]) && can_create_post?(topic)
)
return true
end
return false if topic.archived
is_my_own?(topic) && !topic.edit_time_limit_expired?(user) && !first_post&.locked? &&
(!first_post&.hidden? || can_edit_hidden_post?(first_post))
end
def can_recover_topic?(topic)
if is_staff? || (topic&.category && is_category_group_moderator?(topic.category)) ||
(SiteSetting.tl4_delete_posts_and_topics && user&.has_trust_level?(TrustLevel[4]))
!!(topic && topic.deleted_at)
else
topic && can_recover_post?(topic.ordered_posts.first)
end
end
def can_delete_topic?(topic)
!topic.trashed? &&
(
is_staff? ||
(
is_my_own?(topic) && topic.posts_count <= 1 && topic.created_at &&
topic.created_at > 24.hours.ago
) || is_category_group_moderator?(topic.category) ||
(SiteSetting.tl4_delete_posts_and_topics && user.has_trust_level?(TrustLevel[4]))
) && !topic.is_category_topic? && !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
# Ensure that all posts (including small actions) are at least soft
# deleted.
return false if topic.posts_count > 0
# All other posts that were deleted still must be permanently deleted
# before the topic can be deleted with the exception of small action
# posts that will be deleted right before the topic is.
all_posts_count =
Post
.with_deleted
.where(topic_id: topic.id)
.where(
post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]],
)
.count
return false if all_posts_count > 1
return false if !is_admin? || !can_see_topic?(topic)
return false if !topic.deleted_at
if topic.deleted_by_id == @user.id && topic.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago
return false
end
true
end
def can_toggle_topic_visibility?(topic)
can_moderate?(topic) || can_perform_action_available_to_group_moderators?(topic)
end
alias can_create_unlisted_topic? can_toggle_topic_visibility?
def can_convert_topic?(topic)
return false unless @user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)
return false if topic.blank?
return false if topic.trashed?
return false if topic.is_category_topic?
return true if is_admin?
is_moderator? && can_create_post?(topic)
end
def can_reply_as_new_topic?(topic)
authenticated? && topic && @user.has_trust_level?(TrustLevel[1])
end
def can_see_deleted_topics?(category)
is_staff? || is_category_group_moderator?(category) ||
(SiteSetting.tl4_delete_posts_and_topics && user&.has_trust_level?(TrustLevel[4]))
end
# Accepts an array of `Topic#id` and returns an array of `Topic#id` which the user can see.
def can_see_topic_ids(topic_ids: [], hide_deleted: true)
topic_ids = topic_ids.compact
return topic_ids if is_admin?
return [] if topic_ids.blank?
default_scope = Topic.unscoped.where(id: topic_ids)
# When `hide_deleted` is `true`, hide deleted topics if user is not staff or category moderator
if hide_deleted && !is_staff?
if category_group_moderation_allowed?
default_scope = default_scope.where(<<~SQL)
(
deleted_at IS NULL OR
(
deleted_at IS NOT NULL
AND topics.category_id IN (#{category_group_moderator_scope.select(:id).to_sql})
)
)
SQL
else
default_scope = default_scope.where("deleted_at IS NULL")
end
end
# Filter out topics with shared drafts if user cannot see shared drafts
if !can_see_shared_draft?
default_scope =
default_scope.left_outer_joins(:shared_draft).where("shared_drafts.id IS NULL")
end
all_topics_scope =
if authenticated?
Topic.unscoped.merge(
secured_regular_topic_scope(default_scope, topic_ids: topic_ids).or(
private_message_topic_scope(default_scope),
),
)
else
Topic.unscoped.merge(secured_regular_topic_scope(default_scope, topic_ids: topic_ids))
end
all_topics_scope.pluck(:id)
end
def can_see_topic?(topic, hide_deleted = true)
return false unless topic
return true if is_admin?
return false if hide_deleted && topic.deleted_at && !can_see_deleted_topics?(topic.category)
if topic.private_message?
return authenticated? && topic.all_allowed_users.where(id: @user.id).exists?
end
return false if topic.shared_draft && !can_see_shared_draft?
category = topic.category
can_see_category?(category) &&
(
!category.read_restricted || !is_staged? || secure_category_ids.include?(category.id) ||
topic.user == user
)
end
def can_see_unlisted_topics?
is_staff? || @user.has_trust_level?(TrustLevel[4])
end
def can_get_access_to_topic?(topic)
topic&.access_topic_via_group.present? && authenticated?
end
def filter_allowed_categories(records, category_id_column: "topics.category_id")
return records if is_admin? && !SiteSetting.suppress_secured_categories_from_admin
records =
if allowed_category_ids.size == 0
records.where("#{category_id_column} IS NULL")
else
records.where(
"#{category_id_column} IS NULL or #{category_id_column} IN (?)",
allowed_category_ids,
)
end
records.references(:categories)
end
def can_edit_featured_link?(category_id)
return false unless SiteSetting.topic_featured_link_enabled
return false if @user.trust_level == TrustLevel.levels[:newuser]
Category.where(
id: category_id || SiteSetting.uncategorized_category_id,
topic_featured_link_allowed: true,
).exists?
end
def can_update_bumped_at?
is_staff? || @user.has_trust_level?(TrustLevel[4])
end
def can_banner_topic?(topic)
topic && authenticated? && !topic.private_message? && is_staff?
end
def can_edit_tags?(topic)
return false unless can_tag_topics?
return false if topic.private_message? && !can_tag_pms?
return true if can_edit_topic?(topic)
if topic&.first_post&.wiki &&
(@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i)
return can_create_post?(topic)
end
false
end
def can_perform_action_available_to_group_moderators?(topic)
return false if anonymous? || topic.nil?
return true if is_staff?
return true if @user.has_trust_level?(TrustLevel[4])
is_category_group_moderator?(topic.category)
end
alias can_archive_topic? can_perform_action_available_to_group_moderators?
alias can_close_topic? can_perform_action_available_to_group_moderators?
alias can_open_topic? can_perform_action_available_to_group_moderators?
alias can_split_merge_topic? can_perform_action_available_to_group_moderators?
alias can_edit_staff_notes? can_perform_action_available_to_group_moderators?
alias can_pin_unpin_topic? can_perform_action_available_to_group_moderators?
def can_move_posts?(topic)
return false if is_silenced?
can_perform_action_available_to_group_moderators?(topic)
end
def affected_by_slow_mode?(topic)
topic&.slow_mode_seconds.to_i > 0 && @user.human? && !is_staff?
end
private
def private_message_topic_scope(scope)
pm_scope = scope.private_messages_for_user(user)
pm_scope = pm_scope.or(scope.where(<<~SQL)) if is_moderator?
topics.subtype = '#{TopicSubtype.moderator_warning}'
OR topics.id IN (#{Topic.has_flag_scope.select(:topic_id).to_sql})
SQL
pm_scope
end
def secured_regular_topic_scope(scope, topic_ids:)
secured_scope = Topic.unscoped.secured(self)
# Staged users are allowed to see their own topics in read restricted categories when Category#email_in and
# Category#email_in_allow_strangers has been configured.
if is_staged?
sql = <<~SQL
topics.id IN (
SELECT
topics.id
FROM topics
INNER JOIN categories ON categories.id = topics.category_id
WHERE categories.read_restricted
AND categories.email_in IS NOT NULL
AND categories.email_in_allow_strangers
AND topics.user_id = :user_id
AND topics.id IN (:topic_ids)
)
SQL
secured_scope =
secured_scope.or(Topic.unscoped.where(sql, user_id: user.id, topic_ids: topic_ids))
end
scope.listable_topics.merge(secured_scope)
end
end