# 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, :filter, ].flatten rescue_from ActionController::Redirecting::UnsafeRedirectError do rescue_discourse_actions(:not_found, 404) end # 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 { |t| t.includes_destination_category = t.shared_draft.present? } 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 @rss_description = 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}.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 filter raise Discourse::NotFound if !SiteSetting.experimental_topics_filter topic_query_opts = { no_definitions: !SiteSetting.show_category_definitions_in_topic_lists } %i[page q].each do |key| if params.key?(key.to_s) value = params[key] raise Discourse::InvalidParameters.new(key) if !TopicQuery.validate?(key, value) topic_query_opts[key] = value end end user = list_target_user list = TopicQuery.new(user, topic_query_opts).list_filter list.more_topics_url = construct_url_with(:next, topic_query_opts) list.prev_topics_url = construct_url_with(:prev, topic_query_opts) respond_with_list(list) end def category_default canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}" view_method = @category.default_view view_method = "latest" if %w[latest top].exclude?(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), }, %i[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) define_method action do message_route(action) end end def message_route(action) target_user = fetch_user_from_params( { include_inactive: current_user.try(:staff?) }, %i[user_stat user_option], ) case action when :private_messages_unread, :private_messages_new, :private_messages_group_new, :private_messages_group_unread raise Discourse::NotFound if target_user.id != current_user.id when :private_messages_tag raise Discourse::NotFound if !guardian.can_tag_pms? when :private_messages_warnings guardian.ensure_can_see_warnings!(target_user) when :private_messages_group, :private_messages_group_archive group = Group.find_by("LOWER(name) = ?", params[:group_name].downcase) raise Discourse::NotFound if !group raise Discourse::NotFound unless guardian.can_see_group_messages?(group) else guardian.ensure_can_see_private_messages!(target_user.id) end 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_new private_messages_archive private_messages_group private_messages_group_new private_messages_group_unread private_messages_group_archive private_messages_warnings private_messages_tag ].each { |action| generate_message_route(action) } 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") period = params[:period] || SiteSetting.top_page_default_timeframe.to_sym TopTopic.validate_period(period) @topic_list = TopicQuery.new(nil).list_top_for(period) render "list", formats: [:rss] end def hot_feed discourse_expires_in 1.minute @topic_list = TopicQuery.new(nil).list_hot 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 = "#{target_user.full_url}/activity/topics" @atom_link = "#{target_user.full_url}/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 = params[:period] period ||= ListController.best_period_for(current_user.try(:previous_visit_at), options[:category]) TopTopic.validate_period(period) 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" @params = { period: period } @rss_description = "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=#{period}" @atom_link = "#{Discourse.base_url}/top.rss?period=#{period}" @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[:period] = params[:period] if params[:period].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) raise Discourse::NotFound.new("category not found", check_permalinks: true) if @category.nil? 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 CGI.unescape(current_slug) != CGI.unescape(real_slug) url = CGI.unescape(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) url = public_send(method, opts.merge(page_params)).sub(".json?", "?") # Unicode usernames need to be encoded when calling Rails' path helper. However, it means that the already # encoded username are encoded again which we do not want. As such, we unencode the url once when unicode usernames # have been enabled. url = UrlHelper.unencode(url) if SiteSetting.unicode_usernames url 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).pick(: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