# frozen_string_literal: true class SearchController < ApplicationController before_action :cancel_overloaded_search, only: [:query] skip_before_action :check_xhr, only: :show after_action :add_noindex_header def self.valid_context_types %w{user topic category private_messages tag} end def show permitted_params = params.permit(:q, :page) @search_term = permitted_params[:q] # a q param has been given but it's not in the correct format # eg: ?q[foo]=bar if params[:q].present? && !@search_term.present? raise Discourse::InvalidParameters.new(:q) end if @search_term.present? && @search_term.length < SiteSetting.min_search_term_length raise Discourse::InvalidParameters.new(:q) end if @search_term.present? && @search_term.include?("\u0000") raise Discourse::InvalidParameters.new("string contains null byte") end page = permitted_params[:page] # check for a malformed page parameter if page && (!page.is_a?(String) || page.to_i.to_s != page) raise Discourse::InvalidParameters end rate_limit_errors = rate_limit_search discourse_expires_in 1.minute search_args = { type_filter: 'topic', guardian: guardian, blurb_length: 300, page: if page.to_i <= 10 [page.to_i, 1].max end } context, type = lookup_search_context if context search_args[:search_context] = context search_args[:type_filter] = type if type end search_args[:search_type] = :full_page search_args[:ip_address] = request.remote_ip search_args[:user_id] = current_user.id if current_user.present? if rate_limit_errors result = Search::GroupedSearchResults.new( type_filter: search_args[:type_filter], term: @search_term, search_context: context ) result.error = I18n.t("rate_limiter.slow_down") elsif site_overloaded? result = Search::GroupedSearchResults.new( type_filter: search_args[:type_filter], term: @search_term, search_context: context ) result.error = I18n.t("search.extreme_load_error") else search = Search.new(@search_term, search_args) result = search.execute(readonly_mode: @readonly_mode) result.find_user_data(guardian) if result end serializer = serialize_data(result, GroupedSearchResultSerializer, result: result) respond_to do |format| format.html do store_preloaded("search", MultiJson.dump(serializer)) end format.json do render_json_dump(serializer) end end end def query params.require(:term) if params[:term].include?("\u0000") raise Discourse::InvalidParameters.new("string contains null byte") end rate_limit_errors = rate_limit_search discourse_expires_in 1.minute search_args = { guardian: guardian } search_args[:type_filter] = params[:type_filter] if params[:type_filter].present? search_args[:search_for_id] = true if params[:search_for_id].present? context, type = lookup_search_context if context search_args[:search_context] = context search_args[:type_filter] = type if type end search_args[:search_type] = :header search_args[:ip_address] = request.remote_ip search_args[:user_id] = current_user.id if current_user.present? search_args[:restrict_to_archetype] = params[:restrict_to_archetype] if params[:restrict_to_archetype].present? if rate_limit_errors result = Search::GroupedSearchResults.new( type_filter: search_args[:type_filter], term: params[:term], search_context: context ) result.error = I18n.t("rate_limiter.slow_down") elsif site_overloaded? result = GroupedSearchResults.new( type_filter: search_args["type_filter"], term: params[:term], search_context: context ) else search = Search.new(params[:term], search_args) result = search.execute(readonly_mode: @readonly_mode) end render_serialized(result, GroupedSearchResultSerializer, result: result) end def click params.require(:search_log_id) params.require(:search_result_type) params.require(:search_result_id) search_result_type = params[:search_result_type].downcase.to_sym if SearchLog.search_result_types.has_key?(search_result_type) attributes = { id: params[:search_log_id] } if current_user.present? attributes[:user_id] = current_user.id else attributes[:ip_address] = request.remote_ip end if search_result_type == :tag search_result_id = Tag.find_by_name(params[:search_result_id])&.id else search_result_id = params[:search_result_id] end SearchLog.where(attributes).update_all( search_result_type: SearchLog.search_result_types[search_result_type], search_result_id: search_result_id ) end render json: success_json end protected def site_overloaded? (queue_time = request.env['REQUEST_QUEUE_SECONDS']) && (GlobalSetting.disable_search_queue_threshold > 0) && (queue_time > GlobalSetting.disable_search_queue_threshold) end def rate_limit_search begin if current_user.present? RateLimiter.new(current_user, "search-min", SiteSetting.rate_limit_search_user, 1.minute).performed! else RateLimiter.new(nil, "search-min-#{request.remote_ip}", SiteSetting.rate_limit_search_anon_user, 1.minute).performed! RateLimiter.new(nil, "search-min-anon-global", SiteSetting.rate_limit_search_anon_global, 1.minute).performed! end rescue RateLimiter::LimitExceeded => e return e end false end def cancel_overloaded_search if site_overloaded? render_json_error I18n.t("search.extreme_load_error"), status: 409 end end def lookup_search_context return if params[:skip_context] == "true" search_context = params[:search_context] unless search_context if (context = params[:context]) && (id = params[:context_id]) search_context = { type: context, id: id } end end if search_context.present? raise Discourse::InvalidParameters.new(:search_context) unless SearchController.valid_context_types.include?(search_context[:type]) raise Discourse::InvalidParameters.new(:search_context) if search_context[:id].blank? # A user is found by username context_obj = nil if ['user', 'private_messages'].include? search_context[:type] context_obj = User.find_by(username_lower: search_context[:id].downcase) elsif 'category' == search_context[:type] context_obj = Category.find_by(id: search_context[:id].to_i) elsif 'topic' == search_context[:type] context_obj = Topic.find_by(id: search_context[:id].to_i) elsif 'tag' == search_context[:type] context_obj = Tag.where_name(search_context[:name]).first end type_filter = nil if search_context[:type] == 'private_messages' type_filter = 'private_messages' end guardian.ensure_can_see!(context_obj) [context_obj, type_filter] end end end