Class: Snippet

Direct Known Subclasses

PersonalSnippet, ProjectSnippet

Constant Summary collapse

MAX_FILE_COUNT =
10

Constants included from CanMoveRepositoryStorage

CanMoveRepositoryStorage::RepositoryReadOnlyError

Constants included from Gitlab::SQL::Pattern

Gitlab::SQL::Pattern::MIN_CHARS_FOR_PARTIAL_MATCHING, Gitlab::SQL::Pattern::REGEX_QUOTED_WORD

Constants included from Noteable

Noteable::MAX_NOTES_LIMIT

Constants included from CacheMarkdownField

CacheMarkdownField::INVALIDATED_BY

Constants included from Redactable

Redactable::UNSUBSCRIBE_PATTERN

Constants included from Gitlab::VisibilityLevel

Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::PUBLIC

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Instance Attribute Summary

Attributes included from Noteable

#system_note_timestamp

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Utils::Override

extended, extensions, included, method_added, override, prepended, queue_verification, verify!

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from CanMoveRepositoryStorage

#git_transfer_in_progress?, #reference_counter, #set_repository_read_only!, #set_repository_writable!

Methods included from HasRepository

#after_change_head_branch_does_not_exist, #after_repository_change_head, #commit, #commit_by, #commits_by, #default_branch_from_group_preferences, #default_branch_from_preferences, #empty_repo?, #http_url_to_repo, #lfs_enabled?, #lfs_http_url_to_repo, #reload_default_branch, #repo_exists?, #repository_exists?, #root_ref?, #ssh_url_to_repo, #url_to_repo, #valid_repo?, #web_url

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods included from Gitlab::ShellAdapter

#gitlab_shell

Methods included from Referable

#referable_inspect, #reference_link_text, #to_reference_base

Methods included from Editable

#edited?, #last_edited_by

Methods included from Spammable

#clear_spam_flags!, #invalidate_if_spam, #needs_recaptcha!, #recaptcha_error!, #render_recaptcha?, #spam, #spam!, #spam_description, #spam_title, #spammable_text, #submittable_as_spam?, #submittable_as_spam_by?, #unrecoverable_spam_error!

Methods included from Mentionable

#all_references, #create_cross_references!, #create_new_cross_references!, #directly_addressed_users, #extractors, #gfm_reference, #local_reference, #matches_cross_reference_regex?, #mentioned_users, #referenced_group_users, #referenced_groups, #referenced_mentionables, #referenced_project_users, #referenced_projects, #referenced_users, #user_mention_class, #user_mention_identifier

Methods included from Awardable

#awarded_emoji?, #downvotes, #emoji_awardable?, #grouped_awards, #upvotes, #user_authored?, #user_can_award?

Methods included from Participable

#participant?, #participants, #visible_participants

Methods included from Noteable

#after_note_created, #after_note_destroyed, #base_class_name, #capped_notes_count, #creatable_note_email_address, #discussion_ids_relation, #discussion_notes, #discussion_root_note_ids, #discussions, #discussions_can_be_resolved_by?, #discussions_rendered_on_frontend?, #discussions_resolvable?, #discussions_resolved?, #discussions_to_be_resolved, #etag_caching_enabled?, #expire_note_etag_cache, #grouped_diff_discussions, #has_any_diff_note_positions?, #human_class_name, #lockable?, #note_etag_key, #noteable_target_type_name, #preloads_discussion_diff_highlighting?, #resolvable_discussions, #supports_creating_notes_by_email?, #supports_discussions?, #supports_replying_to_individual_notes?, #supports_resolvable_notes?, #supports_suggestion?

Methods included from CacheMarkdownField

#attribute_invalidated?, #banzai_render_context, #cached_html_for, #cached_html_up_to_date?, #invalidated_markdown_cache?, #latest_cached_markdown_version, #local_version, #mentionable_attributes_changed?, #parent_user, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #skip_project_check?, #store_mentions!, #updated_cached_html_for

Methods included from Gitlab::VisibilityLevel

allowed_for?, allowed_level?, allowed_levels, closest_allowed_level, #internal?, level_name, level_value, levels_for_user, non_restricted_level?, options, #private?, #public?, public_visibility_restricted?, restricted_level?, string_level, string_options, string_values, valid_level?, #visibility, #visibility=, #visibility_attribute_present?, #visibility_attribute_value, #visibility_level_attributes, #visibility_level_previous_changes, #visibility_level_value

Methods inherited from ApplicationRecord

cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from SensitiveSerializableHash

#serializable_hash

Constructor Details

#initialize(attributes = {}) ⇒ Snippet

Returns a new instance of Snippet.


213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'app/models/snippet.rb', line 213

def initialize(attributes = {})
  # We can't use default_value_for because the database has a default
  # value of 0 for visibility_level. If someone attempts to create a
  # private snippet, default_value_for will assume that the
  # visibility_level hasn't changed and will use the application
  # setting default, which could be internal or public.
  #
  # To fix the problem, we assign the actual snippet default if no
  # explicit visibility has been initialized.
  attributes ||= {}

  unless visibility_attribute_present?(attributes)
    attributes[:visibility_level] = Gitlab::CurrentSettings.default_snippet_visibility
  end

  super
end

Class Method Details

.find_by_id_and_project(id:, project:) ⇒ Object


197
198
199
# File 'app/models/snippet.rb', line 197

def find_by_id_and_project(id:, project:)
  Snippet.find_by(id: id, project: project)
end

.find_by_project_title_trunc_created_at(project, title, created_at) ⇒ Object


201
202
203
204
205
206
# File 'app/models/snippet.rb', line 201

def find_by_project_title_trunc_created_at(project, title, created_at)
  where(project: project, title: title)
    .find_by(
      "date_trunc('second', created_at at time zone :tz) at time zone :tz = :created_at",
      tz: created_at.zone, created_at: created_at)
end

.for_project_with_user(project, user = nil) ⇒ Object


164
165
166
167
168
169
170
171
172
# File 'app/models/snippet.rb', line 164

def for_project_with_user(project, user = nil)
  return none unless project.snippets_visible?(user)

  if user && project.team.member?(user)
    project.snippets
  else
    project.snippets.public_to_user(user)
  end
end

193
194
195
# File 'app/models/snippet.rb', line 193

def link_reference_pattern
  @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
end

.max_file_limitObject


208
209
210
# File 'app/models/snippet.rb', line 208

def max_file_limit
  MAX_FILE_COUNT
end

.only_include_authorized_projects(current_user) ⇒ Object


154
155
156
157
158
159
160
161
162
# File 'app/models/snippet.rb', line 154

def only_include_authorized_projects(current_user)
  where(
    'EXISTS (?)',
    ProjectAuthorization
      .select(1)
      .where('project_id = snippets.project_id')
      .where(user_id: current_user.id)
  )
end

.only_include_projects_visible_to(current_user = nil) ⇒ Object


138
139
140
141
142
# File 'app/models/snippet.rb', line 138

def only_include_projects_visible_to(current_user = nil)
  levels = Gitlab::VisibilityLevel.levels_for_user(current_user)

  joins(:project).where(projects: { visibility_level: levels })
end

.only_include_projects_with_snippets_enabled(include_private: false) ⇒ Object


144
145
146
147
148
149
150
151
152
# File 'app/models/snippet.rb', line 144

def only_include_projects_with_snippets_enabled(include_private: false)
  column = ProjectFeature.access_level_attribute(:snippets)
  levels = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]

  levels << ProjectFeature::PRIVATE if include_private

  joins(project: :project_feature)
    .where(project_features: { column => levels })
end

.only_personal_snippetsObject


130
131
132
# File 'app/models/snippet.rb', line 130

def only_personal_snippets
  where(project_id: nil)
end

.only_project_snippetsObject


134
135
136
# File 'app/models/snippet.rb', line 134

def only_project_snippets
  where.not(project_id: nil)
end

.parent_classObject


114
115
116
# File 'app/models/snippet.rb', line 114

def parent_class
  ::Project
end

.reference_patternObject

Pattern used to extract `$123` snippet references from text

This pattern supports cross-project references.


186
187
188
189
190
191
# File 'app/models/snippet.rb', line 186

def reference_pattern
  @reference_pattern ||= %r{
  (#{Project.reference_pattern})?
  #{Regexp.escape(reference_prefix)}(?<snippet>\d+)
}x
end

.reference_prefixObject


179
180
181
# File 'app/models/snippet.rb', line 179

def reference_prefix
  '$'
end

.sanitized_file_name(file_name) ⇒ Object


118
119
120
# File 'app/models/snippet.rb', line 118

def sanitized_file_name(file_name)
  file_name.gsub(/[^a-zA-Z0-9_\-\.]+/, '')
end

.search(query) ⇒ Object

Searches for snippets with a matching title, description or file name.

This method uses ILIKE on PostgreSQL.

query - The search query as a String.

Returns an ActiveRecord::Relation.


110
111
112
# File 'app/models/snippet.rb', line 110

def search(query)
  fuzzy_search(query, [:title, :description, :file_name])
end

.visible_to_or_authored_by(user) ⇒ Object


174
175
176
177
# File 'app/models/snippet.rb', line 174

def visible_to_or_authored_by(user)
  query = where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(user))
  query.or(where(author_id: user.id))
end

.with_optional_visibility(value = nil) ⇒ Object


122
123
124
125
126
127
128
# File 'app/models/snippet.rb', line 122

def with_optional_visibility(value = nil)
  if value
    where(visibility_level: value)
  else
    all
  end
end

Instance Method Details

#all_filesObject


241
242
243
# File 'app/models/snippet.rb', line 241

def all_files
  list_files(default_branch)
end

#allow_possible_spam?Boolean

Returns:

  • (Boolean)

285
286
287
# File 'app/models/snippet.rb', line 285

def allow_possible_spam?
  false
end

#as_json(options = {}) ⇒ Object


303
304
305
306
307
308
# File 'app/models/snippet.rb', line 303

def as_json(options = {})
  options[:except] = Array.wrap(options[:except])
  options[:except] << :secret_token

  super
end

#blobObject


245
246
247
# File 'app/models/snippet.rb', line 245

def blob
  @blob ||= Blob.decorate(SnippetBlob.new(self), self)
end

#blobs(paths = []) ⇒ Object


249
250
251
252
253
254
255
256
# File 'app/models/snippet.rb', line 249

def blobs(paths = [])
  return [] unless repository_exists?

  paths = all_files if paths.empty?
  items = paths.map { |path| [default_branch, path] }

  repository.blobs_at(items).compact
end

#can_cache_field?(field) ⇒ Boolean

Returns:

  • (Boolean)

366
367
368
# File 'app/models/snippet.rb', line 366

def can_cache_field?(field)
  field != :content || MarkupHelper.gitlab_markdown?(file_name)
end

#check_for_spam?(user:) ⇒ Boolean

Returns:

  • (Boolean)

278
279
280
281
# File 'app/models/snippet.rb', line 278

def check_for_spam?(user:)
  visibility_level_changed?(to: Snippet::PUBLIC) ||
    (public? && (title_changed? || content_changed?))
end

#content_html_invalidated?Boolean

Returns:

  • (Boolean)

37
38
39
# File 'app/models/snippet.rb', line 37

def content_html_invalidated?
  default_content_html_invalidator || file_name_changed?
end

#create_repositoryObject


354
355
356
357
358
359
# File 'app/models/snippet.rb', line 354

def create_repository
  return if repository_exists? && snippet_repository

  repository.create_if_not_exists(default_branch)
  track_snippet_repository(repository.storage)
end

#default_branchObject


346
347
348
# File 'app/models/snippet.rb', line 346

def default_branch
  super || Gitlab::DefaultBranch.value(object: project)
end

#default_content_html_invalidatorObject

If file_name changes, it invalidates content


36
# File 'app/models/snippet.rb', line 36

alias_method :default_content_html_invalidator, :content_html_invalidated?

#embeddable?Boolean

Returns:

  • (Boolean)

270
271
272
# File 'app/models/snippet.rb', line 270

def embeddable?
  Ability.allowed?(nil, :read_snippet, self)
end

#file_nameObject


262
263
264
# File 'app/models/snippet.rb', line 262

def file_name
  super.to_s
end

#file_name_on_repoObject


374
375
376
377
378
# File 'app/models/snippet.rb', line 374

def file_name_on_repo
  return if repository.empty?

  list_files(default_branch).first
end

#full_pathObject


333
334
335
336
337
338
339
340
341
342
343
# File 'app/models/snippet.rb', line 333

def full_path
  return unless persisted?

  @full_path ||= begin
    components = []
    components << project.full_path if project_id?
    components << 'snippets'
    components << self.id
    components.join('/')
  end
end

#hexdigestObject


370
371
372
# File 'app/models/snippet.rb', line 370

def hexdigest
  Digest::SHA256.hexdigest("#{title}#{description}#{created_at}#{updated_at}")
end

#hook_attrsObject


258
259
260
# File 'app/models/snippet.rb', line 258

def hook_attrs
  attributes
end

#list_files(ref = nil) ⇒ Object


380
381
382
383
384
# File 'app/models/snippet.rb', line 380

def list_files(ref = nil)
  return [] if repository.empty?

  repository.ls_files(ref || default_branch)
end

#multiple_files?Boolean

Returns:

  • (Boolean)

386
387
388
# File 'app/models/snippet.rb', line 386

def multiple_files?
  list_files.size > 1
end

#notes_with_associationsObject


274
275
276
# File 'app/models/snippet.rb', line 274

def notes_with_associations
  notes.includes(:author)
end

#repositoryObject


311
312
313
# File 'app/models/snippet.rb', line 311

def repository
  @repository ||= Gitlab::GlRepository::SNIPPET.repository_for(self)
end

#repository_size_checkerObject


316
317
318
319
320
321
322
323
324
# File 'app/models/snippet.rb', line 316

def repository_size_checker
  strong_memoize(:repository_size_checker) do
    ::Gitlab::RepositorySizeChecker.new(
      current_size_proc: -> { repository.size.megabytes },
      limit: Gitlab::CurrentSettings.snippet_size_limit,
      namespace: nil
    )
  end
end

#repository_storageObject


350
351
352
# File 'app/models/snippet.rb', line 350

def repository_storage
  snippet_repository&.shard_name || Repository.pick_storage_shard
end

#spammable_entity_typeObject


289
290
291
# File 'app/models/snippet.rb', line 289

def spammable_entity_type
  'snippet'
end

#storageObject


327
328
329
# File 'app/models/snippet.rb', line 327

def storage
  @storage ||= Storage::Hashed.new(self, prefix: Storage::Hashed::SNIPPET_REPOSITORY_PATH_PREFIX)
end

#to_ability_nameObject


293
294
295
# File 'app/models/snippet.rb', line 293

def to_ability_name
  'snippet'
end

#to_reference(from = nil, full: false) ⇒ Object


231
232
233
234
235
236
237
238
239
# File 'app/models/snippet.rb', line 231

def to_reference(from = nil, full: false)
  reference = "#{self.class.reference_prefix}#{id}"

  if project.present?
    "#{project.to_reference_base(from, full: full)}#{reference}"
  else
    reference
  end
end

#track_snippet_repository(shard) ⇒ Object


361
362
363
364
# File 'app/models/snippet.rb', line 361

def track_snippet_repository(shard)
  snippet_repo = snippet_repository || build_snippet_repository
  snippet_repo.update!(shard_name: shard, disk_path: disk_path)
end

#valid_secret_token?(token) ⇒ Boolean

Returns:

  • (Boolean)

297
298
299
300
301
# File 'app/models/snippet.rb', line 297

def valid_secret_token?(token)
  return false unless token && secret_token

  ActiveSupport::SecurityUtils.secure_compare(token.to_s, secret_token.to_s)
end

#visibility_level_fieldObject


266
267
268
# File 'app/models/snippet.rb', line 266

def visibility_level_field
  :visibility_level
end