summaryrefslogtreecommitdiff
path: root/lib/gitlab/highlight.rb
blob: afe1554aec1af60f8b69b0cda1218afcf726ae18 (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
# frozen_string_literal: true

module Gitlab
  class Highlight
    TIMEOUT_BACKGROUND = 30.seconds
    TIMEOUT_FOREGROUND = 1.5.seconds

    def self.highlight(blob_name, blob_content, language: nil, plain: false)
      new(blob_name, blob_content, language: language)
        .highlight(blob_content, continue: false, plain: plain)
    end

    def self.too_large?(size)
      return false unless size.to_i > self.file_size_limit

      over_highlight_size_limit.increment(source: "file size: #{self.file_size_limit}") if Feature.enabled?(:track_file_size_over_highlight_limit)

      true
    end

    attr_reader :blob_name

    def initialize(blob_name, blob_content, language: nil)
      @formatter = Rouge::Formatters::HTMLGitlab
      @language = language
      @blob_name = blob_name
      @blob_content = blob_content
    end

    def highlight(text, continue: false, plain: false, context: {})
      @context = context

      plain ||= self.class.too_large?(text.length)

      highlighted_text = highlight_text(text, continue: continue, plain: plain)
      highlighted_text = link_dependencies(text, highlighted_text) if blob_name
      highlighted_text
    end

    def lexer
      @lexer ||= custom_language || begin
        Rouge::Lexer.guess(filename: @blob_name, source: @blob_content).new
      rescue Rouge::Guesser::Ambiguous => e
        e.alternatives.min_by(&:tag)
      end
    end

    private

    attr_reader :context

    def self.file_size_limit
      if Feature.enabled?(:one_megabyte_file_size_limit)
        1024.kilobytes
      else
        Gitlab.config.extra['maximum_text_highlight_size_kilobytes']
      end
    end

    private_class_method :file_size_limit

    def custom_language
      return unless @language

      Rouge::Lexer.find_fancy(@language)
    end

    def highlight_text(text, continue: true, plain: false)
      if plain
        highlight_plain(text)
      else
        highlight_rich(text, continue: continue)
      end
    end

    def highlight_plain(text)
      @formatter.format(Rouge::Lexers::PlainText.lex(text), context).html_safe
    end

    def highlight_rich(text, continue: true)
      add_highlight_attempt_metric

      tag = lexer.tag
      tokens = lexer.lex(text, continue: continue)
      Timeout.timeout(timeout_time) { @formatter.format(tokens, context.merge(tag: tag)).html_safe }
    rescue Timeout::Error => e
      add_highlight_timeout_metric

      Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
      highlight_plain(text)
    rescue StandardError
      highlight_plain(text)
    end

    def timeout_time
      Gitlab::Runtime.sidekiq? ? TIMEOUT_BACKGROUND : TIMEOUT_FOREGROUND
    end

    def link_dependencies(text, highlighted_text)
      Gitlab::DependencyLinker.link(blob_name, text, highlighted_text)
    end

    def add_highlight_attempt_metric
      return unless Feature.enabled?(:track_highlight_timeouts)

      highlighting_attempt.increment(source: (@language || "undefined"))
    end

    def add_highlight_timeout_metric
      return unless Feature.enabled?(:track_highlight_timeouts)

      highlight_timeout.increment(source: Gitlab::Runtime.sidekiq? ? "background" : "foreground")
    end

    def highlighting_attempt
      @highlight_attempt ||= Gitlab::Metrics.counter(
        :file_highlighting_attempt,
        'Counts the times highlighting has been attempted on a file'
      )
    end

    def highlight_timeout
      @highlight_timeout ||= Gitlab::Metrics.counter(
        :highlight_timeout,
        'Counts the times highlights have timed out'
      )
    end

    def self.over_highlight_size_limit
      @over_highlight_size_limit ||= Gitlab::Metrics.counter(
        :over_highlight_size_limit,
        'Count the times files have been over the highlight size limit'
      )
    end
  end
end