summaryrefslogtreecommitdiff
path: root/app/finders/projects_finder.rb
blob: 893e89daa3c81608b1a5e6f9b6b14d5c6a823e7a (plain)
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
248
249
250
# frozen_string_literal: true

# ProjectsFinder
#
# Used to filter Projects  by set of params
#
# Arguments:
#   current_user - which user use
#   project_ids_relation: int[] - project ids to use
#   params:
#     trending: boolean
#     owned: boolean
#     non_public: boolean
#     starred: boolean
#     sort: string
#     visibility_level: int
#     tags: string[]
#     personal: boolean
#     search: string
#     search_namespaces: boolean
#     minimum_search_length: int
#     non_archived: boolean
#     archived: 'only' or boolean
#     min_access_level: integer
#     last_activity_after: datetime
#     last_activity_before: datetime
#     repository_storage: string
#     without_deleted: boolean
#
class ProjectsFinder < UnionFinder
  include CustomAttributesFilter

  attr_accessor :params
  attr_reader :current_user, :project_ids_relation

  def initialize(params: {}, current_user: nil, project_ids_relation: nil)
    @params = params
    @current_user = current_user
    @project_ids_relation = project_ids_relation
  end

  def execute
    user = params.delete(:user)
    collection =
      if user
        PersonalProjectsFinder.new(user, finder_params).execute(current_user) # rubocop: disable CodeReuse/Finder
      else
        init_collection
      end

    use_cte = params.delete(:use_cte)
    collection = Project.wrap_with_cte(collection) if use_cte
    collection = filter_projects(collection)

    if params[:sort] == 'similarity' && params[:search] && Feature.enabled?(:project_finder_similarity_sort, current_user)
      collection.sorted_by_similarity_desc(params[:search])
    else
      sort(collection)
    end
  end

  private

  def init_collection
    if current_user
      collection_with_user
    else
      collection_without_user
    end
  end

  # EE would override this to add more filters
  def filter_projects(collection)
    collection = by_ids(collection)
    collection = by_personal(collection)
    collection = by_starred(collection)
    collection = by_trending(collection)
    collection = by_visibility_level(collection)
    collection = by_tags(collection)
    collection = by_search(collection)
    collection = by_archived(collection)
    collection = by_custom_attributes(collection)
    collection = by_deleted_status(collection)
    collection = by_last_activity_after(collection)
    collection = by_last_activity_before(collection)
    by_repository_storage(collection)
  end

  def collection_with_user
    if owned_projects?
      current_user.owned_projects
    elsif min_access_level?
      current_user.authorized_projects(params[:min_access_level])
    else
      if private_only? || impossible_visibility_level?
        current_user.authorized_projects
      else
        Project.public_or_visible_to_user(current_user)
      end
    end
  end

  # Builds a collection for an anonymous user.
  def collection_without_user
    if private_only? || owned_projects? || min_access_level?
      Project.none
    else
      Project.public_to_user
    end
  end

  # This is an optimization - surprisingly PostgreSQL does not optimize
  # for this.
  #
  # If the default visiblity level and desired visiblity level filter cancels
  # each other out, don't use the SQL clause for visibility level in
  # `Project.public_or_visible_to_user`. In fact, this then becames equivalent
  # to just authorized projects for the user.
  #
  # E.g.
  # (EXISTS(<authorized_projects>) OR projects.visibility_level IN (10,20))
  #   AND "projects"."visibility_level" = 0
  #
  # is essentially
  # EXISTS(<authorized_projects>) AND "projects"."visibility_level" = 0
  #
  # See https://gitlab.com/gitlab-org/gitlab/issues/37007
  def impossible_visibility_level?
    return unless params[:visibility_level].present?

    public_visibility_levels = Gitlab::VisibilityLevel.levels_for_user(current_user)

    !public_visibility_levels.include?(params[:visibility_level].to_i)
  end

  def owned_projects?
    params[:owned].present?
  end

  def private_only?
    params[:non_public].present?
  end

  def min_access_level?
    params[:min_access_level].present?
  end

  # rubocop: disable CodeReuse/ActiveRecord
  def by_ids(items)
    items = items.where(id: project_ids_relation) if project_ids_relation
    items = items.where('projects.id > ?', params[:id_after]) if params[:id_after]
    items = items.where('projects.id < ?', params[:id_before]) if params[:id_before]
    items
  end
  # rubocop: enable CodeReuse/ActiveRecord

  def union(items)
    find_union(items, Project).with_route
  end

  def by_personal(items)
    params[:personal].present? && current_user ? items.personal(current_user) : items
  end

  def by_starred(items)
    params[:starred].present? && current_user ? items.starred_by(current_user) : items
  end

  def by_trending(items)
    params[:trending].present? ? items.trending : items
  end

  # rubocop: disable CodeReuse/ActiveRecord
  def by_visibility_level(items)
    params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
  end
  # rubocop: enable CodeReuse/ActiveRecord

  def by_tags(items)
    params[:tag].present? ? items.tagged_with(params[:tag]) : items
  end

  def by_search(items)
    params[:search] ||= params[:name]

    return items.none if params[:search].present? && params[:minimum_search_length].present? && params[:search].length < params[:minimum_search_length].to_i

    items.optionally_search(params[:search], include_namespace: params[:search_namespaces].present?)
  end

  def by_deleted_status(items)
    params[:without_deleted].present? ? items.without_deleted : items
  end

  def by_last_activity_after(items)
    if params[:last_activity_after].present?
      items.where("last_activity_at > ?", params[:last_activity_after]) # rubocop: disable CodeReuse/ActiveRecord
    else
      items
    end
  end

  def by_last_activity_before(items)
    if params[:last_activity_before].present?
      items.where("last_activity_at < ?", params[:last_activity_before]) # rubocop: disable CodeReuse/ActiveRecord
    else
      items
    end
  end

  def by_repository_storage(items)
    if params[:repository_storage].present?
      items.where(repository_storage: params[:repository_storage]) # rubocop: disable CodeReuse/ActiveRecord
    else
      items
    end
  end

  def sort(items)
    if params[:sort].present?
      items.sort_by_attribute(params[:sort])
    else
      items.projects_order_id_desc
    end
  end

  def by_archived(projects)
    if params[:non_archived]
      projects.non_archived
    elsif params.key?(:archived)
      if params[:archived] == 'only'
        projects.archived
      elsif Gitlab::Utils.to_boolean(params[:archived])
        projects
      else
        projects.non_archived
      end
    else
      projects
    end
  end

  def finder_params
    return {} unless min_access_level?

    { min_access_level: params[:min_access_level] }
  end
end

ProjectsFinder.prepend_if_ee('::EE::ProjectsFinder')