1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
# frozen_string_literal: true
class SearchController < ApplicationController
include ControllerWithCrossProjectAccessCheck
include SearchHelper
include RedisTracking
include ProductAnalyticsTracking
include SearchRateLimitable
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete, :aggregations].freeze
track_event :show, name: 'i_search_total', destinations: [:redis_hll, :snowplow]
def self.search_rate_limited_endpoints
%i[show count autocomplete]
end
around_action :allow_gitaly_ref_name_caching
before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch
skip_before_action :authenticate_user!
requires_cross_project_access if: -> do
search_term_present = params[:search].present? || params[:term].present?
search_term_present && !params[:project_id].present?
end
before_action :check_search_rate_limit!, only: search_rate_limited_endpoints
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
feature_category :global_search
urgency :low
def show
@project = search_service.project
@group = search_service.group
return unless search_term_valid?
return if check_single_commit_result?
@search_term = params[:search]
@sort = params[:sort] || default_sort
@search_service = Gitlab::View::Presenter::Factory.new(search_service, current_user: current_user).fabricate!
@search_level = @search_service.level
@search_type = search_type
@global_search_duration_s = Benchmark.realtime do
@scope = @search_service.scope
@without_count = @search_service.without_count?
@show_snippets = @search_service.show_snippets?
@search_results = @search_service.search_results
@search_objects = @search_service.search_objects
@search_highlight = @search_service.search_highlight
end
Gitlab::Metrics::GlobalSearchSlis.record_apdex(
elapsed: @global_search_duration_s,
search_type: @search_type,
search_level: @search_level,
search_scope: @scope
)
increment_search_counters
ensure
if @search_type
# If we raise an error somewhere in the @global_search_duration_s benchmark block, we will end up here
# with a 200 status code, but an empty @global_search_duration_s.
Gitlab::Metrics::GlobalSearchSlis.record_error_rate(
error: @global_search_duration_s.nil? || (status < 200 || status >= 400),
search_type: @search_type,
search_level: @search_level,
search_scope: @scope
)
end
end
def count
params.require([:search, :scope])
scope = search_service.scope
count = 0
ApplicationRecord.with_fast_read_statement_timeout do
count = search_service.search_results.formatted_count(scope)
end
# Users switching tabs will keep fetching the same tab counts so it's a
# good idea to cache in their browser just for a short time. They can still
# clear cache if they are seeing an incorrect count but inaccurate count is
# not such a bad thing.
expires_in 1.minute
render json: { count: count }
end
def autocomplete
term = params[:term]
@project = search_service.project
@ref = params[:project_ref] if params[:project_ref].present?
@filter = params[:filter]
render json: search_autocomplete_opts(term, filter: @filter).to_json
end
def opensearch
end
private
# overridden in EE
def default_sort
'created_desc'
end
def search_term_valid?
return false if params[:search].blank?
unless search_service.valid_query_length?
flash[:alert] = t('errors.messages.search_chars_too_long', count: Gitlab::Search::Params::SEARCH_CHAR_LIMIT)
return false
end
unless search_service.valid_terms_count?
flash[:alert] = t('errors.messages.search_terms_too_long', count: Gitlab::Search::Params::SEARCH_TERM_LIMIT)
return false
end
true
end
def check_single_commit_result?
return false if params[:force_search_results]
return false unless @project.present?
# download_code project policy grants user the read_commit ability
return false unless Ability.allowed?(current_user, :download_code, @project)
query = params[:search].strip.downcase
return false unless Commit.valid_hash?(query)
commit = @project.commit_by(oid: query)
return false unless commit.present?
link = search_path(safe_params.merge(force_search_results: true))
flash[:notice] = html_escape(_("You have been redirected to the only result; see the %{a_start}search results%{a_end} instead.")) % { a_start: "<a href=\"#{link}\"><u>".html_safe, a_end: '</u></a>'.html_safe }
redirect_to project_commit_path(@project, commit)
true
end
def increment_search_counters
Gitlab::UsageDataCounters::SearchCounter.count(:all_searches)
return if params[:nav_source] != 'navbar'
Gitlab::UsageDataCounters::SearchCounter.count(:navbar_searches)
end
def append_info_to_payload(payload)
super
# Merging to :metadata will ensure these are logged as top level keys
payload[:metadata] ||= {}
payload[:metadata]['meta.search.group_id'] = params[:group_id]
payload[:metadata]['meta.search.project_id'] = params[:project_id]
payload[:metadata]['meta.search.scope'] = params[:scope] || @scope
payload[:metadata]['meta.search.filters.confidential'] = params[:confidential]
payload[:metadata]['meta.search.filters.state'] = params[:state]
payload[:metadata]['meta.search.force_search_results'] = params[:force_search_results]
payload[:metadata]['meta.search.project_ids'] = params[:project_ids]
payload[:metadata]['meta.search.filters.language'] = params[:language]
payload[:metadata]['meta.search.type'] = @search_type if @search_type.present?
payload[:metadata]['meta.search.level'] = @search_level if @search_level.present?
payload[:metadata][:global_search_duration_s] = @global_search_duration_s if @global_search_duration_s.present?
if search_service.abuse_detected?
payload[:metadata]['abuse.confidence'] = Gitlab::Abuse.confidence(:certain)
payload[:metadata]['abuse.messages'] = search_service.abuse_messages
end
end
def block_anonymous_global_searches
return unless search_service.global_search?
return if current_user
return unless ::Feature.enabled?(:block_anonymous_global_searches, type: :ops)
store_location_for(:user, request.fullpath)
redirect_to new_user_session_path, alert: _('You must be logged in to search across all of GitLab')
end
def check_scope_global_search_enabled
return unless search_service.global_search?
search_allowed = case params[:scope]
when 'blobs'
Feature.enabled?(:global_search_code_tab, current_user, type: :ops)
when 'commits'
Feature.enabled?(:global_search_commits_tab, current_user, type: :ops)
when 'issues'
Feature.enabled?(:global_search_issues_tab, current_user, type: :ops)
when 'merge_requests'
Feature.enabled?(:global_search_merge_requests_tab, current_user, type: :ops)
when 'wiki_blobs'
Feature.enabled?(:global_search_wiki_tab, current_user, type: :ops)
when 'users'
Feature.enabled?(:global_search_users_tab, current_user, type: :ops)
else
true
end
return if search_allowed
redirect_to search_path, alert: _('Global Search is disabled for this scope')
end
def render_timeout(exception)
raise exception unless action_name.to_sym.in?(RESCUE_FROM_TIMEOUT_ACTIONS)
log_exception(exception)
@timeout = true
case action_name.to_sym
when :count
render json: {}, status: :request_timeout
when :autocomplete, :aggregations
render json: [], status: :request_timeout
else
render status: :request_timeout
end
end
def tracking_namespace_source
search_service.project&.namespace || search_service.group
end
def search_type
'basic'
end
end
SearchController.prepend_mod_with('SearchController')
|