snippet.rb 8.9 KB
Newer Older
1 2
# frozen_string_literal: true

3
class Snippet < ApplicationRecord
V
Valery Sizov 已提交
4
  include Gitlab::VisibilityLevel
5
  include Redactable
6
  include CacheMarkdownField
7
  include Noteable
8
  include Participable
9
  include Sortable
10
  include Awardable
11
  include Mentionable
S
Sean McGivern 已提交
12
  include Spammable
13
  include Editable
14
  include Gitlab::SQL::Pattern
15
  include FromUnion
16
  include IgnorableColumns
17
  include HasRepository
18
  extend ::Gitlab::Utils::Override
G
gitlabhq 已提交
19

20
  ignore_column :storage_version, remove_with: '12.9', remove_after: '2020-03-22'
21
  ignore_column :repository_storage, remove_with: '12.10', remove_after: '2020-04-22'
22

23
  cache_markdown_field :title, pipeline: :single_line
24
  cache_markdown_field :description
25 26
  cache_markdown_field :content

27 28
  redact_field :description

B
blackst0ne 已提交
29
  # Aliases to make application_helper#edited_time_ago_with_tooltip helper work properly with snippets.
30
  # See https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/10392/diffs#note_28719102
B
blackst0ne 已提交
31 32 33
  alias_attribute :last_edited_at, :updated_at
  alias_attribute :last_edited_by, :updated_by

34 35 36 37 38 39
  # If file_name changes, it invalidates content
  alias_method :default_content_html_invalidator, :content_html_invalidated?
  def content_html_invalidated?
    default_content_html_invalidator || file_name_changed?
  end

R
Robert Speicher 已提交
40 41
  belongs_to :author, class_name: 'User'
  belongs_to :project
A
Andrew8xx8 已提交
42

43
  has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
44
  has_many :user_mentions, class_name: "SnippetUserMention"
45
  has_one :snippet_repository, inverse_of: :snippet
G
gitlabhq 已提交
46

D
Dmitriy Zaporozhets 已提交
47
  delegate :name, :email, to: :author, prefix: true, allow_nil: true
G
gitlabhq 已提交
48

A
Andrey Kumanyaev 已提交
49
  validates :author, presence: true
50
  validates :title, presence: true, length: { maximum: 255 }
51
  validates :file_name,
52
    length: { maximum: 255 }
53

V
Valeriy Sizov 已提交
54
  validates :content, presence: true
55 56 57 58 59 60 61 62 63 64 65 66
  validates :content,
            length: {
              maximum: ->(_) { Gitlab::CurrentSettings.snippet_size_limit },
              message: -> (_, data) do
                current_value = ActiveSupport::NumberHelper.number_to_human_size(data[:value].size)
                max_size = ActiveSupport::NumberHelper.number_to_human_size(Gitlab::CurrentSettings.snippet_size_limit)

                _("is too long (%{current_value}). The maximum size is %{max_size}.") % { current_value: current_value, max_size: max_size }
              end
            },
            if: :content_changed?

V
Valery Sizov 已提交
67
  validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
G
gitlabhq 已提交
68

A
Andrey Kumanyaev 已提交
69
  # Scopes
70
  scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) }
V
Valery Sizov 已提交
71
  scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) }
72 73
  scope :are_public, -> { public_only }
  scope :are_secret, -> { public_only.where(secret: true) }
74
  scope :fresh, -> { order("created_at DESC") }
75
  scope :inc_author, -> { includes(:author) }
76
  scope :inc_relations_for_view, -> { includes(author: :status) }
N
Nihad Abbasov 已提交
77

78 79
  attr_mentionable :description

Y
Yorick Peterse 已提交
80 81
  participant :author
  participant :notes_with_associations
82

S
Sean McGivern 已提交
83 84 85
  attr_spammable :title, spam_title: true
  attr_spammable :content, spam_description: true

86 87 88 89 90
  attr_encrypted :secret_token,
    key:       Settings.attr_encrypted_db_key_base_truncated,
    mode:      :per_attribute_iv,
    algorithm: 'aes-256-cbc'

91 92 93 94 95 96 97 98
  def self.with_optional_visibility(value = nil)
    if value
      where(visibility_level: value)
    else
      all
    end
  end

99
  def self.only_personal_snippets
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
    where(project_id: nil)
  end

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

    joins(:project).where('projects.visibility_level IN (?)', levels)
  end

  def self.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

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

  def self.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

  def self.visible_to_or_authored_by(user)
140 141
    query = where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(user))
    query.or(where(author_id: user.id))
142 143
  end

144 145 146 147
  def self.reference_prefix
    '$'
  end

148 149 150 151
  # Pattern used to extract `$123` snippet references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
152
    @reference_pattern ||= %r{
153 154
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<snippet>\d+)
155 156 157
    }x
  end

158
  def self.link_reference_pattern
159
    @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
160 161
  end

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
  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

J
Jarka Kadlecova 已提交
180
  def to_reference(from = nil, full: false)
181 182
    reference = "#{self.class.reference_prefix}#{id}"

183
    if project.present?
184
      "#{project.to_reference_base(from, full: full)}#{reference}"
185 186
    else
      reference
187 188 189
    end
  end

G
gitlabhq 已提交
190
  def self.content_types
N
Nihad Abbasov 已提交
191
    [
G
gitlabhq 已提交
192 193 194 195 196
      ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java",
      ".haml", ".html", ".sass", ".scss", ".xml", ".php", ".erb",
      ".js", ".sh", ".coffee", ".yml", ".md"
    ]
  end
G
gitlabhq 已提交
197

D
Douwe Maan 已提交
198 199
  def blob
    @blob ||= Blob.decorate(SnippetBlob.new(self), nil)
200 201
  end

202 203 204 205
  def hook_attrs
    attributes
  end

206 207 208 209
  def file_name
    super.to_s
  end

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

V
Valery Sizov 已提交
214
  def visibility_level_field
215
    :visibility_level
216
  end
V
Valery Sizov 已提交
217

218
  def embeddable?
219
    Ability.allowed?(nil, :read_snippet, self)
220 221
  end

Y
Yorick Peterse 已提交
222
  def notes_with_associations
223
    notes.includes(:author)
Y
Yorick Peterse 已提交
224 225
  end

S
Sean McGivern 已提交
226
  def check_for_spam?
227 228
    visibility_level_changed?(to: Snippet::PUBLIC) ||
      (public? && (title_changed? || content_changed?))
S
Sean McGivern 已提交
229 230
  end

231
  # snippets are the biggest sources of spam
232 233 234 235 236
  override :allow_possible_spam?
  def allow_possible_spam?
    false
  end

S
Sean McGivern 已提交
237 238 239 240
  def spammable_entity_type
    'snippet'
  end

241
  def to_ability_name
242
    'snippet'
243 244
  end

245 246 247 248 249 250 251 252 253 254 255 256 257
  def valid_secret_token?(token)
    return false unless token && secret_token

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

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

    super
  end

258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
  def repository
    @repository ||= Repository.new(full_path, self, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET)
  end

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

  # This is the full_path used to identify the
  # the snippet repository. It will be used mostly
  # for logging purposes.
  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

  def repository_storage
    snippet_repository&.shard_name ||
      Gitlab::CurrentSettings.pick_repository_storage
  end

  def create_repository
    return if repository_exists?

    repository.create_if_not_exists

    track_snippet_repository if repository_exists?
  end

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

299
  class << self
300 301 302 303 304 305 306
    # Searches for snippets with a matching title or file name.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
    # query - The search query as a String.
    #
    # Returns an ActiveRecord::Relation.
307
    def search(query)
308
      fuzzy_search(query, [:title, :file_name])
309 310
    end

311 312 313 314 315 316 317
    # Searches for snippets with matching content.
    #
    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
    #
    # query - The search query as a String.
    #
    # Returns an ActiveRecord::Relation.
318
    def search_code(query)
319
      fuzzy_search(query, [:content])
320
    end
J
Jan Provaznik 已提交
321 322 323 324

    def parent_class
      ::Project
    end
325
  end
G
gitlabhq 已提交
326
end
327 328

Snippet.prepend_if_ee('EE::Snippet')