Class: Banzai::Filter::References::AbstractReferenceFilter

Inherits:
ReferenceFilter
  • Object
show all
Includes:
CrossProjectReference, Concerns::PipelineTimingCheck, Concerns::TimeoutFilterHandler
Defined in:
lib/banzai/filter/references/abstract_reference_filter.rb

Overview

Issues, merge requests, Snippets, Commits and Commit Ranges share similar functionality in reference filtering.

Constant Summary

Constants included from Concerns::TimeoutFilterHandler

Concerns::TimeoutFilterHandler::COMPLEX_MARKDOWN_MESSAGE, Concerns::TimeoutFilterHandler::RENDER_TIMEOUT, Concerns::TimeoutFilterHandler::SANITIZATION_RENDER_TIMEOUT

Constants included from Concerns::PipelineTimingCheck

Concerns::PipelineTimingCheck::MAX_PIPELINE_SECONDS

Constants inherited from ReferenceFilter

ReferenceFilter::REFERENCE_TYPE_ATTRIBUTE, ReferenceFilter::REFERENCE_TYPE_DATA_ATTRIBUTE_NAME

Constants included from Concerns::TextReplacer

Concerns::TextReplacer::REFERENCE_PLACEHOLDER, Concerns::TextReplacer::REFERENCE_PLACEHOLDER_PATTERN

Instance Method Summary collapse

Methods included from CrossProjectReference

#parent_from_ref

Methods included from Concerns::PipelineTimingCheck

#exceeded_pipeline_max?

Methods inherited from ReferenceFilter

call, #call_and_update_nodes, #each_node, #group, #nodes, #nodes?, #object_class, #project, #requires_unescaping?

Methods included from Concerns::TextReplacer

#replace_references_in_text_with_html

Methods included from Concerns::HtmlWriter

#write_opening_tag

Methods included from Concerns::OutputSafety

#escape_once

Methods included from RequestStoreReferenceCache

#cached_call, #get_or_set_cache

Constructor Details

#initialize(doc, context = nil, result = nil) ⇒ AbstractReferenceFilter



13
14
15
16
17
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 13

def initialize(doc, context = nil, result = nil)
  super

  @reference_cache = ReferenceCache.new(self, context, result)
end

Instance Method Details

#callObject



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
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 105

def call
  return doc unless project || group || user

  reference_cache.load_reference_cache(nodes) if respond_to?(:parent_records) && nodes.present?

  ref_pattern = object_reference_pattern
  link_pattern = object_class.link_reference_pattern

  # Compile often used regexps only once outside of the loop
  ref_pattern_anchor = /\A#{ref_pattern}\z/
  link_pattern_start = /\A#{link_pattern}/
  link_pattern_anchor = /\A#{link_pattern}\z/

  nodes.each_with_index do |node, index|
    if text_node?(node) && ref_pattern
      replace_node_when_text_matches(node, index, ref_pattern) do |content|
        object_link_filter(content, ref_pattern)
      end

    elsif element_node?(node)
      yield_valid_link(node) do |link, text, inner_html|
        if ref_pattern && link =~ ref_pattern_anchor
          html = object_link_filter(link, ref_pattern_anchor, link_content_html: inner_html)
          replace_node_with_html(node, index, html) if html

          next
        end

        next unless link_pattern

        if link == text && text =~ link_pattern_start
          html = object_link_filter(text, link_pattern_start, link_reference: true)
          replace_node_with_html(node, index, html) if html

          next
        end

        if link_pattern_anchor.match?(link)
          html = object_link_filter(link, link_pattern_anchor, link_content_html: inner_html,
            link_reference: true)
          replace_node_with_html(node, index, html) if html

          next
        end
      end
    end
  end

  doc
end

#data_attributes_for(original, parent, object, link_content: false, link_reference: false) ⇒ Object

“link_content” is true when “original” is the inner HTML content, or false when “original” is HTML-escaped plain text representing the link as written.



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 244

def data_attributes_for(original, parent, object, link_content: false, link_reference: false)
  parent_id = case parent
              when Group
                { group: parent.id, namespace: parent.id }
              when Project
                { project: parent.id }
              when Namespaces::ProjectNamespace
                { namespace: parent.id, project: parent.project.id }
              end

  {
    original: original,
    link: link_content,
    link_reference: link_reference,
    object_sym => object.id
  }.merge(parent_id)
end

#find_object(parent_object, id) ⇒ Object

Implement in child class Example: project.merge_requests.find

Raises:

  • (NotImplementedError)


65
66
67
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 65

def find_object(parent_object, id)
  raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end

#find_object_cached(parent_object, id) ⇒ Object



81
82
83
84
85
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 81

def find_object_cached(parent_object, id)
  cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do
    find_object(parent_object, id)
  end
end

Override if the link reference pattern produces a different ID (global ID vs internal ID, for instance) to the regular reference pattern.



71
72
73
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 71

def find_object_from_link(parent_object, id)
  find_object(parent_object, id)
end


87
88
89
90
91
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 87

def find_object_from_link_cached(parent_object, id)
  cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do
    find_object_from_link(parent_object, id)
  end
end

#from_ref_cached(ref) ⇒ Object



93
94
95
96
97
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 93

def from_ref_cached(ref)
  cached_call(:"banzai_#{parent_type}_refs", ref) do
    parent_from_ref(ref)
  end
end

#identifier(match_data) ⇒ Object



35
36
37
38
39
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 35

def identifier(match_data)
  symbol = symbol_from_match_data(match_data)

  parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
end

Provides the default content of the created link, if there was no original content to use.

Returns a HTML String. If you override this, ensure you return HTML by escaping any text.



265
266
267
268
269
270
271
272
273
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 265

def object_link_content_html(object, matches)
  parent = project || group || user
  html = CGI.escapeHTML(object.reference_link_text(parent))

  extras = object_link_content_html_extras(object, matches)
  html += " (#{extras.join(', ')})" if extras.any?

  html
end

Provides extra content for the default content of the created link, used by the default implementation of #object_link_content_html.

Returns an Array of HTML Strings. If you override this, ensure you return HTML by escaping any text.



279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 279

def object_link_content_html_extras(object, matches)
  extras = []

  if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
    extras << CGI.escapeHTML("comment #{Regexp.last_match(1)}")
  end

  extension = matches[:extension] if matches.names.include?("extension")

  extras << CGI.escapeHTML(extension) if extension

  extras
end

Replace references (like ‘!123` for merge requests) in text with links to the referenced object’s details page.

text - String text to replace references in. pattern - Reference pattern to match against. link_content_html - Original HTML content of the link being replaced. link_reference - True if this was using the link reference pattern,

false otherwise.

Returns String HTML with references replaced with links, or nil if no replacements were made. All links have ‘gfm` and `gfm-OBJECT_NAME` class names attached for styling.

Note carefully:

  • ‘text` is text, not HTML.

  • ‘link_content_html`, if provided, is HTML.

  • The return value is HTML (or nil).



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 174

def object_link_filter(text, pattern, link_content_html: nil, link_reference: false)
  references_in(text, pattern) do |match_text, id, project_ref, namespace_ref, matches|
    parent_path = if parent_type == :group
                    reference_cache.full_group_path(namespace_ref)
                  elsif parent_type == :namespace
                    reference_cache.full_namespace_path(matches)
                  else
                    reference_cache.full_project_path(namespace_ref, project_ref, matches)
                  end

    parent = from_ref_cached(parent_path)
    next unless parent

    object =
      if link_reference
        find_object_from_link_cached(parent, id)
      else
        find_object_cached(parent, id)
      end

    next unless object

    title = object_link_title(object, matches)
    klass = reference_class(object_sym)

    data_attributes = data_attributes_for(
      link_content_html || CGI.escapeHTML(match_text),
      parent,
      object,
      link_content: !!link_content_html,
      link_reference: link_reference
    )
    data_attributes[:reference_format] = matches[:format] if matches.names.include?("format")
    data_attributes.merge!(additional_object_attributes(object))

    data = data_attribute(data_attributes)

    url =
      if matches.names.include?("url") && matches[:url]
        matches[:url]
      else
        url_for_object_cached(object, parent)
      end

    url.chomp!(matches[:format]) if matches.names.include?("format")

    content =
      if context[:link_text]
        CGI.escapeHTML(context[:link_text])
      else
        link_content_html || object_link_content_html(object, matches)
      end

    link = write_opening_tag("a", {
      "href" => url,
      "title" => title,
      "class" => klass,
      **data
    }) << content.to_s << "</a>"

    wrap_link(link, object)
  end
end


293
294
295
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 293

def object_link_title(object, matches)
  object.title
end

#parentObject



301
302
303
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 301

def parent
  parent_type == :project ? project : group
end

#parent_typeObject



297
298
299
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 297

def parent_type
  :project
end

#parse_symbol(symbol, match_data) ⇒ Object

Transform a symbol extracted from the text to a meaningful value In most cases these will be integers, so we call #to_i by default

This method has the contract that if a string ‘ref` refers to a record `record`, then `parse_symbol(ref) == record_identifier(record)`.



51
52
53
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 51

def parse_symbol(symbol, match_data)
  symbol.to_i
end

#record_identifier(record) ⇒ Object

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)`.



59
60
61
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 59

def record_identifier(record)
  record.id
end

#references_in(text, pattern = object_class.reference_pattern) ⇒ Object

See ReferenceFilter#references_in for requirements on the input, block, and return value of this function.

This function yields the String match, the Integer referenced object ID, an optional String of the external project reference, an optional String of the namespace reference, and the full MatchData.



25
26
27
28
29
30
31
32
33
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 25

def references_in(text, pattern = object_class.reference_pattern)
  replace_references_in_text_with_html(Gitlab::Utils::Gsub.gsub_with_limit(text, pattern,
    limit: Banzai::Filter::FILTER_ITEM_LIMIT)) do |match_data|
    if ident = identifier(match_data)
      yield match_data[0], ident, match_data.named_captures['project'],
         match_data.named_captures['namespace'], match_data
    end
  end
end

#symbol_from_match_data(match_data) ⇒ Object



41
42
43
44
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 41

def symbol_from_match_data(match_data)
  key = object_sym
  match_data[key] if match_data.names.include?(key.to_s)
end

#url_for_object(object, parent_object) ⇒ Object

Implement in child class Example: project_merge_request_url

Raises:

  • (NotImplementedError)


77
78
79
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 77

def url_for_object(object, parent_object)
  raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end

#url_for_object_cached(object, parent_object) ⇒ Object



99
100
101
102
103
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 99

def url_for_object_cached(object, parent_object)
  cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do
    url_for_object(object, parent_object)
  end
end


238
239
240
# File 'lib/banzai/filter/references/abstract_reference_filter.rb', line 238

def wrap_link(link, object)
  link
end