2021-04-15 18:09:01 +00:00
|
|
|
# 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
|
2021-04-22 12:09:49 +00:00
|
|
|
self.object_class = Label
|
2021-04-15 18:09:01 +00:00
|
|
|
|
2021-06-10 00:10:09 +00:00
|
|
|
def parent_records(parent, ids)
|
|
|
|
return Label.none unless parent.is_a?(Project) || parent.is_a?(Group)
|
|
|
|
|
2021-06-29 06:07:52 +00:00
|
|
|
labels = find_labels(parent)
|
2022-08-17 09:11:44 +00:00
|
|
|
label_ids = ids.map { |y| y[:label_id] }.compact
|
2021-06-10 00:10:09 +00:00
|
|
|
|
2021-06-29 06:07:52 +00:00
|
|
|
unless label_ids.empty?
|
|
|
|
id_relation = labels.where(id: label_ids)
|
|
|
|
end
|
|
|
|
|
2022-08-17 09:11:44 +00:00
|
|
|
label_names = ids.map { |y| y[:label_name] }.compact
|
2021-06-29 06:07:52 +00:00
|
|
|
unless label_names.empty?
|
|
|
|
label_relation = labels.where(title: label_names)
|
|
|
|
end
|
|
|
|
|
2021-08-27 00:08:45 +00:00
|
|
|
relation = [id_relation, label_relation].compact
|
|
|
|
return Label.none if relation.all?(Label.none)
|
2021-06-29 06:07:52 +00:00
|
|
|
|
|
|
|
Label.from_union(relation)
|
2021-06-10 00:10:09 +00:00
|
|
|
end
|
|
|
|
|
2021-04-15 18:09:01 +00:00
|
|
|
def find_object(parent_object, id)
|
2021-06-10 00:10:09 +00:00
|
|
|
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 }
|
2021-04-15 18:09:01 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def references_in(text, pattern = Label.reference_pattern)
|
|
|
|
labels = {}
|
2021-06-10 00:10:09 +00:00
|
|
|
|
|
|
|
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}"
|
2021-04-15 18:09:01 +00:00
|
|
|
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)
|
2021-05-11 15:10:20 +00:00
|
|
|
project_path = reference_cache.full_project_path(matches[:namespace], matches[:project])
|
2021-04-15 18:09:01 +00:00
|
|
|
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
|
2021-06-10 00:10:09 +00:00
|
|
|
|
|
|
|
def parent
|
|
|
|
project || group
|
|
|
|
end
|
|
|
|
|
|
|
|
def requires_unescaping?
|
|
|
|
true
|
|
|
|
end
|
2021-04-15 18:09:01 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-11 21:10:21 +00:00
|
|
|
Banzai::Filter::References::LabelReferenceFilter.prepend_mod_with('Banzai::Filter::References::LabelReferenceFilter')
|