mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 02:59:07 +08:00
## Summary Correctly restrict private calendar event access to current group members. The patch replaces stale invitee-row authorization with active invited-group membership checks across event detail serialization, attendance searches, RSVP mutations, and livestream chat metadata, and prunes stale invitee records when a user is removed from a group. ## Source - Patch Triage: https://patch.discourse.org/patch-triage/1377 Co-authored-by: discourse-patch-triage <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
210 lines
7 KiB
Ruby
Vendored
210 lines
7 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
module DiscoursePostEvent
|
|
class EventFinder
|
|
def self.search(user, params = {})
|
|
guardian = Guardian.new(user)
|
|
|
|
build_base_query(guardian, user, params)
|
|
.then { |query| filter_by_post_id(query, params) }
|
|
.then { |query| filter_by_topic_id(query, params) }
|
|
.then { |query| filter_by_attending_user(query, params, guardian, user) }
|
|
.then { |query| filter_by_dates(query, params) }
|
|
.then { |query| filter_by_category(query, params) }
|
|
.then { |query| apply_ordering(query, params) }
|
|
.then { |query| apply_limit(query, params) }
|
|
end
|
|
|
|
private
|
|
|
|
def self.build_base_query(guardian, user, params)
|
|
topics = listable_topics(guardian)
|
|
pms = private_messages(user)
|
|
|
|
scope = DiscoursePostEvent::Event.visible
|
|
scope = scope.open if params[:include_closed].blank?
|
|
|
|
scope
|
|
.joins(post: :topic)
|
|
.merge(Post.secured(guardian))
|
|
.merge(topics.or(pms))
|
|
.joins(latest_event_date_join)
|
|
.select(
|
|
"discourse_post_event_events.*, latest_event_dates.starts_at, latest_event_dates.ends_at, latest_event_dates.finished_at",
|
|
)
|
|
.where(
|
|
"(discourse_post_event_events.recurrence IS NOT NULL) OR (latest_event_dates.starts_at IS NOT NULL) OR (discourse_post_event_events.original_starts_at IS NOT NULL)",
|
|
)
|
|
.group(
|
|
"discourse_post_event_events.id, latest_event_dates.starts_at, latest_event_dates.ends_at, latest_event_dates.finished_at",
|
|
)
|
|
end
|
|
|
|
def self.latest_event_date_join
|
|
<<~SQL
|
|
LEFT JOIN (
|
|
SELECT DISTINCT ON (event_id)
|
|
event_id,
|
|
starts_at,
|
|
ends_at,
|
|
finished_at
|
|
FROM discourse_calendar_post_event_dates
|
|
ORDER BY event_id, #{DiscoursePostEvent::EventDate.current_ordering_sql}
|
|
) latest_event_dates ON latest_event_dates.event_id = discourse_post_event_events.id
|
|
SQL
|
|
end
|
|
|
|
def self.filter_by_post_id(events, params)
|
|
return events if params[:post_id].blank?
|
|
events.where(id: params[:post_id])
|
|
end
|
|
|
|
def self.filter_by_topic_id(events, params)
|
|
return events if params[:topic_id].blank?
|
|
events.where(topics: { id: params[:topic_id] })
|
|
end
|
|
|
|
def self.filter_by_attending_user(events, params, guardian, user)
|
|
return events if params[:attending_user].blank?
|
|
|
|
attending_user = User.find_by(username_lower: params[:attending_user].downcase)
|
|
return events.none if !attending_user
|
|
|
|
statuses = [DiscoursePostEvent::Invitee.statuses[:going]]
|
|
if params[:include_interested].present? &&
|
|
can_include_interested?(guardian, user, attending_user)
|
|
statuses << DiscoursePostEvent::Invitee.statuses[:interested]
|
|
end
|
|
|
|
events =
|
|
events.joins(:invitees).where(
|
|
discourse_post_event_invitees: {
|
|
user_id: attending_user.id,
|
|
status: statuses,
|
|
},
|
|
)
|
|
|
|
guardian.is_admin? ? events : apply_privacy_restrictions(events, user)
|
|
end
|
|
|
|
def self.can_include_interested?(guardian, user, attending_user)
|
|
guardian.is_admin? || user&.id == attending_user.id
|
|
end
|
|
|
|
def self.apply_privacy_restrictions(events, user)
|
|
private_status = DiscoursePostEvent::Event.statuses[:private]
|
|
|
|
# If no user, can only see non-private events
|
|
return events.where.not(status: private_status) if user.nil?
|
|
|
|
# User can see private events if they still belong to an invited group.
|
|
events.where(<<~SQL, private_status, private_status, user.id)
|
|
discourse_post_event_events.status != ?
|
|
OR (
|
|
discourse_post_event_events.status = ?
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM group_users
|
|
INNER JOIN groups ON groups.id = group_users.group_id
|
|
WHERE group_users.user_id = ?
|
|
AND groups.name = ANY(discourse_post_event_events.raw_invitees)
|
|
)
|
|
)
|
|
SQL
|
|
end
|
|
|
|
def self.filter_by_dates(events, params)
|
|
return events if params[:before].blank? && params[:after].blank?
|
|
|
|
before_date = params[:before] == "now" ? Time.current : params[:before]&.to_datetime
|
|
after_date = params[:after] == "now" ? Time.current : params[:after]&.to_datetime
|
|
include_ongoing = params[:include_ongoing].present?
|
|
|
|
recurring_scope = build_recurring_date_scope(after_date, before_date)
|
|
non_recurring_scope =
|
|
build_non_recurring_date_scope(after_date, before_date, include_ongoing:)
|
|
|
|
# Apply the combined scope using OR logic
|
|
events.merge(recurring_scope.or(non_recurring_scope))
|
|
end
|
|
|
|
private
|
|
|
|
def self.build_recurring_date_scope(after_date, before_date)
|
|
scope = DiscoursePostEvent::Event.where.not(recurrence: nil)
|
|
|
|
if after_date
|
|
# For recurring events: original start date OR recurrence_until should be >= after_date
|
|
scope =
|
|
scope.where(
|
|
"original_starts_at >= ? OR recurrence_until IS NULL OR recurrence_until >= ?",
|
|
after_date,
|
|
after_date,
|
|
)
|
|
end
|
|
|
|
if before_date
|
|
# For recurring events: original start date should be < before_date
|
|
scope = scope.where("original_starts_at < ?", before_date)
|
|
end
|
|
|
|
scope
|
|
end
|
|
|
|
def self.build_non_recurring_date_scope(after_date, before_date, include_ongoing: false)
|
|
scope = DiscoursePostEvent::Event.where(recurrence: nil)
|
|
if after_date
|
|
if include_ongoing
|
|
scope =
|
|
scope.where(
|
|
"latest_event_dates.starts_at >= ? OR (latest_event_dates.ends_at IS NOT NULL AND latest_event_dates.ends_at >= ?)",
|
|
after_date,
|
|
after_date,
|
|
)
|
|
else
|
|
scope = scope.where("latest_event_dates.starts_at >= ?", after_date)
|
|
end
|
|
end
|
|
scope = scope.where("latest_event_dates.starts_at < ?", before_date) if before_date
|
|
scope
|
|
end
|
|
|
|
def self.filter_by_category(events, params)
|
|
return events if params[:category_id].blank?
|
|
|
|
category_id = params[:category_id].to_i
|
|
category_ids =
|
|
(
|
|
if params[:include_subcategories].present?
|
|
Category.subcategory_ids(category_id)
|
|
else
|
|
[category_id]
|
|
end
|
|
)
|
|
|
|
events.where(topics: { category_id: category_ids })
|
|
end
|
|
|
|
def self.apply_ordering(events, params)
|
|
order_direction = params[:order] == "desc" ? "DESC" : "ASC"
|
|
events.order(
|
|
"latest_event_dates.starts_at #{order_direction}, discourse_post_event_events.id #{order_direction}",
|
|
)
|
|
end
|
|
|
|
def self.apply_limit(events, params)
|
|
limit = params[:limit]&.to_i || 200
|
|
events.limit(limit.clamp(1, 200))
|
|
end
|
|
|
|
def self.listable_topics(guardian)
|
|
topics = Topic.listable_topics.secured(guardian)
|
|
topics = topics.visible unless guardian.can_see_unlisted_topics?
|
|
topics
|
|
end
|
|
|
|
def self.private_messages(user)
|
|
user ? Topic.private_messages_for_user(user) : Topic.none
|
|
end
|
|
end
|
|
end
|