Commit 7ecc247a authored by Brett Walker's avatar Brett Walker

Adjust class names to new references dir

for reference filters
parent dceae94f
...@@ -3,12 +3,14 @@ ...@@ -3,12 +3,14 @@
module EE module EE
module Banzai module Banzai
module Filter module Filter
module AbstractReferenceFilter module References
extend ::Gitlab::Utils::Override module AbstractReferenceFilter
extend ::Gitlab::Utils::Override
override :current_project_namespace_path override :current_project_namespace_path
def current_project_namespace_path def current_project_namespace_path
@current_project_namespace_path ||= (project&.namespace || group)&.full_path @current_project_namespace_path ||= (project&.namespace || group)&.full_path
end
end end
end end
end end
......
...@@ -3,51 +3,53 @@ ...@@ -3,51 +3,53 @@
module EE module EE
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces epic references with links. References to module References
# epics that do not exist are ignored. # HTML filter that replaces epic references with links. References to
# # epics that do not exist are ignored.
# This filter supports cross-project/group references. #
module EpicReferenceFilter # This filter supports cross-project/group references.
extend ActiveSupport::Concern module EpicReferenceFilter
extend ActiveSupport::Concern
class_methods do class_methods do
def references_in(text, pattern = object_class.reference_pattern) def references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
symbol = $~[object_sym] symbol = $~[object_sym]
if object_class.reference_valid?(symbol) if object_class.reference_valid?(symbol)
yield match, symbol.to_i, nil, $~[:group], $~ yield match, symbol.to_i, nil, $~[:group], $~
else else
match match
end
end end
end end
end end
end
def url_for_object(epic, group) def url_for_object(epic, group)
urls = ::Gitlab::Routing.url_helpers urls = ::Gitlab::Routing.url_helpers
urls.group_epic_url(group, epic, only_path: context[:only_path]) urls.group_epic_url(group, epic, only_path: context[:only_path])
end end
def data_attributes_for(text, group, object, link_content: false, link_reference: false) def data_attributes_for(text, group, object, link_content: false, link_reference: false)
{ {
original: escape_html_entities(text), original: escape_html_entities(text),
link: link_content, link: link_content,
link_reference: link_reference, link_reference: link_reference,
group: group.id, group: group.id,
object_sym => object.id object_sym => object.id
} }
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def parent_records(parent, ids) def parent_records(parent, ids)
parent.epics.where(iid: ids.to_a) parent.epics.where(iid: ids.to_a)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
private private
def parent_type def parent_type
:group :group
end
end end
end end
end end
......
...@@ -3,113 +3,115 @@ ...@@ -3,113 +3,115 @@
module EE module EE
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces iteration references with links. module References
module IterationReferenceFilter # HTML filter that replaces iteration references with links.
include ::Gitlab::Utils::StrongMemoize module IterationReferenceFilter
include ::Gitlab::Utils::StrongMemoize
def find_object(parent, id) def find_object(parent, id)
return unless valid_context?(parent) return unless valid_context?(parent)
find_iteration(parent, id: id) find_iteration(parent, id: id)
end
def valid_context?(parent)
group_context?(parent) || project_context?(parent)
end
def group_context?(parent)
strong_memoize(:group_context) do
parent.is_a?(Group)
end end
end
def project_context?(parent) def valid_context?(parent)
strong_memoize(:project_context) do group_context?(parent) || project_context?(parent)
parent.is_a?(Project)
end end
end
def references_in(text, pattern = ::Iteration.reference_pattern) def group_context?(parent)
# We'll handle here the references that follow the `reference_pattern`. strong_memoize(:group_context) do
# Other patterns (for example, the link pattern) are handled by the parent.is_a?(Group)
# default implementation. end
return super(text, pattern) if pattern != ::Iteration.reference_pattern end
iterations = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
iteration = parse_and_find_iteration($~[:project], $~[:namespace], $~[:iteration_id], $~[:iteration_name])
if iteration def project_context?(parent)
iterations[iteration.id] = yield match, iteration.id, $~[:project], $~[:namespace], $~ strong_memoize(:project_context) do
"#{::Banzai::Filter::AbstractReferenceFilter::REFERENCE_PLACEHOLDER}#{iteration.id}" parent.is_a?(Project)
else
match
end end
end end
return text if iterations.empty? def references_in(text, pattern = ::Iteration.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 != ::Iteration.reference_pattern
iterations = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
iteration = parse_and_find_iteration($~[:project], $~[:namespace], $~[:iteration_id], $~[:iteration_name])
if iteration
iterations[iteration.id] = yield match, iteration.id, $~[:project], $~[:namespace], $~
"#{::Banzai::Filter::References::AbstractReferenceFilter::REFERENCE_PLACEHOLDER}#{iteration.id}"
else
match
end
end
escape_with_placeholders(unescaped_html, iterations) return text if iterations.empty?
end
def parse_and_find_iteration(project_ref, namespace_ref, iteration_id, iteration_name) escape_with_placeholders(unescaped_html, iterations)
project_path = full_project_path(namespace_ref, project_ref) end
# Returns group if project is not found by path def parse_and_find_iteration(project_ref, namespace_ref, iteration_id, iteration_name)
parent = parent_from_ref(project_path) project_path = full_project_path(namespace_ref, project_ref)
return unless parent # Returns group if project is not found by path
parent = parent_from_ref(project_path)
iteration_params = iteration_params(iteration_id, iteration_name) return unless parent
find_iteration(parent, iteration_params) iteration_params = iteration_params(iteration_id, iteration_name)
end
def iteration_params(id, name) find_iteration(parent, iteration_params)
if name
{ name: name.tr('"', '') }
else
{ id: id.to_i }
end end
end
# rubocop: disable CodeReuse/ActiveRecord def iteration_params(id, name)
def find_iteration(parent, params) if name
::Iteration.for_projects_and_groups(project_ids(parent), group_and_ancestors_ids(parent)).find_by(**params) { name: name.tr('"', '') }
end else
# rubocop: enable CodeReuse/ActiveRecord { id: id.to_i }
end
end
def project_ids(parent) # rubocop: disable CodeReuse/ActiveRecord
parent.id if project_context?(parent) def find_iteration(parent, params)
end ::Iteration.for_projects_and_groups(project_ids(parent), group_and_ancestors_ids(parent)).find_by(**params)
end
# rubocop: enable CodeReuse/ActiveRecord
def group_and_ancestors_ids(parent) def project_ids(parent)
if group_context?(parent) parent.id if project_context?(parent)
parent.self_and_ancestors.select(:id)
elsif project_context?(parent)
parent.group&.self_and_ancestors&.select(:id)
end end
end
def url_for_object(iteration, _parent) def group_and_ancestors_ids(parent)
::Gitlab::Routing if group_context?(parent)
.url_helpers parent.self_and_ancestors.select(:id)
.iteration_url(iteration, only_path: context[:only_path]) elsif project_context?(parent)
end parent.group&.self_and_ancestors&.select(:id)
end
end
def url_for_object(iteration, _parent)
::Gitlab::Routing
.url_helpers
.iteration_url(iteration, only_path: context[:only_path])
end
def object_link_text(object, matches) def object_link_text(object, matches)
iteration_link = escape_once(super) iteration_link = escape_once(super)
reference = object.project&.to_reference_base(project) reference = object.project&.to_reference_base(project)
if reference.present? if reference.present?
"#{iteration_link} <i>in #{reference}</i>".html_safe "#{iteration_link} <i>in #{reference}</i>".html_safe
else else
iteration_link iteration_link
end
end end
end
def object_link_title(_object, _matches) def object_link_title(_object, _matches)
'Iteration' 'Iteration'
end
end end
end end
end end
......
...@@ -3,24 +3,26 @@ ...@@ -3,24 +3,26 @@
module EE module EE
module Banzai module Banzai
module Filter module Filter
module LabelReferenceFilter module References
extend ::Gitlab::Utils::Override module LabelReferenceFilter
extend ::Gitlab::Utils::Override
override :data_attributes_for override :data_attributes_for
def data_attributes_for(text, parent, object, link_content: false, link_reference: false) def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
return super unless object.scoped_label? return super unless object.scoped_label?
# Enabling HTML tooltips for scoped labels here and additional escaping is done in `object_link_title` # Enabling HTML tooltips for scoped labels here and additional escaping is done in `object_link_title`
super.merge!( super.merge!(
html: true html: true
) )
end end
override :object_link_title override :object_link_title
def object_link_title(object, matches) def object_link_title(object, matches)
return super unless object.scoped_label? return super unless object.scoped_label?
ERB::Util.html_escape(super) ERB::Util.html_escape(super)
end
end end
end end
end end
......
...@@ -3,61 +3,63 @@ ...@@ -3,61 +3,63 @@
module EE module EE
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces vulnerability references with links. References to module References
# vulnerabilities that do not exist are ignored. # HTML filter that replaces vulnerability references with links. References to
# # vulnerabilities that do not exist are ignored.
# This filter supports cross-project/group references. #
module VulnerabilityReferenceFilter # This filter supports cross-project/group references.
extend ActiveSupport::Concern module VulnerabilityReferenceFilter
extend ActiveSupport::Concern
class_methods do class_methods do
def references_in(text, pattern = object_class.reference_pattern) def references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
symbol = $~[object_sym] symbol = $~[object_sym]
if object_class.reference_valid?(symbol) if object_class.reference_valid?(symbol)
yield match, symbol.to_i, $~[:project], $~[:namespace], $~ yield match, symbol.to_i, $~[:project], $~[:namespace], $~
else else
match match
end
end end
end end
end end
end
def unescape_link(href) def unescape_link(href)
return href if href =~ object_class.reference_pattern return href if href =~ object_class.reference_pattern
super super
end end
def url_for_object(vulnerability, project) def url_for_object(vulnerability, project)
urls = ::Gitlab::Routing.url_helpers urls = ::Gitlab::Routing.url_helpers
urls.project_security_vulnerability_url(project, vulnerability, only_path: context[:only_path]) urls.project_security_vulnerability_url(project, vulnerability, only_path: context[:only_path])
end end
def data_attributes_for(text, project, object, link_content: false, link_reference: false) def data_attributes_for(text, project, object, link_content: false, link_reference: false)
{ {
original: escape_html_entities(text), original: escape_html_entities(text),
link: link_content, link: link_content,
link_reference: link_reference, link_reference: link_reference,
project: project.id, project: project.id,
object_sym => object.id object_sym => object.id
} }
end end
def parent_records(parent, ids) def parent_records(parent, ids)
return ::Vulnerability.none if ids.blank? || parent.nil? return ::Vulnerability.none if ids.blank? || parent.nil?
parent.vulnerabilities.id_in(ids.to_a) parent.vulnerabilities.id_in(ids.to_a)
end end
def record_identifier(record) def record_identifier(record)
record.id.to_i record.id.to_i
end end
private private
def parent_type def parent_type
:project :project
end
end end
end end
end end
......
...@@ -16,9 +16,9 @@ module EE ...@@ -16,9 +16,9 @@ module EE
def reference_filters def reference_filters
[ [
::Banzai::Filter::EpicReferenceFilter, ::Banzai::Filter::References::EpicReferenceFilter,
::Banzai::Filter::IterationReferenceFilter, ::Banzai::Filter::References::IterationReferenceFilter,
::Banzai::Filter::VulnerabilityReferenceFilter, ::Banzai::Filter::References::VulnerabilityReferenceFilter,
*super *super
] ]
end end
......
...@@ -9,9 +9,9 @@ module EE ...@@ -9,9 +9,9 @@ module EE
class_methods do class_methods do
def reference_filters def reference_filters
[ [
::Banzai::Filter::EpicReferenceFilter, ::Banzai::Filter::References::EpicReferenceFilter,
::Banzai::Filter::IterationReferenceFilter, ::Banzai::Filter::References::IterationReferenceFilter,
::Banzai::Filter::VulnerabilityReferenceFilter, ::Banzai::Filter::References::VulnerabilityReferenceFilter,
*super *super
] ]
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::EpicReferenceFilter do RSpec.describe Banzai::Filter::References::EpicReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:urls) { Gitlab::Routing.url_helpers } let(:urls) { Gitlab::Routing.url_helpers }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::IterationReferenceFilter do RSpec.describe Banzai::Filter::References::IterationReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:parent_group) { create(:group, :public) } let(:parent_group) { create(:group, :public) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::LabelReferenceFilter do RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project, :public, name: 'sample-project') } let(:project) { create(:project, :public, name: 'sample-project') }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::VulnerabilityReferenceFilter do RSpec.describe Banzai::Filter::References::VulnerabilityReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:urls) { Gitlab::Routing.url_helpers } let(:urls) { Gitlab::Routing.url_helpers }
......
...@@ -2,445 +2,447 @@ ...@@ -2,445 +2,447 @@
module Banzai module Banzai
module Filter module Filter
# Issues, merge requests, Snippets, Commits and Commit Ranges share module References
# similar functionality in reference filtering. # Issues, merge requests, Snippets, Commits and Commit Ranges share
class AbstractReferenceFilter < ReferenceFilter # similar functionality in reference filtering.
include CrossProjectReference class AbstractReferenceFilter < ReferenceFilter
include CrossProjectReference
# REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
# reference (which we replace with placeholder during re-scaping). The # REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
# random number helps ensure it's pretty close to unique. Since it's a # reference (which we replace with placeholder during re-scaping). The
# transitory value (it never gets saved) we can initialize once, and it # random number helps ensure it's pretty close to unique. Since it's a
# doesn't matter if it changes on a restart. # transitory value (it never gets saved) we can initialize once, and it
REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_" # doesn't matter if it changes on a restart.
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
def self.object_class
# Implement in child class def self.object_class
# Example: MergeRequest # Implement in child class
end # Example: MergeRequest
end
def self.object_name def self.object_name
@object_name ||= object_class.name.underscore @object_name ||= object_class.name.underscore
end end
def self.object_sym def self.object_sym
@object_sym ||= object_name.to_sym @object_sym ||= object_name.to_sym
end end
# Public: Find references in text (like `!123` for merge requests) # Public: Find references in text (like `!123` for merge requests)
# #
# AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches| # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
# object = find_object(project_ref, id) # object = find_object(project_ref, id)
# "<a href=...>#{object.to_reference}</a>" # "<a href=...>#{object.to_reference}</a>"
# end # end
# #
# text - String text to search. # text - String text to search.
# #
# Yields the String match, the Integer referenced object ID, an optional String # Yields the String match, the Integer referenced object ID, an optional String
# of the external project reference, and all of the matchdata. # of the external project reference, and all of the matchdata.
# #
# Returns a String replaced with the return of the block. # Returns a String replaced with the return of the block.
def self.references_in(text, pattern = object_class.reference_pattern) def self.references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
if ident = identifier($~) if ident = identifier($~)
yield match, ident, $~[:project], $~[:namespace], $~ yield match, ident, $~[:project], $~[:namespace], $~
else else
match match
end
end end
end end
end
def self.identifier(match_data) def self.identifier(match_data)
symbol = symbol_from_match(match_data) symbol = symbol_from_match(match_data)
parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
end
def identifier(match_data) parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
self.class.identifier(match_data) end
end
def self.symbol_from_match(match)
key = object_sym
match[key] if match.names.include?(key.to_s)
end
# Transform a symbol extracted from the text to a meaningful value def identifier(match_data)
# In most cases these will be integers, so we call #to_i by default self.class.identifier(match_data)
# end
# This method has the contract that if a string `ref` refers to a
# record `record`, then `parse_symbol(ref) == record_identifier(record)`.
def self.parse_symbol(symbol, match_data)
symbol.to_i
end
# We assume that most classes are identifying records by ID. def self.symbol_from_match(match)
# key = object_sym
# This method has the contract that if a string `ref` refers to a match[key] if match.names.include?(key.to_s)
# record `record`, then `class.parse_symbol(ref) == record_identifier(record)`. end
def record_identifier(record)
record.id
end
def object_class # Transform a symbol extracted from the text to a meaningful value
self.class.object_class # In most cases these will be integers, so we call #to_i by default
end #
# This method has the contract that if a string `ref` refers to a
# record `record`, then `parse_symbol(ref) == record_identifier(record)`.
def self.parse_symbol(symbol, match_data)
symbol.to_i
end
def object_sym # We assume that most classes are identifying records by ID.
self.class.object_sym #
end # This method has the contract that if a string `ref` refers to a
# record `record`, then `class.parse_symbol(ref) == record_identifier(record)`.
def record_identifier(record)
record.id
end
def references_in(*args, &block) def object_class
self.class.references_in(*args, &block) self.class.object_class
end end
# Implement in child class def object_sym
# Example: project.merge_requests.find self.class.object_sym
def find_object(parent_object, id) end
end
# Override if the link reference pattern produces a different ID (global def references_in(*args, &block)
# ID vs internal ID, for instance) to the regular reference pattern. self.class.references_in(*args, &block)
def find_object_from_link(parent_object, id) end
find_object(parent_object, id)
end
# Implement in child class # Implement in child class
# Example: project_merge_request_url # Example: project.merge_requests.find
def url_for_object(object, parent_object) def find_object(parent_object, id)
end end
def find_object_cached(parent_object, id) # Override if the link reference pattern produces a different ID (global
cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do # ID vs internal ID, for instance) to the regular reference pattern.
def find_object_from_link(parent_object, id)
find_object(parent_object, id) find_object(parent_object, id)
end end
end
def find_object_from_link_cached(parent_object, id) # Implement in child class
cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do # Example: project_merge_request_url
find_object_from_link(parent_object, id) def url_for_object(object, parent_object)
end end
end
def from_ref_cached(ref) def find_object_cached(parent_object, id)
cached_call("banzai_#{parent_type}_refs".to_sym, ref) do cached_call(:banzai_find_object, id, path: [object_class, parent_object.id]) do
parent_from_ref(ref) find_object(parent_object, id)
end
end end
end
def url_for_object_cached(object, parent_object) def find_object_from_link_cached(parent_object, id)
cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do cached_call(:banzai_find_object_from_link, id, path: [object_class, parent_object.id]) do
url_for_object(object, parent_object) find_object_from_link(parent_object, id)
end
end end
end
def call def from_ref_cached(ref)
return doc unless project || group || user cached_call("banzai_#{parent_type}_refs".to_sym, ref) do
parent_from_ref(ref)
end
end
ref_pattern = object_class.reference_pattern def url_for_object_cached(object, parent_object)
link_pattern = object_class.link_reference_pattern cached_call(:banzai_url_for_object, object, path: [object_class, parent_object.id]) do
url_for_object(object, parent_object)
end
end
# Compile often used regexps only once outside of the loop def call
ref_pattern_anchor = /\A#{ref_pattern}\z/ return doc unless project || group || user
link_pattern_start = /\A#{link_pattern}/
link_pattern_anchor = /\A#{link_pattern}\z/
nodes.each_with_index do |node, index| ref_pattern = object_class.reference_pattern
if text_node?(node) && ref_pattern link_pattern = object_class.link_reference_pattern
replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
object_link_filter(content, ref_pattern)
end
elsif element_node?(node) # Compile often used regexps only once outside of the loop
yield_valid_link(node) do |link, inner_html| ref_pattern_anchor = /\A#{ref_pattern}\z/
if ref_pattern && link =~ ref_pattern_anchor link_pattern_start = /\A#{link_pattern}/
replace_link_node_with_href(node, index, link) do link_pattern_anchor = /\A#{link_pattern}\z/
object_link_filter(link, ref_pattern, link_content: inner_html)
end
next nodes.each_with_index do |node, index|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
object_link_filter(content, ref_pattern)
end end
next unless link_pattern elsif element_node?(node)
yield_valid_link(node) do |link, inner_html|
if ref_pattern && link =~ ref_pattern_anchor
replace_link_node_with_href(node, index, link) do
object_link_filter(link, ref_pattern, link_content: inner_html)
end
if link == inner_html && inner_html =~ link_pattern_start next
replace_link_node_with_text(node, index) do
object_link_filter(inner_html, link_pattern, link_reference: true)
end end
next next unless link_pattern
end
if link =~ link_pattern_anchor if link == inner_html && inner_html =~ link_pattern_start
replace_link_node_with_href(node, index, link) do replace_link_node_with_text(node, index) do
object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true) object_link_filter(inner_html, link_pattern, link_reference: true)
end
next
end end
next if link =~ link_pattern_anchor
replace_link_node_with_href(node, index, link) do
object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
end
next
end
end end
end end
end end
end
doc doc
end end
# Replace references (like `!123` for merge requests) in text with links # Replace references (like `!123` for merge requests) in text with links
# to the referenced object's details page. # to the referenced object's details page.
# #
# text - String text to replace references in. # text - String text to replace references in.
# pattern - Reference pattern to match against. # pattern - Reference pattern to match against.
# link_content - Original content of the link being replaced. # link_content - Original content of the link being replaced.
# link_reference - True if this was using the link reference pattern, # link_reference - True if this was using the link reference pattern,
# false otherwise. # false otherwise.
# #
# Returns a String with references replaced with links. All links # Returns a String with references replaced with links. All links
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_content: nil, link_reference: false) def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
parent_path = if parent_type == :group parent_path = if parent_type == :group
full_group_path(namespace_ref) full_group_path(namespace_ref)
else else
full_project_path(namespace_ref, project_ref) full_project_path(namespace_ref, project_ref)
end end
parent = from_ref_cached(parent_path) parent = from_ref_cached(parent_path)
if parent if parent
object = object =
if link_reference if link_reference
find_object_from_link_cached(parent, id) find_object_from_link_cached(parent, id)
else else
find_object_cached(parent, id) find_object_cached(parent, id)
end end
end end
if object if object
title = object_link_title(object, matches) title = object_link_title(object, matches)
klass = reference_class(object_sym) klass = reference_class(object_sym)
data_attributes = data_attributes_for(link_content || match, parent, object, data_attributes = data_attributes_for(link_content || match, parent, object,
link_content: !!link_content, link_content: !!link_content,
link_reference: link_reference) link_reference: link_reference)
data = data_attribute(data_attributes) data = data_attribute(data_attributes)
url = url =
if matches.names.include?("url") && matches[:url] if matches.names.include?("url") && matches[:url]
matches[:url] matches[:url]
else else
url_for_object_cached(object, parent) url_for_object_cached(object, parent)
end end
content = link_content || object_link_text(object, matches) content = link_content || object_link_text(object, matches)
link = %(<a href="#{url}" #{data} link = %(<a href="#{url}" #{data}
title="#{escape_once(title)}" title="#{escape_once(title)}"
class="#{klass}">#{content}</a>) class="#{klass}">#{content}</a>)
wrap_link(link, object) wrap_link(link, object)
else else
match match
end
end end
end end
end
def wrap_link(link, object) def wrap_link(link, object)
link link
end end
def data_attributes_for(text, parent, object, link_content: false, link_reference: false) def data_attributes_for(text, parent, object, link_content: false, link_reference: false)
object_parent_type = parent.is_a?(Group) ? :group : :project object_parent_type = parent.is_a?(Group) ? :group : :project
{ {
original: escape_html_entities(text), original: escape_html_entities(text),
link: link_content, link: link_content,
link_reference: link_reference, link_reference: link_reference,
object_parent_type => parent.id, object_parent_type => parent.id,
object_sym => object.id object_sym => object.id
} }
end end
def object_link_text_extras(object, matches) def object_link_text_extras(object, matches)
extras = [] extras = []
if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/ if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
extras << "comment #{Regexp.last_match(1)}" extras << "comment #{Regexp.last_match(1)}"
end end
extension = matches[:extension] if matches.names.include?("extension") extension = matches[:extension] if matches.names.include?("extension")
extras << extension if extension extras << extension if extension
extras extras
end end
def object_link_title(object, matches) def object_link_title(object, matches)
object.title object.title
end end
def object_link_text(object, matches) def object_link_text(object, matches)
parent = project || group || user parent = project || group || user
text = object.reference_link_text(parent) text = object.reference_link_text(parent)
extras = object_link_text_extras(object, matches) extras = object_link_text_extras(object, matches)
text += " (#{extras.join(", ")})" if extras.any? text += " (#{extras.join(", ")})" if extras.any?
text text
end end
# Returns a Hash containing all object references (e.g. issue IDs) per the # Returns a Hash containing all object references (e.g. issue IDs) per the
# project they belong to. # project they belong to.
def references_per_parent def references_per_parent
@references_per ||= {} @references_per ||= {}
@references_per[parent_type] ||= begin @references_per[parent_type] ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new } refs = Hash.new { |hash, key| hash[key] = Set.new }
regex = [ regex = [
object_class.link_reference_pattern, object_class.link_reference_pattern,
object_class.reference_pattern object_class.reference_pattern
].compact.reduce { |a, b| Regexp.union(a, b) } ].compact.reduce { |a, b| Regexp.union(a, b) }
nodes.each do |node| nodes.each do |node|
node.to_html.scan(regex) do node.to_html.scan(regex) do
path = if parent_type == :project path = if parent_type == :project
full_project_path($~[:namespace], $~[:project]) full_project_path($~[:namespace], $~[:project])
else else
full_group_path($~[:group]) full_group_path($~[:group])
end end
if ident = identifier($~) if ident = identifier($~)
refs[path] << ident refs[path] << ident
end
end end
end end
end
refs refs
end
end end
end
# Returns a Hash containing referenced projects grouped per their full # Returns a Hash containing referenced projects grouped per their full
# path. # path.
def parent_per_reference def parent_per_reference
@per_reference ||= {} @per_reference ||= {}
@per_reference[parent_type] ||= begin @per_reference[parent_type] ||= begin
refs = Set.new refs = Set.new
references_per_parent.each do |ref, _| references_per_parent.each do |ref, _|
refs << ref refs << ref
end end
find_for_paths(refs.to_a).index_by(&:full_path) find_for_paths(refs.to_a).index_by(&:full_path)
end
end end
end
def relation_for_paths(paths) def relation_for_paths(paths)
klass = parent_type.to_s.camelize.constantize klass = parent_type.to_s.camelize.constantize
result = klass.where_full_path_in(paths) result = klass.where_full_path_in(paths)
return result if parent_type == :group return result if parent_type == :group
result.includes(:namespace) if parent_type == :project result.includes(:namespace) if parent_type == :project
end end
# Returns projects for the given paths. # Returns projects for the given paths.
def find_for_paths(paths) def find_for_paths(paths)
if Gitlab::SafeRequestStore.active? if Gitlab::SafeRequestStore.active?
cache = refs_cache cache = refs_cache
to_query = paths - cache.keys to_query = paths - cache.keys
unless to_query.empty? unless to_query.empty?
records = relation_for_paths(to_query) records = relation_for_paths(to_query)
found = [] found = []
records.each do |record| records.each do |record|
ref = record.full_path ref = record.full_path
get_or_set_cache(cache, ref) { record } get_or_set_cache(cache, ref) { record }
found << ref found << ref
end end
not_found = to_query - found not_found = to_query - found
not_found.each do |ref| not_found.each do |ref|
get_or_set_cache(cache, ref) { nil } get_or_set_cache(cache, ref) { nil }
end
end end
end
cache.slice(*paths).values.compact cache.slice(*paths).values.compact
else else
relation_for_paths(paths) relation_for_paths(paths)
end
end end
end
def current_parent_path def current_parent_path
@current_parent_path ||= parent&.full_path @current_parent_path ||= parent&.full_path
end end
def current_project_namespace_path def current_project_namespace_path
@current_project_namespace_path ||= project&.namespace&.full_path @current_project_namespace_path ||= project&.namespace&.full_path
end end
def records_per_parent def records_per_parent
@_records_per_project ||= {} @_records_per_project ||= {}
@_records_per_project[object_class.to_s.underscore] ||= begin @_records_per_project[object_class.to_s.underscore] ||= begin
hash = Hash.new { |h, k| h[k] = {} } hash = Hash.new { |h, k| h[k] = {} }
parent_per_reference.each do |path, parent| parent_per_reference.each do |path, parent|
record_ids = references_per_parent[path] record_ids = references_per_parent[path]
parent_records(parent, record_ids).each do |record| parent_records(parent, record_ids).each do |record|
hash[parent][record_identifier(record)] = record hash[parent][record_identifier(record)] = record
end
end end
end
hash hash
end
end end
end
private private
def full_project_path(namespace, project_ref) def full_project_path(namespace, project_ref)
return current_parent_path unless project_ref return current_parent_path unless project_ref
namespace_ref = namespace || current_project_namespace_path namespace_ref = namespace || current_project_namespace_path
"#{namespace_ref}/#{project_ref}" "#{namespace_ref}/#{project_ref}"
end end
def refs_cache def refs_cache
Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {} Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
end end
def parent_type def parent_type
:project :project
end end
def parent def parent
parent_type == :project ? project : group parent_type == :project ? project : group
end end
def full_group_path(group_ref) def full_group_path(group_ref)
return current_parent_path unless group_ref return current_parent_path unless group_ref
group_ref group_ref
end end
def unescape_html_entities(text) def unescape_html_entities(text)
CGI.unescapeHTML(text.to_s) CGI.unescapeHTML(text.to_s)
end end
def escape_html_entities(text) def escape_html_entities(text)
CGI.escapeHTML(text.to_s) CGI.escapeHTML(text.to_s)
end end
def escape_with_placeholders(text, placeholder_data) def escape_with_placeholders(text, placeholder_data)
escaped = escape_html_entities(text) escaped = escape_html_entities(text)
escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match| escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match|
placeholder_data[Regexp.last_match(1).to_i] placeholder_data[Regexp.last_match(1).to_i]
end
end end
end end
end end
end end
end end
Banzai::Filter::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::AbstractReferenceFilter') Banzai::Filter::References::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::AbstractReferenceFilter')
...@@ -2,27 +2,29 @@ ...@@ -2,27 +2,29 @@
module Banzai module Banzai
module Filter module Filter
class AlertReferenceFilter < IssuableReferenceFilter module References
self.reference_type = :alert class AlertReferenceFilter < IssuableReferenceFilter
self.reference_type = :alert
def self.object_class def self.object_class
AlertManagement::Alert AlertManagement::Alert
end end
def self.object_sym def self.object_sym
:alert :alert
end end
def parent_records(parent, ids) def parent_records(parent, ids)
parent.alert_management_alerts.where(iid: ids.to_a) parent.alert_management_alerts.where(iid: ids.to_a)
end end
def url_for_object(alert, project) def url_for_object(alert, project)
::Gitlab::Routing.url_helpers.details_project_alert_management_url( ::Gitlab::Routing.url_helpers.details_project_alert_management_url(
project, project,
alert.iid, alert.iid,
only_path: context[:only_path] only_path: context[:only_path]
) )
end
end end
end end
end end
......
...@@ -2,44 +2,46 @@ ...@@ -2,44 +2,46 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces commit range references with links. module References
# # HTML filter that replaces commit range references with links.
# This filter supports cross-project references. #
class CommitRangeReferenceFilter < AbstractReferenceFilter # This filter supports cross-project references.
self.reference_type = :commit_range class CommitRangeReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit_range
def self.object_class
CommitRange def self.object_class
end CommitRange
end
def self.references_in(text, pattern = CommitRange.reference_pattern) def self.references_in(text, pattern = CommitRange.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
yield match, $~[:commit_range], $~[:project], $~[:namespace], $~ yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
end
end end
end
def initialize(*args) def initialize(*args)
super super
@commit_map = {} @commit_map = {}
end end
def find_object(project, id) def find_object(project, id)
return unless project.is_a?(Project) return unless project.is_a?(Project)
range = CommitRange.new(id, project) range = CommitRange.new(id, project)
range.valid_commits? ? range : nil range.valid_commits? ? range : nil
end end
def url_for_object(range, project) def url_for_object(range, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
h.project_compare_url(project, h.project_compare_url(project,
range.to_param.merge(only_path: context[:only_path])) range.to_param.merge(only_path: context[:only_path]))
end end
def object_link_title(range, matches) def object_link_title(range, matches)
nil nil
end
end end
end end
end end
......
...@@ -2,84 +2,86 @@ ...@@ -2,84 +2,86 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces commit references with links. module References
# # HTML filter that replaces commit references with links.
# This filter supports cross-project references. #
class CommitReferenceFilter < AbstractReferenceFilter # This filter supports cross-project references.
self.reference_type = :commit class CommitReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit
def self.object_class
Commit def self.object_class
end Commit
end
def self.references_in(text, pattern = Commit.reference_pattern) def self.references_in(text, pattern = Commit.reference_pattern)
text.gsub(pattern) do |match| text.gsub(pattern) do |match|
yield match, $~[:commit], $~[:project], $~[:namespace], $~ yield match, $~[:commit], $~[:project], $~[:namespace], $~
end
end end
end
def find_object(project, id) def find_object(project, id)
return unless project.is_a?(Project) && project.valid_repo? return unless project.is_a?(Project) && project.valid_repo?
_, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) } _, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
record record
end end
def referenced_merge_request_commit_shas def referenced_merge_request_commit_shas
return [] unless noteable.is_a?(MergeRequest) return [] unless noteable.is_a?(MergeRequest)
@referenced_merge_request_commit_shas ||= begin @referenced_merge_request_commit_shas ||= begin
referenced_shas = references_per_parent.values.reduce(:|).to_a referenced_shas = references_per_parent.values.reduce(:|).to_a
noteable.all_commit_shas.select do |sha| noteable.all_commit_shas.select do |sha|
referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) } referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
end
end end
end end
end
# The default behaviour is `#to_i` - we just pass the hash through. # The default behaviour is `#to_i` - we just pass the hash through.
def self.parse_symbol(sha_hash, _match) def self.parse_symbol(sha_hash, _match)
sha_hash sha_hash
end end
def url_for_object(commit, project) def url_for_object(commit, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
if referenced_merge_request_commit_shas.include?(commit.id) if referenced_merge_request_commit_shas.include?(commit.id)
h.diffs_project_merge_request_url(project, h.diffs_project_merge_request_url(project,
noteable, noteable,
commit_id: commit.id, commit_id: commit.id,
only_path: only_path?) only_path: only_path?)
else else
h.project_commit_url(project, h.project_commit_url(project,
commit, commit,
only_path: only_path?) only_path: only_path?)
end
end end
end
def object_link_text_extras(object, matches) def object_link_text_extras(object, matches)
extras = super extras = super
path = matches[:path] if matches.names.include?("path") path = matches[:path] if matches.names.include?("path")
if path == '/builds' if path == '/builds'
extras.unshift "builds" extras.unshift "builds"
end end
extras extras
end end
private private
def parent_records(parent, ids) def parent_records(parent, ids)
parent.commits_by(oids: ids.to_a) parent.commits_by(oids: ids.to_a)
end end
def noteable def noteable
context[:noteable] context[:noteable]
end end
def only_path? def only_path?
context[:only_path] context[:only_path]
end
end end
end end
end end
......
...@@ -2,105 +2,107 @@ ...@@ -2,105 +2,107 @@
module Banzai module Banzai
module Filter module Filter
class DesignReferenceFilter < AbstractReferenceFilter module References
class Identifier class DesignReferenceFilter < AbstractReferenceFilter
include Comparable class Identifier
attr_reader :issue_iid, :filename include Comparable
attr_reader :issue_iid, :filename
def initialize(issue_iid:, filename:)
@issue_iid = issue_iid def initialize(issue_iid:, filename:)
@filename = filename @issue_iid = issue_iid
@filename = filename
end
def as_composite_id(id_for_iid)
id = id_for_iid[issue_iid]
return unless id
{ issue_id: id, filename: filename }
end
def <=>(other)
return unless other.is_a?(Identifier)
[issue_iid, filename] <=> [other.issue_iid, other.filename]
end
alias_method :eql?, :==
def hash
[issue_iid, filename].hash
end
end end
def as_composite_id(id_for_iid) self.reference_type = :design
id = id_for_iid[issue_iid]
return unless id
{ issue_id: id, filename: filename } def find_object(project, identifier)
records_per_parent[project][identifier]
end end
def <=>(other) def parent_records(project, identifiers)
return unless other.is_a?(Identifier) return [] unless project.design_management_enabled?
[issue_iid, filename] <=> [other.issue_iid, other.filename] iids = identifiers.map(&:issue_iid).to_set
end issues = project.issues.where(iid: iids)
alias_method :eql?, :== id_for_iid = issues.index_by(&:iid).transform_values(&:id)
issue_by_id = issues.index_by(&:id)
def hash designs(identifiers, id_for_iid).each do |d|
[issue_iid, filename].hash issue = issue_by_id[d.issue_id]
# optimisation: assign values we have already fetched
d.project = project
d.issue = issue
end
end end
end
self.reference_type = :design
def find_object(project, identifier)
records_per_parent[project][identifier]
end
def parent_records(project, identifiers)
return [] unless project.design_management_enabled?
iids = identifiers.map(&:issue_iid).to_set
issues = project.issues.where(iid: iids)
id_for_iid = issues.index_by(&:iid).transform_values(&:id)
issue_by_id = issues.index_by(&:id)
designs(identifiers, id_for_iid).each do |d| def relation_for_paths(paths)
issue = issue_by_id[d.issue_id] super.includes(:route, :namespace, :group)
# optimisation: assign values we have already fetched
d.project = project
d.issue = issue
end end
end
def relation_for_paths(paths)
super.includes(:route, :namespace, :group)
end
def parent_type def parent_type
:project :project
end end
# optimisation to reuse the parent_per_reference query information # optimisation to reuse the parent_per_reference query information
def parent_from_ref(ref) def parent_from_ref(ref)
parent_per_reference[ref || current_parent_path] parent_per_reference[ref || current_parent_path]
end end
def url_for_object(design, project) def url_for_object(design, project)
path_options = { vueroute: design.filename } path_options = { vueroute: design.filename }
Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options) Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
end end
def data_attributes_for(_text, _project, design, **_kwargs) def data_attributes_for(_text, _project, design, **_kwargs)
super.merge(issue: design.issue_id) super.merge(issue: design.issue_id)
end end
def self.object_class def self.object_class
::DesignManagement::Design ::DesignManagement::Design
end end
def self.object_sym def self.object_sym
:design :design
end end
def self.parse_symbol(raw, match_data) def self.parse_symbol(raw, match_data)
filename = match_data[:url_filename] filename = match_data[:url_filename]
iid = match_data[:issue].to_i iid = match_data[:issue].to_i
Identifier.new(filename: CGI.unescape(filename), issue_iid: iid) Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
end end
def record_identifier(design) def record_identifier(design)
Identifier.new(filename: design.filename, issue_iid: design.issue.iid) Identifier.new(filename: design.filename, issue_iid: design.issue.iid)
end end
private private
def designs(identifiers, id_for_iid) def designs(identifiers, id_for_iid)
identifiers identifiers
.map { |identifier| identifier.as_composite_id(id_for_iid) } .map { |identifier| identifier.as_composite_id(id_for_iid) }
.compact .compact
.in_groups_of(100, false) # limitation of by_issue_id_and_filename, so we batch .in_groups_of(100, false) # limitation of by_issue_id_and_filename, so we batch
.flat_map { |ids| DesignManagement::Design.by_issue_id_and_filename(ids) } .flat_map { |ids| DesignManagement::Design.by_issue_id_and_filename(ids) }
end
end end
end end
end end
......
...@@ -2,21 +2,23 @@ ...@@ -2,21 +2,23 @@
module Banzai module Banzai
module Filter module Filter
# The actual filter is implemented in the EE mixin module References
class EpicReferenceFilter < IssuableReferenceFilter # The actual filter is implemented in the EE mixin
self.reference_type = :epic class EpicReferenceFilter < IssuableReferenceFilter
self.reference_type = :epic
def self.object_class def self.object_class
Epic Epic
end end
private private
def group def group
context[:group] || context[:project]&.group context[:group] || context[:project]&.group
end
end end
end end
end end
end end
Banzai::Filter::EpicReferenceFilter.prepend_if_ee('EE::Banzai::Filter::EpicReferenceFilter') Banzai::Filter::References::EpicReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::EpicReferenceFilter')
...@@ -2,116 +2,118 @@ ...@@ -2,116 +2,118 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces external issue tracker references with links. module References
# References are ignored if the project doesn't use an external issue # HTML filter that replaces external issue tracker references with links.
# tracker. # References are ignored if the project doesn't use an external issue
# # tracker.
# This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue
# Public: Find `JIRA-123` issue references in text
# #
# ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue| # This filter does not support cross-project references.
# "<a href=...>##{issue}</a>" class ExternalIssueReferenceFilter < ReferenceFilter
# end self.reference_type = :external_issue
#
# text - String text to search. # Public: Find `JIRA-123` issue references in text
# #
# Yields the String match and the String issue reference. # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
# # "<a href=...>##{issue}</a>"
# Returns a String replaced with the return of the block. # end
def self.references_in(text, pattern) #
text.gsub(pattern) do |match| # text - String text to search.
yield match, $~[:issue] #
# Yields the String match and the String issue reference.
#
# Returns a String replaced with the return of the block.
def self.references_in(text, pattern)
text.gsub(pattern) do |match|
yield match, $~[:issue]
end
end end
end
def call def call
# Early return if the project isn't using an external tracker # Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker? return doc if project.nil? || default_issues_tracker?
ref_pattern = issue_reference_pattern ref_pattern = issue_reference_pattern
ref_start_pattern = /\A#{ref_pattern}\z/ ref_start_pattern = /\A#{ref_pattern}\z/
nodes.each_with_index do |node, index| nodes.each_with_index do |node, index|
if text_node?(node) if text_node?(node)
replace_text_when_pattern_matches(node, index, ref_pattern) do |content| replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
issue_link_filter(content) issue_link_filter(content)
end end
elsif element_node?(node) elsif element_node?(node)
yield_valid_link(node) do |link, inner_html| yield_valid_link(node) do |link, inner_html|
if link =~ ref_start_pattern if link =~ ref_start_pattern
replace_link_node_with_href(node, index, link) do replace_link_node_with_href(node, index, link) do
issue_link_filter(link, link_content: inner_html) issue_link_filter(link, link_content: inner_html)
end
end end
end end
end end
end end
end
doc
end
private doc
end
# Replace `JIRA-123` issue references in text with links to the referenced private
# issue's details page.
# # Replace `JIRA-123` issue references in text with links to the referenced
# text - String text to replace references in. # issue's details page.
# link_content - Original content of the link being replaced. #
# # text - String text to replace references in.
# Returns a String with `JIRA-123` references replaced with links. All # link_content - Original content of the link being replaced.
# links have `gfm` and `gfm-issue` class names attached for styling. #
def issue_link_filter(text, link_content: nil) # Returns a String with `JIRA-123` references replaced with links. All
self.class.references_in(text, issue_reference_pattern) do |match, id| # links have `gfm` and `gfm-issue` class names attached for styling.
url = url_for_issue(id) def issue_link_filter(text, link_content: nil)
klass = reference_class(:issue) self.class.references_in(text, issue_reference_pattern) do |match, id|
data = data_attribute(project: project.id, external_issue: id) url = url_for_issue(id)
content = link_content || match klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
%(<a href="#{url}" #{data} content = link_content || match
title="#{escape_once(issue_title)}"
class="#{klass}">#{content}</a>) %(<a href="#{url}" #{data}
title="#{escape_once(issue_title)}"
class="#{klass}">#{content}</a>)
end
end end
end
def url_for_issue(issue_id) def url_for_issue(issue_id)
return '' if project.nil? return '' if project.nil?
url = if only_path? url = if only_path?
project.external_issue_tracker.issue_path(issue_id) project.external_issue_tracker.issue_path(issue_id)
else else
project.external_issue_tracker.issue_url(issue_id) project.external_issue_tracker.issue_url(issue_id)
end end
# Ensure we return a valid URL to prevent possible XSS. # Ensure we return a valid URL to prevent possible XSS.
URI.parse(url).to_s URI.parse(url).to_s
rescue URI::InvalidURIError rescue URI::InvalidURIError
'' ''
end end
def default_issues_tracker? def default_issues_tracker?
external_issues_cached(:default_issues_tracker?) external_issues_cached(:default_issues_tracker?)
end end
def issue_reference_pattern def issue_reference_pattern
external_issues_cached(:external_issue_reference_pattern) external_issues_cached(:external_issue_reference_pattern)
end end
def project def project
context[:project] context[:project]
end end
def issue_title def issue_title
"Issue in #{project.external_issue_tracker.title}" "Issue in #{project.external_issue_tracker.title}"
end end
def external_issues_cached(attribute) def external_issues_cached(attribute)
cached_attributes = Gitlab::SafeRequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} } cached_attributes = Gitlab::SafeRequestStore[:banzai_external_issues_tracker_attributes] ||= Hash.new { |h, k| h[k] = {} }
cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend cached_attributes[project.id][attribute] = project.public_send(attribute) if cached_attributes[project.id][attribute].nil? # rubocop:disable GitlabSecurity/PublicSend
cached_attributes[project.id][attribute] cached_attributes[project.id][attribute]
end
end end
end end
end end
......
...@@ -2,31 +2,33 @@ ...@@ -2,31 +2,33 @@
module Banzai module Banzai
module Filter module Filter
class FeatureFlagReferenceFilter < IssuableReferenceFilter module References
self.reference_type = :feature_flag class FeatureFlagReferenceFilter < IssuableReferenceFilter
self.reference_type = :feature_flag
def self.object_class def self.object_class
Operations::FeatureFlag Operations::FeatureFlag
end end
def self.object_sym def self.object_sym
:feature_flag :feature_flag
end end
def parent_records(parent, ids) def parent_records(parent, ids)
parent.operations_feature_flags.where(iid: ids.to_a) parent.operations_feature_flags.where(iid: ids.to_a)
end end
def url_for_object(feature_flag, project) def url_for_object(feature_flag, project)
::Gitlab::Routing.url_helpers.edit_project_feature_flag_url( ::Gitlab::Routing.url_helpers.edit_project_feature_flag_url(
project, project,
feature_flag.iid, feature_flag.iid,
only_path: context[:only_path] only_path: context[:only_path]
) )
end end
def object_link_title(object, matches) def object_link_title(object, matches)
object.name object.name
end
end end
end end
end end
......
...@@ -2,17 +2,19 @@ ...@@ -2,17 +2,19 @@
module Banzai module Banzai
module Filter module Filter
class IssuableReferenceFilter < AbstractReferenceFilter module References
def record_identifier(record) class IssuableReferenceFilter < AbstractReferenceFilter
record.iid.to_i def record_identifier(record)
end record.iid.to_i
end
def find_object(parent, iid) def find_object(parent, iid)
records_per_parent[parent][iid] records_per_parent[parent][iid]
end end
def parent_from_ref(ref) def parent_from_ref(ref)
parent_per_reference[ref || current_parent_path] parent_per_reference[ref || current_parent_path]
end
end end
end end
end end
......
...@@ -2,55 +2,57 @@ ...@@ -2,55 +2,57 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces issue references with links. References to module References
# issues that do not exist are ignored. # HTML filter that replaces issue references with links. References to
# # issues that do not exist are ignored.
# This filter supports cross-project references. #
# # This filter supports cross-project references.
# When external issues tracker like Jira is activated we should not #
# use issue reference pattern, but we should still be able # When external issues tracker like Jira is activated we should not
# to reference issues from other GitLab projects. # use issue reference pattern, but we should still be able
class IssueReferenceFilter < IssuableReferenceFilter # to reference issues from other GitLab projects.
self.reference_type = :issue class IssueReferenceFilter < IssuableReferenceFilter
self.reference_type = :issue
def self.object_class
Issue def self.object_class
end Issue
end
def url_for_object(issue, project) def url_for_object(issue, project)
return issue_path(issue, project) if only_path? return issue_path(issue, project) if only_path?
issue_url(issue, project) issue_url(issue, project)
end end
def parent_records(parent, ids) def parent_records(parent, ids)
parent.issues.where(iid: ids.to_a) parent.issues.where(iid: ids.to_a)
end end
def object_link_text_extras(issue, matches) def object_link_text_extras(issue, matches)
super + design_link_extras(issue, matches.named_captures['path']) super + design_link_extras(issue, matches.named_captures['path'])
end end
private private
def issue_path(issue, project) def issue_path(issue, project)
Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid) Gitlab::Routing.url_helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue.iid)
end end
def issue_url(issue, project) def issue_url(issue, project)
Gitlab::Routing.url_helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue.iid) Gitlab::Routing.url_helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue.iid)
end end
def design_link_extras(issue, path) def design_link_extras(issue, path)
if path == '/designs' && read_designs?(issue) if path == '/designs' && read_designs?(issue)
['designs'] ['designs']
else else
[] []
end
end end
end
def read_designs?(issue) def read_designs?(issue)
issue.project.design_management_enabled? issue.project.design_management_enabled?
end
end end
end end
end end
......
...@@ -2,15 +2,17 @@ ...@@ -2,15 +2,17 @@
module Banzai module Banzai
module Filter module Filter
# The actual filter is implemented in the EE mixin module References
class IterationReferenceFilter < AbstractReferenceFilter # The actual filter is implemented in the EE mixin
self.reference_type = :iteration class IterationReferenceFilter < AbstractReferenceFilter
self.reference_type = :iteration
def self.object_class def self.object_class
Iteration Iteration
end
end end
end end
end end
end end
Banzai::Filter::IterationReferenceFilter.prepend_if_ee('EE::Banzai::Filter::IterationReferenceFilter') Banzai::Filter::References::IterationReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::IterationReferenceFilter')
...@@ -2,128 +2,130 @@ ...@@ -2,128 +2,130 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces label references with links. module References
class LabelReferenceFilter < AbstractReferenceFilter # HTML filter that replaces label references with links.
self.reference_type = :label class LabelReferenceFilter < AbstractReferenceFilter
self.reference_type = :label
def self.object_class def self.object_class
Label Label
end end
def find_object(parent_object, id)
find_labels(parent_object).find(id)
end
def references_in(text, pattern = Label.reference_pattern)
labels = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
namespace, project = $~[:namespace], $~[:project]
project_path = full_project_path(namespace, project)
label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
if label def find_object(parent_object, id)
labels[label.id] = yield match, label.id, project, namespace, $~ find_labels(parent_object).find(id)
"#{REFERENCE_PLACEHOLDER}#{label.id}"
else
match
end
end end
return text if labels.empty? def references_in(text, pattern = Label.reference_pattern)
labels = {}
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
namespace, project = $~[:namespace], $~[:project]
project_path = full_project_path(namespace, project)
label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
if label
labels[label.id] = yield match, label.id, project, namespace, $~
"#{REFERENCE_PLACEHOLDER}#{label.id}"
else
match
end
end
escape_with_placeholders(unescaped_html, labels) return text if labels.empty?
end
def find_label_cached(parent_ref, label_id, label_name) escape_with_placeholders(unescaped_html, labels)
cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do
find_label(parent_ref, label_id, label_name)
end end
end
def find_label(parent_ref, label_id, label_name) def find_label_cached(parent_ref, label_id, label_name)
parent = parent_from_ref(parent_ref) cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do
return unless parent find_label(parent_ref, label_id, label_name)
end
end
label_params = label_params(label_id, label_name) def find_label(parent_ref, label_id, label_name)
find_labels(parent).find_by(label_params) parent = parent_from_ref(parent_ref)
end return unless parent
def find_labels(parent) label_params = label_params(label_id, label_name)
params = if parent.is_a?(Group) find_labels(parent).find_by(label_params)
{ group_id: parent.id, end
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
# Parameters to pass to `Label.find_by` based on the given arguments def find_labels(parent)
# params = if parent.is_a?(Group)
# id - Integer ID to pass. If present, returns {id: id} { group_id: parent.id,
# name - String name to pass. If `id` is absent, finds by name without include_ancestor_groups: true,
# surrounding quotes. only_group_labels: true }
# else
# Returns a Hash. { project: parent,
def label_params(id, name) include_ancestor_groups: true }
if name end
{ name: name.tr('"', '') }
else LabelsFinder.new(nil, params).execute(skip_authorization: true)
{ id: id.to_i }
end end
end
def url_for_object(label, parent) # Parameters to pass to `Label.find_by` based on the given arguments
label_url_method = #
if context[:label_url_method] # id - Integer ID to pass. If present, returns {id: id}
context[:label_url_method] # name - String name to pass. If `id` is absent, finds by name without
elsif parent.is_a?(Project) # surrounding quotes.
:project_issues_url #
# Returns a Hash.
def label_params(id, name)
if name
{ name: name.tr('"', '') }
else
{ id: id.to_i }
end end
end
return unless label_url_method 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
Gitlab::Routing.url_helpers.public_send(label_url_method, parent, label_name: label.name, only_path: context[:only_path]) # rubocop:disable GitlabSecurity/PublicSend return unless label_url_method
end
def object_link_text(object, matches) Gitlab::Routing.url_helpers.public_send(label_url_method, parent, label_name: label.name, only_path: context[:only_path]) # rubocop:disable GitlabSecurity/PublicSend
label_suffix = '' end
parent = project || group
if project || full_path_ref?(matches) def object_link_text(object, matches)
project_path = full_project_path(matches[:namespace], matches[:project]) label_suffix = ''
parent_from_ref = from_ref_cached(project_path) parent = project || group
reference = parent_from_ref.to_human_reference(parent)
label_suffix = " <i>in #{ERB::Util.html_escape(reference)}</i>" if reference.present? if project || full_path_ref?(matches)
end project_path = full_project_path(matches[:namespace], matches[:project])
parent_from_ref = from_ref_cached(project_path)
reference = parent_from_ref.to_human_reference(parent)
presenter = object.present(issuable_subject: parent) label_suffix = " <i>in #{ERB::Util.html_escape(reference)}</i>" if reference.present?
LabelsHelper.render_colored_label(presenter, suffix: label_suffix) end
end
def wrap_link(link, label) presenter = object.present(issuable_subject: parent)
presenter = label.present(issuable_subject: project || group) LabelsHelper.render_colored_label(presenter, suffix: label_suffix)
LabelsHelper.wrap_label_html(link, small: true, label: presenter) end
end
def full_path_ref?(matches) def wrap_link(link, label)
matches[:namespace] && matches[:project] presenter = label.present(issuable_subject: project || group)
end LabelsHelper.wrap_label_html(link, small: true, label: presenter)
end
def reference_class(type, tooltip: true) def full_path_ref?(matches)
super + ' gl-link gl-label-link' matches[:namespace] && matches[:project]
end end
def reference_class(type, tooltip: true)
super + ' gl-link gl-label-link'
end
def object_link_title(object, matches) def object_link_title(object, matches)
presenter = object.present(issuable_subject: project || group) presenter = object.present(issuable_subject: project || group)
LabelsHelper.label_tooltip_title(presenter) LabelsHelper.label_tooltip_title(presenter)
end
end end
end end
end end
end end
Banzai::Filter::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::LabelReferenceFilter') Banzai::Filter::References::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::LabelReferenceFilter')
...@@ -2,95 +2,97 @@ ...@@ -2,95 +2,97 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces merge request references with links. References module References
# to merge requests that do not exist are ignored. # HTML filter that replaces merge request references with links. References
# # to merge requests that do not exist are ignored.
# This filter supports cross-project references. #
class MergeRequestReferenceFilter < IssuableReferenceFilter # This filter supports cross-project references.
self.reference_type = :merge_request class MergeRequestReferenceFilter < IssuableReferenceFilter
self.reference_type = :merge_request
def self.object_class
MergeRequest def self.object_class
end MergeRequest
end
def url_for_object(mr, project) def url_for_object(mr, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
h.project_merge_request_url(project, mr, h.project_merge_request_url(project, mr,
only_path: context[:only_path]) only_path: context[:only_path])
end end
def object_link_title(object, matches) def object_link_title(object, matches)
# The method will return `nil` if object is not a commit # The method will return `nil` if object is not a commit
# allowing for properly handling the extended MR Tooltip # allowing for properly handling the extended MR Tooltip
object_link_commit_title(object, matches) object_link_commit_title(object, matches)
end end
def object_link_text_extras(object, matches) def object_link_text_extras(object, matches)
extras = super extras = super
if commit_ref = object_link_commit_ref(object, matches) if commit_ref = object_link_commit_ref(object, matches)
klass = reference_class(:commit, tooltip: false) klass = reference_class(:commit, tooltip: false)
commit_ref_tag = %(<span class="#{klass}">#{commit_ref}</span>) commit_ref_tag = %(<span class="#{klass}">#{commit_ref}</span>)
return extras.unshift(commit_ref_tag) return extras.unshift(commit_ref_tag)
end end
path = matches[:path] if matches.names.include?("path") path = matches[:path] if matches.names.include?("path")
case path case path
when '/diffs' when '/diffs'
extras.unshift "diffs" extras.unshift "diffs"
when '/commits' when '/commits'
extras.unshift "commits" extras.unshift "commits"
when '/builds' when '/builds'
extras.unshift "builds" extras.unshift "builds"
end end
extras extras
end end
def parent_records(parent, ids) def parent_records(parent, ids)
parent.merge_requests parent.merge_requests
.where(iid: ids.to_a) .where(iid: ids.to_a)
.includes(target_project: :namespace) .includes(target_project: :namespace)
end end
def reference_class(object_sym, options = {}) def reference_class(object_sym, options = {})
super(object_sym, tooltip: false) super(object_sym, tooltip: false)
end end
def data_attributes_for(text, parent, object, **data) def data_attributes_for(text, parent, object, **data)
super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title) super.merge(project_path: parent.full_path, iid: object.iid, mr_title: object.title)
end end
private private
def object_link_commit_title(object, matches) def object_link_commit_title(object, matches)
object_link_commit(object, matches)&.title object_link_commit(object, matches)&.title
end end
def object_link_commit_ref(object, matches) def object_link_commit_ref(object, matches)
object_link_commit(object, matches)&.short_id object_link_commit(object, matches)&.short_id
end end
def object_link_commit(object, matches) def object_link_commit(object, matches)
return unless matches.names.include?('query') && query = matches[:query] return unless matches.names.include?('query') && query = matches[:query]
# Removes leading "?". CGI.parse expects "arg1&arg2&arg3" # Removes leading "?". CGI.parse expects "arg1&arg2&arg3"
params = CGI.parse(query.sub(/^\?/, '')) params = CGI.parse(query.sub(/^\?/, ''))
return unless commit_sha = params['commit_id']&.first return unless commit_sha = params['commit_id']&.first
if commit = find_commit_by_sha(object, commit_sha) if commit = find_commit_by_sha(object, commit_sha)
Commit.from_hash(commit.to_hash, object.project) Commit.from_hash(commit.to_hash, object.project)
end
end end
end
def find_commit_by_sha(object, commit_sha) def find_commit_by_sha(object, commit_sha)
@all_commits ||= {} @all_commits ||= {}
@all_commits[object.id] ||= object.all_commits @all_commits[object.id] ||= object.all_commits
@all_commits[object.id].find { |commit| commit.sha == commit_sha } @all_commits[object.id].find { |commit| commit.sha == commit_sha }
end
end end
end end
end end
......
...@@ -2,136 +2,138 @@ ...@@ -2,136 +2,138 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces milestone references with links. module References
class MilestoneReferenceFilter < AbstractReferenceFilter # HTML filter that replaces milestone references with links.
include Gitlab::Utils::StrongMemoize class MilestoneReferenceFilter < AbstractReferenceFilter
include Gitlab::Utils::StrongMemoize
self.reference_type = :milestone self.reference_type = :milestone
def self.object_class def self.object_class
Milestone Milestone
end end
# Links to project milestones contain the IID, but when we're handling # Links to project milestones contain the IID, but when we're handling
# 'regular' references, we need to use the global ID to disambiguate # 'regular' references, we need to use the global ID to disambiguate
# between group and project milestones. # between group and project milestones.
def find_object(parent, id) def find_object(parent, id)
return unless valid_context?(parent) return unless valid_context?(parent)
find_milestone_with_finder(parent, id: id) find_milestone_with_finder(parent, id: id)
end end
def find_object_from_link(parent, iid) def find_object_from_link(parent, iid)
return unless valid_context?(parent) return unless valid_context?(parent)
find_milestone_with_finder(parent, iid: iid) find_milestone_with_finder(parent, iid: iid)
end
def valid_context?(parent)
strong_memoize(:valid_context) do
group_context?(parent) || project_context?(parent)
end end
end
def group_context?(parent) def valid_context?(parent)
strong_memoize(:group_context) do strong_memoize(:valid_context) do
parent.is_a?(Group) group_context?(parent) || project_context?(parent)
end
end end
end
def project_context?(parent) def group_context?(parent)
strong_memoize(:project_context) do strong_memoize(:group_context) do
parent.is_a?(Project) parent.is_a?(Group)
end
end end
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 = {} def project_context?(parent)
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match| strong_memoize(:project_context) do
milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) parent.is_a?(Project)
if milestone
milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
"#{REFERENCE_PLACEHOLDER}#{milestone.id}"
else
match
end end
end end
return text if milestones.empty? 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) do |match|
milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
if milestone
milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~
"#{REFERENCE_PLACEHOLDER}#{milestone.id}"
else
match
end
end
escape_with_placeholders(unescaped_html, milestones) return text if milestones.empty?
end
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) escape_with_placeholders(unescaped_html, milestones)
project_path = full_project_path(namespace_ref, project_ref) end
# Returns group if project is not found by path def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
parent = parent_from_ref(project_path) project_path = full_project_path(namespace_ref, project_ref)
return unless parent # Returns group if project is not found by path
parent = parent_from_ref(project_path)
milestone_params = milestone_params(milestone_id, milestone_name) return unless parent
find_milestone_with_finder(parent, milestone_params) milestone_params = milestone_params(milestone_id, milestone_name)
end
def milestone_params(iid, name) find_milestone_with_finder(parent, milestone_params)
if name
{ name: name.tr('"', '') }
else
{ iid: iid.to_i }
end end
end
def find_milestone_with_finder(parent, params) def milestone_params(iid, name)
finder_params = milestone_finder_params(parent, params[:iid].present?) if name
{ name: name.tr('"', '') }
else
{ iid: iid.to_i }
end
end
MilestonesFinder.new(finder_params).find_by(params) def find_milestone_with_finder(parent, params)
end finder_params = milestone_finder_params(parent, params[:iid].present?)
def milestone_finder_params(parent, find_by_iid) MilestonesFinder.new(finder_params).find_by(params)
{ order: nil, state: 'all' }.tap do |params| end
params[:project_ids] = parent.id if project_context?(parent)
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 # We don't support IID lookups because IIDs can clash between
# group/project milestones and group/subgroup milestones. # group/project milestones and group/subgroup milestones.
params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid params[:group_ids] = self_and_ancestors_ids(parent) unless find_by_iid
end
end end
end
def self_and_ancestors_ids(parent) def self_and_ancestors_ids(parent)
if group_context?(parent) if group_context?(parent)
parent.self_and_ancestors.select(:id) parent.self_and_ancestors.select(:id)
elsif project_context?(parent) elsif project_context?(parent)
parent.group&.self_and_ancestors&.select(:id) parent.group&.self_and_ancestors&.select(:id)
end
end end
end
def url_for_object(milestone, project) def url_for_object(milestone, project)
Gitlab::Routing Gitlab::Routing
.url_helpers .url_helpers
.milestone_url(milestone, only_path: context[:only_path]) .milestone_url(milestone, only_path: context[:only_path])
end end
def object_link_text(object, matches) def object_link_text(object, matches)
milestone_link = escape_once(super) milestone_link = escape_once(super)
reference = object.project&.to_reference_base(project) reference = object.project&.to_reference_base(project)
if reference.present? if reference.present?
"#{milestone_link} <i>in #{reference}</i>".html_safe "#{milestone_link} <i>in #{reference}</i>".html_safe
else else
milestone_link milestone_link
end
end end
end
def object_link_title(object, matches) def object_link_title(object, matches)
nil nil
end
end end
end end
end end
......
...@@ -2,115 +2,117 @@ ...@@ -2,115 +2,117 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces project references with links. module References
class ProjectReferenceFilter < ReferenceFilter # HTML filter that replaces project references with links.
self.reference_type = :project class ProjectReferenceFilter < ReferenceFilter
self.reference_type = :project
# Public: Find `namespace/project>` project references in text
# # Public: Find `namespace/project>` project references in text
# ProjectReferenceFilter.references_in(text) do |match, project| #
# "<a href=...>#{project}></a>" # ProjectReferenceFilter.references_in(text) do |match, project|
# end # "<a href=...>#{project}></a>"
# # end
# text - String text to search. #
# # text - String text to search.
# Yields the String match, and the String project name. #
# # Yields the String match, and the String project name.
# Returns a String replaced with the return of the block. #
def self.references_in(text) # Returns a String replaced with the return of the block.
text.gsub(Project.markdown_reference_pattern) do |match| def self.references_in(text)
yield match, "#{$~[:namespace]}/#{$~[:project]}" text.gsub(Project.markdown_reference_pattern) do |match|
yield match, "#{$~[:namespace]}/#{$~[:project]}"
end
end end
end
def call def call
ref_pattern = Project.markdown_reference_pattern ref_pattern = Project.markdown_reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/ ref_pattern_start = /\A#{ref_pattern}\z/
nodes.each_with_index do |node, index| nodes.each_with_index do |node, index|
if text_node?(node) if text_node?(node)
replace_text_when_pattern_matches(node, index, ref_pattern) do |content| replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
project_link_filter(content) project_link_filter(content)
end end
elsif element_node?(node) elsif element_node?(node)
yield_valid_link(node) do |link, inner_html| yield_valid_link(node) do |link, inner_html|
if link =~ ref_pattern_start if link =~ ref_pattern_start
replace_link_node_with_href(node, index, link) do replace_link_node_with_href(node, index, link) do
project_link_filter(link, link_content: inner_html) project_link_filter(link, link_content: inner_html)
end
end end
end end
end end
end end
end
doc doc
end end
# Replace `namespace/project>` project references in text with links to the referenced # Replace `namespace/project>` project references in text with links to the referenced
# project page. # project page.
# #
# text - String text to replace references in. # text - String text to replace references in.
# link_content - Original content of the link being replaced. # link_content - Original content of the link being replaced.
# #
# Returns a String with `namespace/project>` references replaced with links. All links # Returns a String with `namespace/project>` references replaced with links. All links
# have `gfm` and `gfm-project` class names attached for styling. # have `gfm` and `gfm-project` class names attached for styling.
def project_link_filter(text, link_content: nil) def project_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, project_path| self.class.references_in(text) do |match, project_path|
cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
if project = projects_hash[project_path.downcase] if project = projects_hash[project_path.downcase]
link_to_project(project, link_content: link_content) || match link_to_project(project, link_content: link_content) || match
else else
match match
end
end end
end end
end end
end
# Returns a Hash containing all Project objects for the project # Returns a Hash containing all Project objects for the project
# references in the current document. # references in the current document.
# #
# The keys of this Hash are the project paths, the values the # The keys of this Hash are the project paths, the values the
# corresponding Project objects. # corresponding Project objects.
def projects_hash def projects_hash
@projects ||= Project.eager_load(:route, namespace: [:route]) @projects ||= Project.eager_load(:route, namespace: [:route])
.where_full_path_in(projects) .where_full_path_in(projects)
.index_by(&:full_path) .index_by(&:full_path)
.transform_keys(&:downcase) .transform_keys(&:downcase)
end end
# Returns all projects referenced in the current document. # Returns all projects referenced in the current document.
def projects def projects
refs = Set.new refs = Set.new
nodes.each do |node| nodes.each do |node|
node.to_html.scan(Project.markdown_reference_pattern) do node.to_html.scan(Project.markdown_reference_pattern) do
refs << "#{$~[:namespace]}/#{$~[:project]}" refs << "#{$~[:namespace]}/#{$~[:project]}"
end
end end
end
refs.to_a refs.to_a
end end
private private
def urls def urls
Gitlab::Routing.url_helpers Gitlab::Routing.url_helpers
end end
def link_class def link_class
reference_class(:project) reference_class(:project)
end end
def link_to_project(project, link_content: nil) def link_to_project(project, link_content: nil)
url = urls.project_url(project, only_path: context[:only_path]) url = urls.project_url(project, only_path: context[:only_path])
data = data_attribute(project: project.id) data = data_attribute(project: project.id)
content = link_content || project.to_reference content = link_content || project.to_reference
link_tag(url, data, content, project.name) link_tag(url, data, content, project.name)
end end
def link_tag(url, data, link_content, title) def link_tag(url, data, link_content, title)
%(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end
end end
end end
end end
......
...@@ -3,212 +3,214 @@ ...@@ -3,212 +3,214 @@
# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js # Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js
module Banzai module Banzai
module Filter module Filter
# Base class for GitLab Flavored Markdown reference filters. module References
# # Base class for GitLab Flavored Markdown reference filters.
# References within <pre>, <code>, <a>, and <style> elements are ignored.
#
# Context options:
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
include RequestStoreReferenceCache
include OutputSafety
class << self
attr_accessor :reference_type
def call(doc, context = nil, result = nil)
new(doc, context, result).call_and_update_nodes
end
end
def initialize(doc, context = nil, result = nil)
super
@new_nodes = {}
@nodes = self.result[:reference_filter_nodes]
end
def call_and_update_nodes
with_update_nodes { call }
end
# Returns a data attribute String to attach to a reference link
# #
# attributes - Hash, where the key becomes the data attribute name and the # References within <pre>, <code>, <a>, and <style> elements are ignored.
# value is the data attribute value
# #
# Examples: # Context options:
# # :project (required) - Current project, ignored if reference is cross-project.
# data_attribute(project: 1, issue: 2) # :only_path - Generate path-only links.
# # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" class ReferenceFilter < HTML::Pipeline::Filter
# include RequestStoreReferenceCache
# data_attribute(project: 3, merge_request: 4) include OutputSafety
# # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
# class << self
# Returns a String attr_accessor :reference_type
def data_attribute(attributes = {})
attributes = attributes.reject { |_, v| v.nil? } def call(doc, context = nil, result = nil)
new(doc, context, result).call_and_update_nodes
attributes[:reference_type] ||= self.class.reference_type end
attributes[:container] ||= 'body' end
attributes[:placement] ||= 'top'
attributes.delete(:original) if context[:no_original_data]
attributes.map do |key, value|
%Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
end.join(' ')
end
def ignore_ancestor_query def initialize(doc, context = nil, result = nil)
@ignore_ancestor_query ||= begin super
parents = %w(pre code a style)
parents << 'blockquote' if context[:ignore_blockquotes]
parents.map { |n| "ancestor::#{n}" }.join(' or ') @new_nodes = {}
@nodes = self.result[:reference_filter_nodes]
end end
end
def project def call_and_update_nodes
context[:project] with_update_nodes { call }
end end
def group # Returns a data attribute String to attach to a reference link
context[:group] #
end # attributes - Hash, where the key becomes the data attribute name and the
# value is the data attribute value
#
# Examples:
#
# data_attribute(project: 1, issue: 2)
# # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
#
# data_attribute(project: 3, merge_request: 4)
# # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
#
# Returns a String
def data_attribute(attributes = {})
attributes = attributes.reject { |_, v| v.nil? }
attributes[:reference_type] ||= self.class.reference_type
attributes[:container] ||= 'body'
attributes[:placement] ||= 'top'
attributes.delete(:original) if context[:no_original_data]
attributes.map do |key, value|
%Q(data-#{key.to_s.dasherize}="#{escape_once(value)}")
end.join(' ')
end
def user def ignore_ancestor_query
context[:user] @ignore_ancestor_query ||= begin
end parents = %w(pre code a style)
parents << 'blockquote' if context[:ignore_blockquotes]
def skip_project_check? parents.map { |n| "ancestor::#{n}" }.join(' or ')
context[:skip_project_check] end
end end
def reference_class(type, tooltip: true) def project
gfm_klass = "gfm gfm-#{type}" context[:project]
end
return gfm_klass unless tooltip def group
context[:group]
end
"#{gfm_klass} has-tooltip" def user
end context[:user]
end
# Ensure that a :project key exists in context def skip_project_check?
# context[:skip_project_check]
# Note that while the key might exist, its value could be nil! end
def validate
needs :project unless skip_project_check?
end
# Iterates over all <a> and text() nodes in a document. def reference_class(type, tooltip: true)
# gfm_klass = "gfm gfm-#{type}"
# Nodes are skipped whenever their ancestor is one of the nodes returned
# by `ignore_ancestor_query`. Link tags are not processed if they have a
# "gfm" class or the "href" attribute is empty.
def each_node
return to_enum(__method__) unless block_given?
doc.xpath(query).each do |node| return gfm_klass unless tooltip
yield node
end
end
# Returns an Array containing all HTML nodes. "#{gfm_klass} has-tooltip"
def nodes end
@nodes ||= each_node.to_a
end
# Yields the link's URL and inner HTML whenever the node is a valid <a> tag. # Ensure that a :project key exists in context
def yield_valid_link(node) #
link = unescape_link(node.attr('href').to_s) # Note that while the key might exist, its value could be nil!
inner_html = node.inner_html def validate
needs :project unless skip_project_check?
end
return unless link.force_encoding('UTF-8').valid_encoding? # Iterates over all <a> and text() nodes in a document.
#
# Nodes are skipped whenever their ancestor is one of the nodes returned
# by `ignore_ancestor_query`. Link tags are not processed if they have a
# "gfm" class or the "href" attribute is empty.
def each_node
return to_enum(__method__) unless block_given?
doc.xpath(query).each do |node|
yield node
end
end
yield link, inner_html # Returns an Array containing all HTML nodes.
end def nodes
@nodes ||= each_node.to_a
end
def unescape_link(href) # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
CGI.unescape(href) def yield_valid_link(node)
end link = unescape_link(node.attr('href').to_s)
inner_html = node.inner_html
def replace_text_when_pattern_matches(node, index, pattern) return unless link.force_encoding('UTF-8').valid_encoding?
return unless node.text =~ pattern
content = node.to_html yield link, inner_html
html = yield content end
replace_text_with_html(node, index, html) unless html == content def unescape_link(href)
end CGI.unescape(href)
end
def replace_link_node_with_text(node, index) def replace_text_when_pattern_matches(node, index, pattern)
html = yield return unless node.text =~ pattern
replace_text_with_html(node, index, html) unless html == node.text content = node.to_html
end html = yield content
def replace_link_node_with_href(node, index, link) replace_text_with_html(node, index, html) unless html == content
html = yield end
replace_text_with_html(node, index, html) unless html == link def replace_link_node_with_text(node, index)
end html = yield
def text_node?(node) replace_text_with_html(node, index, html) unless html == node.text
node.is_a?(Nokogiri::XML::Text) end
end
def element_node?(node) def replace_link_node_with_href(node, index, link)
node.is_a?(Nokogiri::XML::Element) html = yield
end
private replace_text_with_html(node, index, html) unless html == link
end
def query def text_node?(node)
@query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})] node.is_a?(Nokogiri::XML::Text)
| descendant-or-self::a[ end
not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
]}
end
def replace_text_with_html(node, index, html) def element_node?(node)
replace_and_update_new_nodes(node, index, html) node.is_a?(Nokogiri::XML::Element)
end end
def replace_and_update_new_nodes(node, index, html) private
previous_node = node.previous
next_node = node.next
parent_node = node.parent
# Unfortunately node.replace(html) returns re-parented nodes, not the actual replaced nodes in the doc
# We need to find the actual nodes in the doc that were replaced
node.replace(html)
@new_nodes[index] = []
# We replaced node with new nodes, so we find first new node. If previous_node is nil, we take first parent child def query
new_node = previous_node ? previous_node.next : parent_node&.children&.first @query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
| descendant-or-self::a[
not(contains(concat(" ", @class, " "), " gfm ")) and not(@href = "")
]}
end
# We iterate from first to last replaced node and store replaced nodes in @new_nodes def replace_text_with_html(node, index, html)
while new_node && new_node != next_node replace_and_update_new_nodes(node, index, html)
@new_nodes[index] << new_node.xpath(query)
new_node = new_node.next
end end
@new_nodes[index].flatten! def replace_and_update_new_nodes(node, index, html)
end previous_node = node.previous
next_node = node.next
parent_node = node.parent
# Unfortunately node.replace(html) returns re-parented nodes, not the actual replaced nodes in the doc
# We need to find the actual nodes in the doc that were replaced
node.replace(html)
@new_nodes[index] = []
# We replaced node with new nodes, so we find first new node. If previous_node is nil, we take first parent child
new_node = previous_node ? previous_node.next : parent_node&.children&.first
# We iterate from first to last replaced node and store replaced nodes in @new_nodes
while new_node && new_node != next_node
@new_nodes[index] << new_node.xpath(query)
new_node = new_node.next
end
@new_nodes[index].flatten!
end
def only_path? def only_path?
context[:only_path] context[:only_path]
end end
def with_update_nodes def with_update_nodes
@new_nodes = {} @new_nodes = {}
yield.tap { update_nodes! } yield.tap { update_nodes! }
end end
# Once Filter completes replacing nodes, we update nodes with @new_nodes # Once Filter completes replacing nodes, we update nodes with @new_nodes
def update_nodes! def update_nodes!
@new_nodes.sort_by { |index, _new_nodes| -index }.each do |index, new_nodes| @new_nodes.sort_by { |index, _new_nodes| -index }.each do |index, new_nodes|
nodes[index, 1] = new_nodes nodes[index, 1] = new_nodes
end
result[:reference_filter_nodes] = nodes
end end
result[:reference_filter_nodes] = nodes
end end
end end
end end
......
...@@ -2,27 +2,29 @@ ...@@ -2,27 +2,29 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces snippet references with links. References to module References
# snippets that do not exist are ignored. # HTML filter that replaces snippet references with links. References to
# # snippets that do not exist are ignored.
# This filter supports cross-project references. #
class SnippetReferenceFilter < AbstractReferenceFilter # This filter supports cross-project references.
self.reference_type = :snippet class SnippetReferenceFilter < AbstractReferenceFilter
self.reference_type = :snippet
def self.object_class def self.object_class
Snippet Snippet
end end
def find_object(project, id) def find_object(project, id)
return unless project.is_a?(Project) return unless project.is_a?(Project)
project.snippets.find_by(id: id) project.snippets.find_by(id: id)
end end
def url_for_object(snippet, project) def url_for_object(snippet, project)
h = Gitlab::Routing.url_helpers h = Gitlab::Routing.url_helpers
h.project_snippet_url(project, snippet, h.project_snippet_url(project, snippet,
only_path: context[:only_path]) only_path: context[:only_path])
end
end end
end end
end end
......
...@@ -2,178 +2,180 @@ ...@@ -2,178 +2,180 @@
module Banzai module Banzai
module Filter module Filter
# HTML filter that replaces user or group references with links. module References
# # HTML filter that replaces user or group references with links.
# A special `@all` reference is also supported.
class UserReferenceFilter < ReferenceFilter
self.reference_type = :user
# Public: Find `@user` user references in text
#
# UserReferenceFilter.references_in(text) do |match, username|
# "<a href=...>@#{user}</a>"
# end
#
# text - String text to search.
#
# Yields the String match, and the String user name.
# #
# Returns a String replaced with the return of the block. # A special `@all` reference is also supported.
def self.references_in(text) class UserReferenceFilter < ReferenceFilter
text.gsub(User.reference_pattern) do |match| self.reference_type = :user
yield match, $~[:user]
# Public: Find `@user` user references in text
#
# UserReferenceFilter.references_in(text) do |match, username|
# "<a href=...>@#{user}</a>"
# end
#
# text - String text to search.
#
# Yields the String match, and the String user name.
#
# Returns a String replaced with the return of the block.
def self.references_in(text)
text.gsub(User.reference_pattern) do |match|
yield match, $~[:user]
end
end end
end
def call def call
return doc if project.nil? && group.nil? && !skip_project_check? return doc if project.nil? && group.nil? && !skip_project_check?
ref_pattern = User.reference_pattern ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/ ref_pattern_start = /\A#{ref_pattern}\z/
nodes.each_with_index do |node, index| nodes.each_with_index do |node, index|
if text_node?(node) if text_node?(node)
replace_text_when_pattern_matches(node, index, ref_pattern) do |content| replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
user_link_filter(content) user_link_filter(content)
end end
elsif element_node?(node) elsif element_node?(node)
yield_valid_link(node) do |link, inner_html| yield_valid_link(node) do |link, inner_html|
if link =~ ref_pattern_start if link =~ ref_pattern_start
replace_link_node_with_href(node, index, link) do replace_link_node_with_href(node, index, link) do
user_link_filter(link, link_content: inner_html) user_link_filter(link, link_content: inner_html)
end
end end
end end
end end
end end
end
doc doc
end end
# Replace `@user` user references in text with links to the referenced # Replace `@user` user references in text with links to the referenced
# user's profile page. # user's profile page.
# #
# text - String text to replace references in. # text - String text to replace references in.
# link_content - Original content of the link being replaced. # link_content - Original content of the link being replaced.
# #
# Returns a String with `@user` references replaced with links. All links # Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling. # have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil) def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username| self.class.references_in(text) do |match, username|
if username == 'all' && !skip_project_check? if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content) link_to_all(link_content: link_content)
else else
cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do cached_call(:banzai_url_for_object, match, path: [User, username.downcase]) do
if namespace = namespaces[username.downcase] if namespace = namespaces[username.downcase]
link_to_namespace(namespace, link_content: link_content) || match link_to_namespace(namespace, link_content: link_content) || match
else else
match match
end
end end
end end
end end
end end
end
# Returns a Hash containing all Namespace objects for the username # Returns a Hash containing all Namespace objects for the username
# references in the current document. # references in the current document.
# #
# The keys of this Hash are the namespace paths, the values the # The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects. # corresponding Namespace objects.
def namespaces def namespaces
@namespaces ||= Namespace.eager_load(:owner, :route) @namespaces ||= Namespace.eager_load(:owner, :route)
.where_full_path_in(usernames) .where_full_path_in(usernames)
.index_by(&:full_path) .index_by(&:full_path)
.transform_keys(&:downcase) .transform_keys(&:downcase)
end end
# Returns all usernames referenced in the current document. # Returns all usernames referenced in the current document.
def usernames def usernames
refs = Set.new refs = Set.new
nodes.each do |node| nodes.each do |node|
node.to_html.scan(User.reference_pattern) do node.to_html.scan(User.reference_pattern) do
refs << $~[:user] refs << $~[:user]
end
end end
end
refs.to_a refs.to_a
end end
private private
def urls def urls
Gitlab::Routing.url_helpers Gitlab::Routing.url_helpers
end end
def link_class def link_class
[reference_class(:project_member, tooltip: false), "js-user-link"].join(" ") [reference_class(:project_member, tooltip: false), "js-user-link"].join(" ")
end end
def link_to_all(link_content: nil) def link_to_all(link_content: nil)
author = context[:author] author = context[:author]
if author && !team_member?(author) if author && !team_member?(author)
link_content link_content
else else
parent_url(link_content, author) parent_url(link_content, author)
end
end end
end
def link_to_namespace(namespace, link_content: nil) def link_to_namespace(namespace, link_content: nil)
if namespace.is_a?(Group) if namespace.is_a?(Group)
link_to_group(namespace.full_path, namespace, link_content: link_content) link_to_group(namespace.full_path, namespace, link_content: link_content)
else else
link_to_user(namespace.path, namespace, link_content: link_content) link_to_user(namespace.path, namespace, link_content: link_content)
end
end end
end
def link_to_group(group, namespace, link_content: nil)
url = urls.group_url(group, only_path: context[:only_path])
data = data_attribute(group: namespace.id)
content = link_content || Group.reference_prefix + group
link_tag(url, data, content, namespace.full_name) def link_to_group(group, namespace, link_content: nil)
end url = urls.group_url(group, only_path: context[:only_path])
data = data_attribute(group: namespace.id)
content = link_content || Group.reference_prefix + group
def link_to_user(user, namespace, link_content: nil) link_tag(url, data, content, namespace.full_name)
url = urls.user_url(user, only_path: context[:only_path]) end
data = data_attribute(user: namespace.owner_id)
content = link_content || User.reference_prefix + user
link_tag(url, data, content, namespace.owner_name) def link_to_user(user, namespace, link_content: nil)
end url = urls.user_url(user, only_path: context[:only_path])
data = data_attribute(user: namespace.owner_id)
content = link_content || User.reference_prefix + user
def link_tag(url, data, link_content, title) link_tag(url, data, content, namespace.owner_name)
%(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>) end
end
def parent def link_tag(url, data, link_content, title)
context[:project] || context[:group] %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end end
def parent_group? def parent
parent.is_a?(Group) context[:project] || context[:group]
end end
def team_member?(user) def parent_group?
if parent_group? parent.is_a?(Group)
parent.member?(user)
else
parent.team.member?(user)
end end
end
def parent_url(link_content, author) def team_member?(user)
if parent_group? if parent_group?
url = urls.group_url(parent, only_path: context[:only_path]) parent.member?(user)
data = data_attribute(group: group.id, author: author.try(:id)) else
else parent.team.member?(user)
url = urls.project_url(parent, only_path: context[:only_path]) end
data = data_attribute(project: project.id, author: author.try(:id))
end end
content = link_content || User.reference_prefix + 'all' def parent_url(link_content, author)
link_tag(url, data, content, 'All Project and Group Members') if parent_group?
url = urls.group_url(parent, only_path: context[:only_path])
data = data_attribute(group: group.id, author: author.try(:id))
else
url = urls.project_url(parent, only_path: context[:only_path])
data = data_attribute(project: project.id, author: author.try(:id))
end
content = link_content || User.reference_prefix + 'all'
link_tag(url, data, content, 'All Project and Group Members')
end
end end
end end
end end
......
...@@ -2,21 +2,23 @@ ...@@ -2,21 +2,23 @@
module Banzai module Banzai
module Filter module Filter
# The actual filter is implemented in the EE mixin module References
class VulnerabilityReferenceFilter < IssuableReferenceFilter # The actual filter is implemented in the EE mixin
self.reference_type = :vulnerability class VulnerabilityReferenceFilter < IssuableReferenceFilter
self.reference_type = :vulnerability
def self.object_class def self.object_class
Vulnerability Vulnerability
end end
private private
def project def project
context[:project] context[:project]
end
end end
end end
end end
end end
Banzai::Filter::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::VulnerabilityReferenceFilter') Banzai::Filter::References::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::VulnerabilityReferenceFilter')
...@@ -51,19 +51,19 @@ module Banzai ...@@ -51,19 +51,19 @@ module Banzai
def self.reference_filters def self.reference_filters
[ [
Filter::UserReferenceFilter, Filter::References::UserReferenceFilter,
Filter::ProjectReferenceFilter, Filter::References::ProjectReferenceFilter,
Filter::DesignReferenceFilter, Filter::References::DesignReferenceFilter,
Filter::IssueReferenceFilter, Filter::References::IssueReferenceFilter,
Filter::ExternalIssueReferenceFilter, Filter::References::ExternalIssueReferenceFilter,
Filter::MergeRequestReferenceFilter, Filter::References::MergeRequestReferenceFilter,
Filter::SnippetReferenceFilter, Filter::References::SnippetReferenceFilter,
Filter::CommitRangeReferenceFilter, Filter::References::CommitRangeReferenceFilter,
Filter::CommitReferenceFilter, Filter::References::CommitReferenceFilter,
Filter::LabelReferenceFilter, Filter::References::LabelReferenceFilter,
Filter::MilestoneReferenceFilter, Filter::References::MilestoneReferenceFilter,
Filter::AlertReferenceFilter, Filter::References::AlertReferenceFilter,
Filter::FeatureFlagReferenceFilter Filter::References::FeatureFlagReferenceFilter
] ]
end end
......
...@@ -6,7 +6,7 @@ module Banzai ...@@ -6,7 +6,7 @@ module Banzai
def self.filters def self.filters
@filters ||= FilterArray[ @filters ||= FilterArray[
Filter::SanitizationFilter, Filter::SanitizationFilter,
Filter::LabelReferenceFilter Filter::References::LabelReferenceFilter
] ]
end end
end end
......
...@@ -17,15 +17,15 @@ module Banzai ...@@ -17,15 +17,15 @@ module Banzai
def self.reference_filters def self.reference_filters
[ [
Filter::UserReferenceFilter, Filter::References::UserReferenceFilter,
Filter::IssueReferenceFilter, Filter::References::IssueReferenceFilter,
Filter::ExternalIssueReferenceFilter, Filter::References::ExternalIssueReferenceFilter,
Filter::MergeRequestReferenceFilter, Filter::References::MergeRequestReferenceFilter,
Filter::SnippetReferenceFilter, Filter::References::SnippetReferenceFilter,
Filter::CommitRangeReferenceFilter, Filter::References::CommitRangeReferenceFilter,
Filter::CommitReferenceFilter, Filter::References::CommitReferenceFilter,
Filter::AlertReferenceFilter, Filter::References::AlertReferenceFilter,
Filter::FeatureFlagReferenceFilter Filter::References::FeatureFlagReferenceFilter
] ]
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::AbstractReferenceFilter do RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:doc) { Nokogiri::HTML.fragment('') } let(:doc) { Nokogiri::HTML.fragment('') }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::AlertReferenceFilter do RSpec.describe Banzai::Filter::References::AlertReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::CommitRangeReferenceFilter do RSpec.describe Banzai::Filter::References::CommitRangeReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::CommitReferenceFilter do RSpec.describe Banzai::Filter::References::CommitReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::DesignReferenceFilter do RSpec.describe Banzai::Filter::References::DesignReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
include DesignManagementTestHelpers include DesignManagementTestHelpers
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::ExternalIssueReferenceFilter do RSpec.describe Banzai::Filter::References::ExternalIssueReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let_it_be_with_refind(:project) { create(:project) } let_it_be_with_refind(:project) { create(:project) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::FeatureFlagReferenceFilter do RSpec.describe Banzai::Filter::References::FeatureFlagReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::IssueReferenceFilter do RSpec.describe Banzai::Filter::References::IssueReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
include DesignManagementTestHelpers include DesignManagementTestHelpers
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
require 'html/pipeline' require 'html/pipeline'
RSpec.describe Banzai::Filter::LabelReferenceFilter do RSpec.describe Banzai::Filter::References::LabelReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project, :public, name: 'sample-project') } let(:project) { create(:project, :public, name: 'sample-project') }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::MergeRequestReferenceFilter do RSpec.describe Banzai::Filter::References::MergeRequestReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::MilestoneReferenceFilter do RSpec.describe Banzai::Filter::References::MilestoneReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let_it_be(:parent_group) { create(:group, :public) } let_it_be(:parent_group) { create(:group, :public) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::ProjectReferenceFilter do RSpec.describe Banzai::Filter::References::ProjectReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
def invalidate_reference(reference) def invalidate_reference(reference)
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::ReferenceFilter do RSpec.describe Banzai::Filter::References::ReferenceFilter do
let(:project) { build_stubbed(:project) } let(:project) { build_stubbed(:project) }
describe '#each_node' do describe '#each_node' do
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::SnippetReferenceFilter do RSpec.describe Banzai::Filter::References::SnippetReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Banzai::Filter::UserReferenceFilter do RSpec.describe Banzai::Filter::References::UserReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
def get_reference(user) def get_reference(user)
......
...@@ -25,7 +25,7 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do ...@@ -25,7 +25,7 @@ RSpec.describe Banzai::Pipeline::GfmPipeline do
issue = create(:issue, project: project) issue = create(:issue, project: project)
markdown = "text #{issue.to_reference(project, full: true)}" markdown = "text #{issue.to_reference(project, full: true)}"
expect_any_instance_of(Banzai::Filter::ReferenceFilter).to receive(:each_node).once expect_any_instance_of(Banzai::Filter::References::ReferenceFilter).to receive(:each_node).once
described_class.call(markdown, project: project) described_class.call(markdown, project: project)
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment