discourse/app/controllers/list_controller.rb
Roman Rizzi b45a30c40f
FIX: Users without shared drafts access can still have access to the category. (#11476)
This is an edge-case of 9fb3629. An admin could set the shared draft category to one where both TL2 and TL3 users have access but only give shared draft access to TL3 users. If something like this happens, we need to make sure that TL2 users won't be able to see them, and they won't be listed on latest.

Before this change, `SharedDrafts` were lazily created when a destination category was selected. We now create it alongside the topic and set the destination to the same shared draft category.
2020-12-14 16:08:20 -03:00

453 lines
15 KiB
Ruby

# frozen_string_literal: true
class ListController < ApplicationController
include TopicListResponder
include TopicQueryParams
skip_before_action :check_xhr
before_action :set_category, only: [
:category_default,
# filtered topics lists
Discourse.filters.map { |f| :"category_#{f}" },
Discourse.filters.map { |f| :"category_none_#{f}" },
# top summaries
:category_top,
:category_none_top,
# top pages (ie. with a period)
TopTopic.periods.map { |p| :"category_top_#{p}" },
TopTopic.periods.map { |p| :"category_none_top_#{p}" },
# category feeds
:category_feed,
].flatten
before_action :ensure_logged_in, except: [
:topics_by,
# anonymous filters
Discourse.anonymous_filters,
Discourse.anonymous_filters.map { |f| "#{f}_feed" },
# anonymous categorized filters
:category_default,
Discourse.anonymous_filters.map { |f| :"category_#{f}" },
Discourse.anonymous_filters.map { |f| :"category_none_#{f}" },
# category feeds
:category_feed,
# user topics feed
:user_topics_feed,
# top summaries
:top,
:category_top,
:category_none_top,
# top pages (ie. with a period)
TopTopic.periods.map { |p| :"top_#{p}" },
TopTopic.periods.map { |p| :"top_#{p}_feed" },
TopTopic.periods.map { |p| :"category_top_#{p}" },
TopTopic.periods.map { |p| :"category_none_top_#{p}" },
:group_topics
].flatten
# Create our filters
Discourse.filters.each do |filter|
define_method(filter) do |options = nil|
list_opts = build_topic_list_options
list_opts.merge!(options) if options
user = list_target_user
if params[:category].blank? && filter == :latest && !SiteSetting.show_category_definitions_in_topic_lists
list_opts[:no_definitions] = true
end
list = TopicQuery.new(user, list_opts).public_send("list_#{filter}")
if guardian.can_create_shared_draft? && @category.present?
if @category.id == SiteSetting.shared_drafts_category.to_i
# On shared drafts, show the destination category
list.topics.each do |t|
t.includes_destination_category = t.shared_draft.present?
end
else
# When viewing a non-shared draft category, find topics whose
# destination are this category
shared_drafts = TopicQuery.new(
user,
category: SiteSetting.shared_drafts_category,
destination_category_id: list_opts[:category]
).list_latest
if shared_drafts.present? && shared_drafts.topics.present?
list.shared_drafts = shared_drafts.topics
end
end
end
list.more_topics_url = construct_url_with(:next, list_opts)
list.prev_topics_url = construct_url_with(:prev, list_opts)
if Discourse.anonymous_filters.include?(filter)
@description = SiteSetting.site_description
@rss = filter
# Note the first is the default and we don't add a title
if (filter.to_s != current_homepage) && use_crawler_layout?
filter_title = I18n.t("js.filters.#{filter.to_s}.title", count: 0)
if list_opts[:category] && @category
@title = I18n.t('js.filters.with_category', filter: filter_title, category: @category.name)
else
@title = I18n.t('js.filters.with_topics', filter: filter_title)
end
@title << " - #{SiteSetting.title}"
elsif @category.blank? && (filter.to_s == current_homepage) && SiteSetting.short_site_description.present?
@title = "#{SiteSetting.title} - #{SiteSetting.short_site_description}"
end
end
respond_with_list(list)
end
define_method("category_#{filter}") do
canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}"
self.public_send(filter, category: @category.id)
end
define_method("category_none_#{filter}") do
self.public_send(filter, category: @category.id, no_subcategories: true)
end
end
def category_default
canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}"
view_method = @category.default_view
view_method = 'latest' unless %w(latest top).include?(view_method)
self.public_send(view_method, category: @category.id)
end
def topics_by
list_opts = build_topic_list_options
target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts) }, [:user_stat, :user_option])
ensure_can_see_profile!(target_user)
list = generate_list_for("topics_by", target_user, list_opts)
list.more_topics_url = construct_url_with(:next, list_opts)
list.prev_topics_url = construct_url_with(:prev, list_opts)
respond_with_list(list)
end
def group_topics
group = Group.find_by(name: params[:group_name])
raise Discourse::NotFound unless group
guardian.ensure_can_see_group!(group)
guardian.ensure_can_see_group_members!(group)
list_opts = build_topic_list_options
list = generate_list_for("group_topics", group, list_opts)
list.more_topics_url = construct_url_with(:next, list_opts)
list.prev_topics_url = construct_url_with(:prev, list_opts)
respond_with_list(list)
end
def self.generate_message_route(action)
case action
when :private_messages_tag
define_method("#{action}") do
raise Discourse::NotFound if !guardian.can_tag_pms?
message_route(action)
end
when :private_messages_group, :private_messages_group_archive
define_method("#{action}") do
group = Group.find_by(name: params[:group_name])
raise Discourse::NotFound if !group
raise Discourse::NotFound unless guardian.can_see_group_messages?(group)
message_route(action)
end
else
define_method("#{action}") do
message_route(action)
end
end
end
def message_route(action)
target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) }, [:user_stat, :user_option])
guardian.ensure_can_see_private_messages!(target_user.id)
list_opts = build_topic_list_options
list = generate_list_for(action.to_s, target_user, list_opts)
url_prefix = "topics"
list.more_topics_url = construct_url_with(:next, list_opts, url_prefix)
list.prev_topics_url = construct_url_with(:prev, list_opts, url_prefix)
respond_with_list(list)
end
%i{
private_messages
private_messages_sent
private_messages_unread
private_messages_archive
private_messages_group
private_messages_group_archive
private_messages_tag
}.each do |action|
generate_message_route(action)
end
def latest_feed
discourse_expires_in 1.minute
options = { order: 'created' }.merge(build_topic_list_options)
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.latest")}"
@link = "#{Discourse.base_url}/latest"
@atom_link = "#{Discourse.base_url}/latest.rss"
@description = I18n.t("rss_description.latest")
@topic_list = TopicQuery.new(nil, options).list_latest
render 'list', formats: [:rss]
end
def top_feed
discourse_expires_in 1.minute
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.top")}"
@link = "#{Discourse.base_url}/top"
@atom_link = "#{Discourse.base_url}/top.rss"
@description = I18n.t("rss_description.top")
@topic_list = TopicQuery.new(nil).list_top_for(SiteSetting.top_page_default_timeframe.to_sym)
render 'list', formats: [:rss]
end
def category_feed
guardian.ensure_can_see!(@category)
discourse_expires_in 1.minute
@title = "#{@category.name} - #{SiteSetting.title}"
@link = "#{Discourse.base_url_no_prefix}#{@category.url}"
@atom_link = "#{Discourse.base_url_no_prefix}#{@category.url}.rss"
@description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}"
@topic_list = TopicQuery.new(current_user).list_new_in_category(@category)
render 'list', formats: [:rss]
end
def user_topics_feed
discourse_expires_in 1.minute
target_user = fetch_user_from_params
ensure_can_see_profile!(target_user)
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.user_topics", username: target_user.username)}"
@link = "#{Discourse.base_url}/u/#{target_user.username}/activity/topics"
@atom_link = "#{Discourse.base_url}/u/#{target_user.username}/activity/topics.rss"
@description = I18n.t("rss_description.user_topics", username: target_user.username)
@topic_list = TopicQuery
.new(nil, order: 'created')
.public_send("list_topics_by", target_user)
render 'list', formats: [:rss]
end
def top(options = nil)
options ||= {}
period = ListController.best_period_for(current_user.try(:previous_visit_at), options[:category])
public_send("top_#{period}", options)
end
def category_top
top(category: @category.id)
end
def category_none_top
top(category: @category.id, no_subcategories: true)
end
TopTopic.periods.each do |period|
define_method("top_#{period}") do |options = nil|
top_options = build_topic_list_options
top_options.merge!(options) if options
top_options[:per_page] = SiteSetting.topics_per_period_in_top_page
user = list_target_user
list = TopicQuery.new(user, top_options).list_top_for(period)
list.for_period = period
list.more_topics_url = construct_url_with(:next, top_options)
list.prev_topics_url = construct_url_with(:prev, top_options)
@rss = "top_#{period}"
if use_crawler_layout?
@title = I18n.t("js.filters.top.#{period}.title") + " - #{SiteSetting.title}"
end
respond_with_list(list)
end
define_method("category_top_#{period}") do
self.public_send("top_#{period}", category: @category.id)
end
define_method("category_none_top_#{period}") do
self.public_send("top_#{period}",
category: @category.id,
no_subcategories: true
)
end
# rss feed
define_method("top_#{period}_feed") do |options = nil|
discourse_expires_in 1.minute
@description = I18n.t("rss_description.top_#{period}")
@title = "#{SiteSetting.title} - #{@description}"
@link = "#{Discourse.base_url}/top/#{period}"
@atom_link = "#{Discourse.base_url}/top/#{period}.rss"
@topic_list = TopicQuery.new(nil).list_top_for(period)
render 'list', formats: [:rss]
end
end
protected
def next_page_params
page_params.merge(page: params[:page].to_i + 1)
end
def prev_page_params
pg = params[:page].to_i
if pg > 1
page_params.merge(page: pg - 1)
else
page_params.merge(page: nil)
end
end
private
def page_params
route_params = { format: 'json' }
if @category.present?
slug_path = @category.slug_path
route_params[:category_slug_path_with_id] =
(slug_path + [@category.id.to_s]).join("/")
end
route_params[:username] = UrlHelper.encode_component(params[:username]) if params[:username].present?
route_params
end
def set_category
category_slug_path_with_id = params.require(:category_slug_path_with_id)
@category = Category.find_by_slug_path_with_id(category_slug_path_with_id)
if @category.nil?
raise Discourse::NotFound.new("category not found", check_permalinks: true)
end
params[:category] = @category.id.to_s
if !guardian.can_see?(@category)
if SiteSetting.detailed_404
raise Discourse::InvalidAccess
else
raise Discourse::NotFound
end
end
# Check if the category slug is incorrect and redirect to a link containing
# the correct one.
current_slug = category_slug_path_with_id
if SiteSetting.slug_generation_method == "encoded"
current_slug = current_slug.split("/").map { |slug| CGI.escape(slug) }.join("/")
end
real_slug = @category.full_slug("/")
if current_slug != real_slug
url = request.fullpath.gsub(current_slug, real_slug)
if ActionController::Base.config.relative_url_root
url = url.sub(ActionController::Base.config.relative_url_root, "")
end
return redirect_to path(url), status: 301
end
@description_meta = if @category.uncategorized?
I18n.t("category.uncategorized_description", locale: SiteSetting.default_locale)
elsif @category.description_text.present?
@category.description_text
else
SiteSetting.site_description
end
if use_crawler_layout?
@subcategories = @category.subcategories.select { |c| guardian.can_see?(c) }
end
end
def list_target_user
if params[:user_id] && guardian.is_staff?
User.find(params[:user_id].to_i)
else
current_user
end
end
def generate_list_for(action, target_user, opts)
TopicQuery.new(current_user, opts).public_send("list_#{action}", target_user)
end
def construct_url_with(action, opts, url_prefix = nil)
method = url_prefix.blank? ? "#{action_name}_path" : "#{url_prefix}_#{action_name}_path"
page_params =
case action
when :prev
prev_page_params
when :next
next_page_params
else
raise "unreachable"
end
opts = opts.dup
if SiteSetting.unicode_usernames && opts[:group_name]
opts[:group_name] = UrlHelper.encode_component(opts[:group_name])
end
opts.delete(:category) if page_params.include?(:category_slug_path_with_id)
public_send(method, opts.merge(page_params)).sub('.json?', '?')
end
def ensure_can_see_profile!(target_user = nil)
raise Discourse::NotFound unless guardian.can_see_profile?(target_user)
end
def self.best_period_for(previous_visit_at, category_id = nil)
default_period = ((category_id && Category.where(id: category_id).pluck_first(:default_top_period)) ||
SiteSetting.top_page_default_timeframe).to_sym
best_period_with_topics_for(previous_visit_at, category_id, default_period) || default_period
end
def self.best_period_with_topics_for(previous_visit_at, category_id = nil, default_period = SiteSetting.top_page_default_timeframe)
best_periods_for(previous_visit_at, default_period.to_sym).find do |period|
top_topics = TopTopic.where("#{period}_score > 0")
top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id
top_topics = top_topics.limit(SiteSetting.topics_per_period_in_top_page)
top_topics.count == SiteSetting.topics_per_period_in_top_page
end
end
def self.best_periods_for(date, default_period = :all)
return [default_period, :all].uniq unless date
periods = []
periods << :daily if date > (1.week + 1.day).ago
periods << :weekly if date > (1.month + 1.week).ago
periods << :monthly if date > (3.months + 3.weeks).ago
periods << :quarterly if date > (1.year + 1.month).ago
periods << :yearly if date > 3.years.ago
periods << :all
periods
end
end