discourse/app/queries/reports/list_query.rb
Sam 746000edc8
FIX: Enforce can_see_ip checks across admin IP features (#40019)
Same-IP user lookups now identify the target by user_id and
ip_type rather than accepting a raw IP in params, so the IP is
resolved server-side and never round-trips through clients that
lack permission to see it.

Additionally:

- Hide the `suspicious_logins` report (list, bulk, show, CSV
  export and the security dashboard tile) from non-admin staff
  lacking `can_see_ip?`.
- Hide the IP column and CSV export button on the screened
  emails page from staff lacking `can_see_ip?`.
- Omit `ip_address` from `ScreenedUrlSerializer` for staff
  lacking `can_see_ip?`.
- Require `can_see_ip?` (in addition to `can_see_emails?`) to
  export the `screened_email` entity.
- Record the username (not the IP) in the staff-log context
  for "delete other accounts with same IP" when the acting
  user lacks `can_see_ip?`.
2026-05-19 11:37:20 +08:00

124 lines
3.4 KiB
Ruby
Vendored

# frozen_string_literal: true
module Reports
class ListQuery
class FormattedReport
attr_reader :type, :name
def initialize(name)
@name = name
@type = name.to_s.gsub("report_", "")
end
def visible?(guardian:)
return false if Report.hidden?(type, guardian:)
if SiteSetting.reporting_improvements
return false if plugin_report? && plugin_disabled?
return false if Report::LEGACY_REPORTS.include?(type)
end
true
end
def to_h
result = {
type:,
title:,
description:,
description_link: I18n.t("reports.#{type}.description_link", default: "").presence,
}
if SiteSetting.reporting_improvements && plugin_report?
result[:plugin] = plugin_name
result[:plugin_display_name] = plugin_display_name
end
result
end
private
# HACK: We need to show a different label and description for some
# old reports while people are still relying on them, that lets us
# point toward the new 'Site traffic' report as well. Not ideal,
# but apart from duplicating the report there's not a nicer way to do this.
def title
return I18n.t("reports.#{type}.title_legacy") if legacy?
I18n.t("reports.#{type}.title")
end
def description
return I18n.t("reports.#{type}.description_legacy") if legacy?
I18n.t("reports.#{type}.description", default: "").presence
end
def legacy?
SiteSetting.use_legacy_pageviews &&
type.in?(%w[consolidated_page_views consolidated_page_views_browser_detection])
end
def plugin_name
return @plugin_name if defined?(@plugin_name)
@plugin_name = resolve_plugin_name
end
def resolve_plugin_name
return unless Report.singleton_class.method_defined?(@name)
source_path = Report.method(@name).source_location.first
return unless source_path&.include?("/plugins/")
# Extract plugin name from path like /plugins/discourse-ai/...
match = source_path.match(%r{/plugins/([^/]+)/})
match[1] if match
rescue NameError
nil
end
def plugin_instance
return @plugin_instance if defined?(@plugin_instance)
@plugin_instance = plugin_name && Discourse.plugins_by_name[plugin_name]
end
def plugin_display_name
plugin_instance&.humanized_name
end
def plugin_report?
plugin_name.present?
end
def plugin_disabled?
return true unless plugin_instance
!plugin_instance.enabled?
end
end
def self.call(guardian:)
page_view_req_report_methods =
["page_view_total_reqs"] +
ApplicationRequest
.req_types
.keys
.select { |r| r =~ /\Apage_view_/ && r !~ /mobile/ && r !~ /beacon/ }
.map { |r| r + "_reqs" }
if !SiteSetting.use_legacy_pageviews
page_view_req_report_methods << "page_view_legacy_total_reqs"
end
reports_methods =
page_view_req_report_methods +
Report.singleton_methods.grep(/\Areport_(?!about|storage_stats)/)
reports_methods
.filter_map do |report_name|
report = Reports::ListQuery::FormattedReport.new(report_name)
report.to_h if report.visible?(guardian:)
end
.sort_by { |report| report[:title] }
end
end
end