summaryrefslogtreecommitdiff
path: root/lib/banzai/filter/references/milestone_reference_filter.rb
blob: 609aaf885ba52d7c8da4c8c1dcd339fa4dcbeb94 (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
153
154
155
# frozen_string_literal: true

module Banzai
  module Filter
    module References
      # HTML filter that replaces milestone references with links.
      class MilestoneReferenceFilter < AbstractReferenceFilter
        self.reference_type = :milestone
        self.object_class   = Milestone

        def parent_records(parent, ids)
          return Milestone.none unless valid_context?(parent)

          milestone_iids = ids.map {|y| y[:milestone_iid]}.compact
          unless milestone_iids.empty?
            iid_relation = find_milestones(parent, true).where(iid: milestone_iids)
          end

          milestone_names = ids.map {|y| y[:milestone_name]}.compact
          unless milestone_names.empty?
            milestone_relation = find_milestones(parent, false).where(name: milestone_names)
          end

          relation = [iid_relation, milestone_relation].compact
          return Milestone.none if relation.all?(Milestone.none)

          Milestone.from_union(relation).includes(:project, :group)
        end

        def find_object(parent_object, id)
          key = reference_cache.records_per_parent[parent_object].keys.find do |k|
            k[:milestone_iid] == id[:milestone_iid] || k[:milestone_name] == id[:milestone_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 milestone_iid
        # or the milestone_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)
          if symbol
            # when parsing links, there is no `match_data[:milestone_iid]`, but `symbol`
            # holds the iid
            { milestone_iid: symbol.to_i, milestone_name: nil }
          else
            { milestone_iid: match_data[:milestone_iid]&.to_i, milestone_name: match_data[:milestone_name]&.tr('"', '') }
          end
        end

        # 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)
          { milestone_iid: record.iid, milestone_name: record.name }
        end

        def valid_context?(parent)
          group_context?(parent) || project_context?(parent)
        end

        def group_context?(parent)
          parent.is_a?(Group)
        end

        def project_context?(parent)
          parent.is_a?(Project)
        end

        def references_in(text, pattern = Milestone.reference_pattern)
          # We'll handle here the references that follow the `reference_pattern`.
          # Other patterns (for example, the link pattern) are handled by the
          # default implementation.
          return super(text, pattern) if pattern != Milestone.reference_pattern

          milestones = {}

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

            if milestone != match
              milestones[index] = milestone
              "#{REFERENCE_PLACEHOLDER}#{index}"
            else
              match
            end
          end

          return text if milestones.empty?

          escape_with_placeholders(unescaped_html, milestones)
        end

        def find_milestones(parent, find_by_iid = false)
          finder_params = milestone_finder_params(parent, find_by_iid)

          MilestonesFinder.new(finder_params).execute
        end

        def milestone_finder_params(parent, find_by_iid)
          { order: nil, state: 'all' }.tap do |params|
            params[:project_ids] = parent.id if project_context?(parent)

            # We don't support IID lookups because IIDs can clash between
            # group/project milestones and group/subgroup milestones.
            params[:group_ids] = group_and_ancestors_ids(parent) unless find_by_iid
          end
        end

        def group_and_ancestors_ids(parent)
          if group_context?(parent)
            parent.self_and_ancestors.select(:id)
          elsif project_context?(parent)
            parent.group&.self_and_ancestors&.select(:id)
          end
        end

        def url_for_object(milestone, project)
          Gitlab::Routing
            .url_helpers
            .milestone_url(milestone, only_path: context[:only_path])
        end

        def object_link_text(object, matches)
          milestone_link = escape_once(super)
          reference = object.project&.to_reference_base(project)

          if reference.present?
            "#{milestone_link} <i>in #{reference}</i>".html_safe
          else
            milestone_link
          end
        end

        def object_link_title(object, matches)
          nil
        end

        def parent
          project || group
        end

        def requires_unescaping?
          true
        end
      end
    end
  end
end