2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-03-04 01:15:08 +08:00
discourse/app/controllers/search_controller.rb
Sam 20811cc9c0
FIX: exclude 't' shortcut from min length bypass (#37440)
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.
2026-02-05 06:58:19 +11:00

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