summaryrefslogtreecommitdiff
path: root/lib/banzai/filter/references/label_reference_filter.rb
blob: 3ae9c5f8d90a447adc9623ba211030ac2805b2cd (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
# frozen_string_literal: true

module Banzai
  module Filter
    module References
      # HTML filter that replaces label references with links.
      class LabelReferenceFilter < AbstractReferenceFilter
        self.reference_type = :label
        self.object_class   = Label

        def parent_records(parent, ids)
          return Label.none unless parent.is_a?(Project) || parent.is_a?(Group)

          labels = find_labels(parent)
          label_ids = ids.map {|y| y[:label_id]}.compact

          unless label_ids.empty?
            id_relation = labels.where(id: label_ids)
          end

          label_names = ids.map {|y| y[:label_name]}.compact
          unless label_names.empty?
            label_relation = labels.where(title: label_names)
          end

          return Label.none if (relation = [id_relation, label_relation].compact).empty?

          Label.from_union(relation)
        end

        def find_object(parent_object, id)
          key = reference_cache.records_per_parent[parent_object].keys.find do |k|
            k[:label_id] == id[:label_id] || k[:label_name] == id[:label_name]
          end

          reference_cache.records_per_parent[parent_object][key] if key
        end

        # Transform a symbol extracted from the text to a meaningful value
        #
        # This method has the contract that if a string `ref` refers to a
        # record `record`, then `parse_symbol(ref) == record_identifier(record)`.
        #
        # This contract is slightly broken here, as we only have either the label_id
        # or the label_name, but not both.  But below, we have both pieces of information.
        # But it's accounted for in `find_object`
        def parse_symbol(symbol, match_data)
          { label_id: match_data[:label_id]&.to_i, label_name: match_data[:label_name]&.tr('"', '') }
        end

        # We assume that most classes are identifying records by ID.
        #
        # This method has the contract that if a string `ref` refers to a
        # record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
        # See note in `parse_symbol` above
        def record_identifier(record)
          { label_id: record.id, label_name: record.title }
        end

        def references_in(text, pattern = Label.reference_pattern)
          labels = {}

          unescaped_html = unescape_html_entities(text).gsub(pattern).with_index do |match, index|
            ident = identifier($~)
            label = yield match, ident, $~[:project], $~[:namespace], $~

            if label != match
              labels[index] = label
              "#{REFERENCE_PLACEHOLDER}#{index}"
            else
              match
            end
          end

          return text if labels.empty?

          escape_with_placeholders(unescaped_html, labels)
        end

        def find_labels(parent)
          params = if parent.is_a?(Group)
                     { group_id: parent.id,
                       include_ancestor_groups: true,
                       only_group_labels: true }
                   else
                     { project: parent,
                       include_ancestor_groups: true }
                   end

          LabelsFinder.new(nil, params).execute(skip_authorization: true)
        end

        def url_for_object(label, parent)
          label_url_method =
            if context[:label_url_method]
              context[:label_url_method]
            elsif parent.is_a?(Project)
              :project_issues_url
            end

          return unless label_url_method

          Gitlab::Routing.url_helpers.public_send(label_url_method, parent, label_name: label.name, only_path: context[:only_path]) # rubocop:disable GitlabSecurity/PublicSend
        end

        def object_link_text(object, matches)
          label_suffix = ''
          parent = project || group

          if project || full_path_ref?(matches)
            project_path    = reference_cache.full_project_path(matches[:namespace], matches[:project])
            parent_from_ref = from_ref_cached(project_path)
            reference       = parent_from_ref.to_human_reference(parent)

            label_suffix = " <i>in #{ERB::Util.html_escape(reference)}</i>" if reference.present?
          end

          presenter = object.present(issuable_subject: parent)
          LabelsHelper.render_colored_label(presenter, suffix: label_suffix)
        end

        def wrap_link(link, label)
          presenter = label.present(issuable_subject: project || group)
          LabelsHelper.wrap_label_html(link, small: true, label: presenter)
        end

        def full_path_ref?(matches)
          matches[:namespace] && matches[:project]
        end

        def reference_class(type, tooltip: true)
          super + ' gl-link gl-label-link'
        end

        def object_link_title(object, matches)
          presenter = object.present(issuable_subject: project || group)
          LabelsHelper.label_tooltip_title(presenter)
        end

        def parent
          project || group
        end

        def requires_unescaping?
          true
        end
      end
    end
  end
end

Banzai::Filter::References::LabelReferenceFilter.prepend_mod_with('Banzai::Filter::References::LabelReferenceFilter')