discourse/app/controllers/search_controller.rb
Penar Musaraj 8222810099
FIX: Limits for PM and group header search (#16887)
When searching for PMs or PMs in a group inbox, results in the header search were not being limited to 5 with a "More" link to the full page search. This PR fixes that.

It also simplifies the logic and updates the search API docs to include recently added `in:messages` and `group_messages:groupname` options.
2022-05-24 11:31:24 -04:00

244 lines
7.2 KiB
Ruby

# 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']
if queue_time
threshold = GlobalSetting.disable_search_queue_threshold.to_f
threshold > 0 && queue_time > threshold
else
false
end
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