mirror of
https://github.com/discourse/discourse.git
synced 2026-03-04 01:15:08 +08:00
The 't' shortcut (in:title) modifies where to search but still requires actual search terms to produce meaningful results. Unlike 'l' (latest) and 'r' (recent) which return results on their own, 't' alone should enforce the minimum search term length. Renames `valid_search_shortcut?` to `min_length_bypass?` to better describe the method's purpose.
280 lines
8.4 KiB
Ruby
280 lines
8.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class SearchController < ApplicationController
|
|
before_action :block_crawler, only: :show
|
|
before_action :cancel_overloaded_search, only: [:query]
|
|
skip_before_action :check_xhr, only: :show
|
|
after_action :add_noindex_header
|
|
|
|
PAGE_LIMIT = 10
|
|
|
|
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
|
|
raise Discourse::InvalidParameters.new(:q) if params[:q].present? && !@search_term.present?
|
|
|
|
if @search_term.present? && @search_term.length < SiteSetting.min_search_term_length &&
|
|
!Search.min_length_bypass?(@search_term)
|
|
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
|
|
raise Discourse::InvalidParameters if page && (!page.is_a?(String) || page.to_i.to_s != page)
|
|
if page && page.to_i > PAGE_LIMIT
|
|
raise Discourse::InvalidParameters.new("page parameter must not be greater than 10")
|
|
end
|
|
|
|
discourse_expires_in 1.minute
|
|
|
|
search_args = {
|
|
type_filter: "topic",
|
|
guardian: guardian,
|
|
blurb_length: 300,
|
|
page: [page.to_i, 1].max,
|
|
}
|
|
|
|
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_agent] = request.user_agent
|
|
search_args[:user_id] = current_user.id if current_user.present?
|
|
|
|
if rate_limit_search
|
|
return(
|
|
render json: failed_json.merge(message: I18n.t("rate_limiter.slow_down")),
|
|
status: :too_many_requests
|
|
)
|
|
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, scope: guardian)
|
|
|
|
respond_to do |format|
|
|
format.html { store_preloaded("search", MultiJson.dump(serializer)) }
|
|
format.json { render_json_dump(serializer) }
|
|
end
|
|
end
|
|
|
|
def query
|
|
params.require(:term)
|
|
|
|
if params[:term].include?("\u0000")
|
|
raise Discourse::InvalidParameters.new("string contains null byte")
|
|
end
|
|
|
|
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_agent] = request.user_agent
|
|
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_search
|
|
return(
|
|
render json: failed_json.merge(message: I18n.t("rate_limiter.slow_down")),
|
|
status: :too_many_requests
|
|
)
|
|
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[Middleware::ProcessingRequest::REQUEST_QUEUE_SECONDS_ENV_KEY]
|
|
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}-per-sec",
|
|
SiteSetting.rate_limit_search_anon_user_per_second,
|
|
1.second,
|
|
).performed!
|
|
RateLimiter.new(
|
|
nil,
|
|
"search-min-#{request.remote_ip}-per-min",
|
|
SiteSetting.rate_limit_search_anon_user_per_minute,
|
|
1.minute,
|
|
).performed!
|
|
RateLimiter.new(
|
|
nil,
|
|
"search-min-anon-global-per-sec",
|
|
SiteSetting.rate_limit_search_anon_global_per_second,
|
|
1.second,
|
|
).performed!
|
|
RateLimiter.new(
|
|
nil,
|
|
"search-min-anon-global-per-min",
|
|
SiteSetting.rate_limit_search_anon_global_per_minute,
|
|
1.minute,
|
|
).performed!
|
|
end
|
|
rescue RateLimiter::LimitExceeded => e
|
|
return e
|
|
end
|
|
false
|
|
end
|
|
|
|
def block_crawler
|
|
if use_crawler_layout?
|
|
crawler_html = <<~HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta name='robots' content='noindex'>
|
|
</head>
|
|
<body>
|
|
<p><em>*waves hand*</em> This is not the content you are looking for</p>
|
|
</body>
|
|
</html>
|
|
HTML
|
|
|
|
response.headers["X-Robots-Tag"] = "noindex"
|
|
|
|
render html: crawler_html.html_safe, layout: false, content_type: "text/html"
|
|
end
|
|
end
|
|
|
|
def cancel_overloaded_search
|
|
render_json_error I18n.t("search.extreme_load_error"), status: 409 if site_overloaded?
|
|
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, name: id }
|
|
end
|
|
end
|
|
|
|
if search_context.present?
|
|
if SearchController.valid_context_types.exclude?(search_context[:type])
|
|
raise Discourse::InvalidParameters.new(:search_context)
|
|
end
|
|
raise Discourse::InvalidParameters.new(:search_context) if search_context[:id].blank?
|
|
|
|
# A user is found by username
|
|
context_obj = nil
|
|
if %w[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]
|
|
if !DiscourseTagging.hidden_tag_names(guardian).include?(search_context[:id])
|
|
context_obj = Tag.where_name(search_context[:id]).first
|
|
end
|
|
end
|
|
|
|
type_filter = nil
|
|
type_filter = "private_messages" if search_context[:type] == "private_messages"
|
|
|
|
guardian.ensure_can_see!(context_obj)
|
|
|
|
[context_obj, type_filter]
|
|
end
|
|
end
|
|
end
|