snippet.rb 7.6 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 10
  include Referable
  include Sortable
11
  include Awardable
12
  include Mentionable
S
Sean McGivern 已提交
13
  include Spammable
14
  include Editable
15
  include Gitlab::SQL::Pattern
16
  include FromUnion
17
  extend ::Gitlab::Utils::Override
G
gitlabhq 已提交
18

19
  cache_markdown_field :title, pipeline: :single_line
20
  cache_markdown_field :description
21 22
  cache_markdown_field :content

23 24
  redact_field :description

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

30 31 32 33 34 35
  # 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 已提交
36 37
  belongs_to :author, class_name: 'User'
  belongs_to :project
A
Andrew8xx8 已提交
38

39
  has_many :notes, as: :noteable, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
G
gitlabhq 已提交
40

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

A
Andrey Kumanyaev 已提交
43
  validates :author, presence: true
44
  validates :title, presence: true, length: { maximum: 255 }
45
  validates :file_name,
46
    length: { maximum: 255 }
47

V
Valeriy Sizov 已提交
48
  validates :content, presence: true
49 50 51 52 53 54 55 56 57 58 59 60
  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 已提交
61
  validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
G
gitlabhq 已提交
62

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

Y
Yorick Peterse 已提交
72 73
  participant :author
  participant :notes_with_associations
74

S
Sean McGivern 已提交
75 76 77
  attr_spammable :title, spam_title: true
  attr_spammable :content, spam_description: true

78 79 80 81 82
  attr_encrypted :secret_token,
    key:       Settings.attr_encrypted_db_key_base_truncated,
    mode:      :per_attribute_iv,
    algorithm: 'aes-256-cbc'

83 84 85 86 87 88 89 90
  def self.with_optional_visibility(value = nil)
    if value
      where(visibility_level: value)
    else
      all
    end
  end

91
  def self.only_personal_snippets
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
    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)
132 133
    query = where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(user))
    query.or(where(author_id: user.id))
134 135
  end

136 137 138 139
  def self.reference_prefix
    '$'
  end

140 141 142 143
  # Pattern used to extract `$123` snippet references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
144
    @reference_pattern ||= %r{
145 146
      (#{Project.reference_pattern})?
      #{Regexp.escape(reference_prefix)}(?<snippet>\d+)
147 148 149
    }x
  end

150
  def self.link_reference_pattern
151
    @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/)
152 153
  end

154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
  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 已提交
172
  def to_reference(from = nil, full: false)
173 174
    reference = "#{self.class.reference_prefix}#{id}"

175
    if project.present?
J
Jarka Kadlecova 已提交
176
      "#{project.to_reference(from, full: full)}#{reference}"
177 178
    else
      reference
179 180 181
    end
  end

G
gitlabhq 已提交
182
  def self.content_types
N
Nihad Abbasov 已提交
183
    [
G
gitlabhq 已提交
184 185 186 187 188
      ".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java",
      ".haml", ".html", ".sass", ".scss", ".xml", ".php", ".erb",
      ".js", ".sh", ".coffee", ".yml", ".md"
    ]
  end
G
gitlabhq 已提交
189

D
Douwe Maan 已提交
190 191
  def blob
    @blob ||= Blob.decorate(SnippetBlob.new(self), nil)
192 193
  end

194 195 196 197
  def hook_attrs
    attributes
  end

198 199 200 201
  def file_name
    super.to_s
  end

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

V
Valery Sizov 已提交
206
  def visibility_level_field
207
    :visibility_level
208
  end
V
Valery Sizov 已提交
209

210
  def embeddable?
211 212 213
    ability = project_id? ? :read_project_snippet : :read_personal_snippet

    Ability.allowed?(nil, ability, self)
214 215
  end

Y
Yorick Peterse 已提交
216
  def notes_with_associations
217
    notes.includes(:author)
Y
Yorick Peterse 已提交
218 219
  end

S
Sean McGivern 已提交
220
  def check_for_spam?
221 222
    visibility_level_changed?(to: Snippet::PUBLIC) ||
      (public? && (title_changed? || content_changed?))
S
Sean McGivern 已提交
223 224
  end

225 226 227 228 229 230
  # snippers are the biggest sources of spam
  override :allow_possible_spam?
  def allow_possible_spam?
    false
  end

S
Sean McGivern 已提交
231 232 233 234
  def spammable_entity_type
    'snippet'
  end

235 236 237 238
  def to_ability_name
    model_name.singular
  end

239 240 241 242 243 244 245 246 247 248 249 250 251
  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

252
  class << self
253 254 255 256 257 258 259
    # 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.
260
    def search(query)
261
      fuzzy_search(query, [:title, :file_name])
262 263
    end

264 265 266 267 268 269 270
    # 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.
271
    def search_code(query)
272
      fuzzy_search(query, [:content])
273
    end
J
Jan Provaznik 已提交
274 275 276 277

    def parent_class
      ::Project
    end
278
  end
G
gitlabhq 已提交
279
end
280 281

Snippet.prepend_if_ee('EE::Snippet')