Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-05-19 18:44:42 +0300
commit4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch)
tree5423a1c7516cffe36384133ade12572cf709398d /lib/banzai/filter
parente570267f2f6b326480d284e0164a6464ba4081bc (diff)
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'lib/banzai/filter')
-rw-r--r--lib/banzai/filter/base_relative_link_filter.rb33
-rw-r--r--lib/banzai/filter/custom_emoji_filter.rb10
-rw-r--r--lib/banzai/filter/markdown_pre_escape_filter.rb2
-rw-r--r--lib/banzai/filter/references/abstract_reference_filter.rb192
-rw-r--r--lib/banzai/filter/references/alert_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/commit_range_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/commit_reference_filter.rb17
-rw-r--r--lib/banzai/filter/references/design_reference_filter.rb20
-rw-r--r--lib/banzai/filter/references/epic_reference_filter.rb2
-rw-r--r--lib/banzai/filter/references/external_issue_reference_filter.rb37
-rw-r--r--lib/banzai/filter/references/feature_flag_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/issuable_reference_filter.rb6
-rw-r--r--lib/banzai/filter/references/issue_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/iteration_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/label_reference_filter.rb11
-rw-r--r--lib/banzai/filter/references/merge_request_reference_filter.rb5
-rw-r--r--lib/banzai/filter/references/milestone_reference_filter.rb7
-rw-r--r--lib/banzai/filter/references/project_reference_filter.rb36
-rw-r--r--lib/banzai/filter/references/reference_cache.rb178
-rw-r--r--lib/banzai/filter/references/reference_filter.rb137
-rw-r--r--lib/banzai/filter/references/snippet_reference_filter.rb11
-rw-r--r--lib/banzai/filter/references/user_reference_filter.rb36
-rw-r--r--lib/banzai/filter/references/vulnerability_reference_filter.rb13
-rw-r--r--lib/banzai/filter/sanitization_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb2
-rw-r--r--lib/banzai/filter/upload_link_filter.rb12
26 files changed, 407 insertions, 395 deletions
diff --git a/lib/banzai/filter/base_relative_link_filter.rb b/lib/banzai/filter/base_relative_link_filter.rb
index fd526df4c48..84a6e18e77b 100644
--- a/lib/banzai/filter/base_relative_link_filter.rb
+++ b/lib/banzai/filter/base_relative_link_filter.rb
@@ -10,19 +10,16 @@ module Banzai
protected
def linkable_attributes
- strong_memoize(:linkable_attributes) do
- attrs = []
-
- attrs += doc.search('a:not(.gfm)').map do |el|
- el.attribute('href')
- end
-
- attrs += doc.search('img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
- [el.attribute('src'), el.attribute('data-src')]
- end
-
- attrs.reject do |attr|
- attr.blank? || attr.value.start_with?('//')
+ if Feature.enabled?(:optimize_linkable_attributes, project, default_enabled: :yaml)
+ # Nokorigi Nodeset#search performs badly for documents with many nodes
+ #
+ # Here we store fetched attributes in the shared variable "result"
+ # This variable is passed through the chain of filters and can be
+ # accessed by them
+ result[:linkable_attributes] ||= fetch_linkable_attributes
+ else
+ strong_memoize(:linkable_attributes) do
+ fetch_linkable_attributes
end
end
end
@@ -40,6 +37,16 @@ module Banzai
def unescape_and_scrub_uri(uri)
Addressable::URI.unescape(uri).scrub.delete("\0")
end
+
+ def fetch_linkable_attributes
+ attrs = []
+
+ attrs += doc.search('a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
+ [el.attribute('href'), el.attribute('src'), el.attribute('data-src')]
+ end
+
+ attrs.reject { |attr| attr.blank? || attr.value.start_with?('//') }
+ end
end
end
end
diff --git a/lib/banzai/filter/custom_emoji_filter.rb b/lib/banzai/filter/custom_emoji_filter.rb
index 1ee8f4e31e8..3171231dc9b 100644
--- a/lib/banzai/filter/custom_emoji_filter.rb
+++ b/lib/banzai/filter/custom_emoji_filter.rb
@@ -3,6 +3,8 @@
module Banzai
module Filter
class CustomEmojiFilter < HTML::Pipeline::Filter
+ include Gitlab::Utils::StrongMemoize
+
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call
@@ -14,7 +16,7 @@ module Banzai
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
next unless content.include?(':')
- next unless namespace && namespace.custom_emoji.any?
+ next unless has_custom_emoji?
html = custom_emoji_name_element_filter(content)
@@ -46,6 +48,12 @@ module Banzai
private
+ def has_custom_emoji?
+ strong_memoize(:has_custom_emoji) do
+ namespace&.custom_emoji&.any?
+ end
+ end
+
def namespace
context[:project].namespace.root_ancestor
end
diff --git a/lib/banzai/filter/markdown_pre_escape_filter.rb b/lib/banzai/filter/markdown_pre_escape_filter.rb
index bedc2d0fd04..0c53444681d 100644
--- a/lib/banzai/filter/markdown_pre_escape_filter.rb
+++ b/lib/banzai/filter/markdown_pre_escape_filter.rb
@@ -26,7 +26,7 @@ module Banzai
class MarkdownPreEscapeFilter < HTML::Pipeline::TextFilter
# We just need to target those that are special GitLab references
REFERENCE_CHARACTERS = '@#!$&~%^'
- ASCII_PUNCTUATION = %r{([\\][#{REFERENCE_CHARACTERS}])}.freeze
+ ASCII_PUNCTUATION = %r{(\\[#{REFERENCE_CHARACTERS}])}.freeze
LITERAL_KEYWORD = 'cmliteral'
def call
diff --git a/lib/banzai/filter/references/abstract_reference_filter.rb b/lib/banzai/filter/references/abstract_reference_filter.rb
index 7109373dbce..08014ccdcce 100644
--- a/lib/banzai/filter/references/abstract_reference_filter.rb
+++ b/lib/banzai/filter/references/abstract_reference_filter.rb
@@ -8,6 +8,12 @@ module Banzai
class AbstractReferenceFilter < ReferenceFilter
include CrossProjectReference
+ def initialize(doc, context = nil, result = nil)
+ super
+
+ @reference_cache = ReferenceCache.new(self, context)
+ end
+
# REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found
# reference (which we replace with placeholder during re-scaping). The
# random number helps ensure it's pretty close to unique. Since it's a
@@ -16,22 +22,9 @@ module Banzai
REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_"
REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze
- def self.object_class
- # Implement in child class
- # Example: MergeRequest
- end
-
- def self.object_name
- @object_name ||= object_class.name.underscore
- end
-
- def self.object_sym
- @object_sym ||= object_name.to_sym
- end
-
# Public: Find references in text (like `!123` for merge requests)
#
- # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
+ # references_in(text) do |match, id, project_ref, matches|
# object = find_object(project_ref, id)
# "<a href=...>#{object.to_reference}</a>"
# end
@@ -42,7 +35,7 @@ module Banzai
# of the external project reference, and all of the matchdata.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text, pattern = object_class.reference_pattern)
+ def references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
if ident = identifier($~)
yield match, ident, $~[:project], $~[:namespace], $~
@@ -52,17 +45,13 @@ module Banzai
end
end
- def self.identifier(match_data)
+ def identifier(match_data)
symbol = symbol_from_match(match_data)
parse_symbol(symbol, match_data) if object_class.reference_valid?(symbol)
end
- def identifier(match_data)
- self.class.identifier(match_data)
- end
-
- def self.symbol_from_match(match)
+ def symbol_from_match(match)
key = object_sym
match[key] if match.names.include?(key.to_s)
end
@@ -72,7 +61,7 @@ module Banzai
#
# 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)
+ def parse_symbol(symbol, match_data)
symbol.to_i
end
@@ -84,21 +73,10 @@ module Banzai
record.id
end
- def object_class
- self.class.object_class
- end
-
- def object_sym
- self.class.object_sym
- end
-
- def references_in(*args, &block)
- self.class.references_in(*args, &block)
- end
-
# Implement in child class
# Example: project.merge_requests.find
def find_object(parent_object, id)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end
# Override if the link reference pattern produces a different ID (global
@@ -110,6 +88,7 @@ module Banzai
# Implement in child class
# Example: project_merge_request_url
def url_for_object(object, parent_object)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
end
def find_object_cached(parent_object, id)
@@ -139,7 +118,9 @@ module Banzai
def call
return doc unless project || group || user
- ref_pattern = object_class.reference_pattern
+ reference_cache.load_reference_cache(nodes) if respond_to?(:parent_records)
+
+ ref_pattern = object_reference_pattern
link_pattern = object_class.link_reference_pattern
# Compile often used regexps only once outside of the loop
@@ -201,9 +182,9 @@ module Banzai
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
parent_path = if parent_type == :group
- full_group_path(namespace_ref)
+ reference_cache.full_group_path(namespace_ref)
else
- full_project_path(namespace_ref, project_ref)
+ reference_cache.full_project_path(namespace_ref, project_ref)
end
parent = from_ref_cached(parent_path)
@@ -290,127 +271,6 @@ module Banzai
text
end
- # Returns a Hash containing all object references (e.g. issue IDs) per the
- # project they belong to.
- def references_per_parent
- @references_per ||= {}
-
- @references_per[parent_type] ||= begin
- refs = Hash.new { |hash, key| hash[key] = Set.new }
- regex = [
- object_class.link_reference_pattern,
- object_class.reference_pattern
- ].compact.reduce { |a, b| Regexp.union(a, b) }
-
- nodes.each do |node|
- node.to_html.scan(regex) do
- path = if parent_type == :project
- full_project_path($~[:namespace], $~[:project])
- else
- full_group_path($~[:group])
- end
-
- if ident = identifier($~)
- refs[path] << ident
- end
- end
- end
-
- refs
- end
- end
-
- # Returns a Hash containing referenced projects grouped per their full
- # path.
- def parent_per_reference
- @per_reference ||= {}
-
- @per_reference[parent_type] ||= begin
- refs = Set.new
-
- references_per_parent.each do |ref, _|
- refs << ref
- end
-
- find_for_paths(refs.to_a).index_by(&:full_path)
- end
- end
-
- def relation_for_paths(paths)
- klass = parent_type.to_s.camelize.constantize
- result = klass.where_full_path_in(paths)
- return result if parent_type == :group
-
- result.includes(:namespace) if parent_type == :project
- end
-
- # Returns projects for the given paths.
- def find_for_paths(paths)
- if Gitlab::SafeRequestStore.active?
- cache = refs_cache
- to_query = paths - cache.keys
-
- unless to_query.empty?
- records = relation_for_paths(to_query)
-
- found = []
- records.each do |record|
- ref = record.full_path
- get_or_set_cache(cache, ref) { record }
- found << ref
- end
-
- not_found = to_query - found
- not_found.each do |ref|
- get_or_set_cache(cache, ref) { nil }
- end
- end
-
- cache.slice(*paths).values.compact
- else
- relation_for_paths(paths)
- end
- end
-
- def current_parent_path
- @current_parent_path ||= parent&.full_path
- end
-
- def current_project_namespace_path
- @current_project_namespace_path ||= project&.namespace&.full_path
- end
-
- def records_per_parent
- @_records_per_project ||= {}
-
- @_records_per_project[object_class.to_s.underscore] ||= begin
- hash = Hash.new { |h, k| h[k] = {} }
-
- parent_per_reference.each do |path, parent|
- record_ids = references_per_parent[path]
-
- parent_records(parent, record_ids).each do |record|
- hash[parent][record_identifier(record)] = record
- end
- end
-
- hash
- end
- end
-
- private
-
- def full_project_path(namespace, project_ref)
- return current_parent_path unless project_ref
-
- namespace_ref = namespace || current_project_namespace_path
- "#{namespace_ref}/#{project_ref}"
- end
-
- def refs_cache
- Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
- end
-
def parent_type
:project
end
@@ -419,19 +279,9 @@ module Banzai
parent_type == :project ? project : group
end
- def full_group_path(group_ref)
- return current_parent_path unless group_ref
-
- group_ref
- end
-
- def unescape_html_entities(text)
- CGI.unescapeHTML(text.to_s)
- end
+ private
- def escape_html_entities(text)
- CGI.escapeHTML(text.to_s)
- end
+ attr_accessor :reference_cache
def escape_with_placeholders(text, placeholder_data)
escaped = escape_html_entities(text)
@@ -444,5 +294,3 @@ module Banzai
end
end
end
-
-Banzai::Filter::References::AbstractReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::AbstractReferenceFilter')
diff --git a/lib/banzai/filter/references/alert_reference_filter.rb b/lib/banzai/filter/references/alert_reference_filter.rb
index 90fef536605..512d4028520 100644
--- a/lib/banzai/filter/references/alert_reference_filter.rb
+++ b/lib/banzai/filter/references/alert_reference_filter.rb
@@ -5,12 +5,9 @@ module Banzai
module References
class AlertReferenceFilter < IssuableReferenceFilter
self.reference_type = :alert
+ self.object_class = AlertManagement::Alert
- def self.object_class
- AlertManagement::Alert
- end
-
- def self.object_sym
+ def object_sym
:alert
end
diff --git a/lib/banzai/filter/references/commit_range_reference_filter.rb b/lib/banzai/filter/references/commit_range_reference_filter.rb
index ad79f8a173c..df7f42eaa70 100644
--- a/lib/banzai/filter/references/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/references/commit_range_reference_filter.rb
@@ -8,12 +8,9 @@ module Banzai
# This filter supports cross-project references.
class CommitRangeReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit_range
+ self.object_class = CommitRange
- def self.object_class
- CommitRange
- end
-
- def self.references_in(text, pattern = CommitRange.reference_pattern)
+ def references_in(text, pattern = object_reference_pattern)
text.gsub(pattern) do |match|
yield match, $~[:commit_range], $~[:project], $~[:namespace], $~
end
diff --git a/lib/banzai/filter/references/commit_reference_filter.rb b/lib/banzai/filter/references/commit_reference_filter.rb
index 457921bd07d..157dc696cc8 100644
--- a/lib/banzai/filter/references/commit_reference_filter.rb
+++ b/lib/banzai/filter/references/commit_reference_filter.rb
@@ -8,12 +8,9 @@ module Banzai
# This filter supports cross-project references.
class CommitReferenceFilter < AbstractReferenceFilter
self.reference_type = :commit
+ self.object_class = Commit
- def self.object_class
- Commit
- end
-
- def self.references_in(text, pattern = Commit.reference_pattern)
+ def references_in(text, pattern = object_reference_pattern)
text.gsub(pattern) do |match|
yield match, $~[:commit], $~[:project], $~[:namespace], $~
end
@@ -22,7 +19,7 @@ module Banzai
def find_object(project, id)
return unless project.is_a?(Project) && project.valid_repo?
- _, record = records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
+ _, record = reference_cache.records_per_parent[project].detect { |k, _v| Gitlab::Git.shas_eql?(k, id) }
record
end
@@ -31,7 +28,7 @@ module Banzai
return [] unless noteable.is_a?(MergeRequest)
@referenced_merge_request_commit_shas ||= begin
- referenced_shas = references_per_parent.values.reduce(:|).to_a
+ referenced_shas = reference_cache.references_per_parent.values.reduce(:|).to_a
noteable.all_commit_shas.select do |sha|
referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
end
@@ -39,7 +36,7 @@ module Banzai
end
# The default behaviour is `#to_i` - we just pass the hash through.
- def self.parse_symbol(sha_hash, _match)
+ def parse_symbol(sha_hash, _match)
sha_hash
end
@@ -69,12 +66,12 @@ module Banzai
extras
end
- private
-
def parent_records(parent, ids)
parent.commits_by(oids: ids.to_a)
end
+ private
+
def noteable
context[:noteable]
end
diff --git a/lib/banzai/filter/references/design_reference_filter.rb b/lib/banzai/filter/references/design_reference_filter.rb
index 61234e61c15..01e1036dcec 100644
--- a/lib/banzai/filter/references/design_reference_filter.rb
+++ b/lib/banzai/filter/references/design_reference_filter.rb
@@ -33,9 +33,10 @@ module Banzai
end
self.reference_type = :design
+ self.object_class = ::DesignManagement::Design
def find_object(project, identifier)
- records_per_parent[project][identifier]
+ reference_cache.records_per_parent[project][identifier]
end
def parent_records(project, identifiers)
@@ -58,15 +59,6 @@ module Banzai
super.includes(:route, :namespace, :group)
end
- def parent_type
- :project
- end
-
- # optimisation to reuse the parent_per_reference query information
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
- end
-
def url_for_object(design, project)
path_options = { vueroute: design.filename }
Gitlab::Routing.url_helpers.designs_project_issue_path(project, design.issue, path_options)
@@ -76,15 +68,11 @@ module Banzai
super.merge(issue: design.issue_id)
end
- def self.object_class
- ::DesignManagement::Design
- end
-
- def self.object_sym
+ def object_sym
:design
end
- def self.parse_symbol(raw, match_data)
+ def parse_symbol(raw, match_data)
filename = match_data[:url_filename]
iid = match_data[:issue].to_i
Identifier.new(filename: CGI.unescape(filename), issue_iid: iid)
diff --git a/lib/banzai/filter/references/epic_reference_filter.rb b/lib/banzai/filter/references/epic_reference_filter.rb
index 4ee446e5317..e25734c8b0f 100644
--- a/lib/banzai/filter/references/epic_reference_filter.rb
+++ b/lib/banzai/filter/references/epic_reference_filter.rb
@@ -21,4 +21,4 @@ module Banzai
end
end
-Banzai::Filter::References::EpicReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::EpicReferenceFilter')
+Banzai::Filter::References::EpicReferenceFilter.prepend_mod_with('Banzai::Filter::References::EpicReferenceFilter')
diff --git a/lib/banzai/filter/references/external_issue_reference_filter.rb b/lib/banzai/filter/references/external_issue_reference_filter.rb
index 247e20967df..1061a9917dd 100644
--- a/lib/banzai/filter/references/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/references/external_issue_reference_filter.rb
@@ -10,10 +10,11 @@ module Banzai
# This filter does not support cross-project references.
class ExternalIssueReferenceFilter < ReferenceFilter
self.reference_type = :external_issue
+ self.object_class = ExternalIssue
# Public: Find `JIRA-123` issue references in text
#
- # ExternalIssueReferenceFilter.references_in(text, pattern) do |match, issue|
+ # references_in(text, pattern) do |match, issue|
# "<a href=...>##{issue}</a>"
# end
#
@@ -22,7 +23,7 @@ module Banzai
# 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)
+ def references_in(text, pattern = object_reference_pattern)
text.gsub(pattern) do |match|
yield match, $~[:issue]
end
@@ -32,27 +33,7 @@ module Banzai
# Early return if the project isn't using an external tracker
return doc if project.nil? || default_issues_tracker?
- ref_pattern = issue_reference_pattern
- ref_start_pattern = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- issue_link_filter(content)
- end
-
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_start_pattern
- replace_link_node_with_href(node, index, link) do
- issue_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
+ super
end
private
@@ -65,8 +46,8 @@ module Banzai
#
# Returns a String with `JIRA-123` references replaced with links. All
# links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text, link_content: nil)
- self.class.references_in(text, issue_reference_pattern) do |match, id|
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text) do |match, id|
url = url_for_issue(id)
klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
@@ -97,14 +78,10 @@ module Banzai
external_issues_cached(:default_issues_tracker?)
end
- def issue_reference_pattern
+ def object_reference_pattern
external_issues_cached(:external_issue_reference_pattern)
end
- def project
- context[:project]
- end
-
def issue_title
"Issue in #{project.external_issue_tracker.title}"
end
diff --git a/lib/banzai/filter/references/feature_flag_reference_filter.rb b/lib/banzai/filter/references/feature_flag_reference_filter.rb
index be9ded1ff43..0fb2b1b3b24 100644
--- a/lib/banzai/filter/references/feature_flag_reference_filter.rb
+++ b/lib/banzai/filter/references/feature_flag_reference_filter.rb
@@ -5,12 +5,9 @@ module Banzai
module References
class FeatureFlagReferenceFilter < IssuableReferenceFilter
self.reference_type = :feature_flag
+ self.object_class = Operations::FeatureFlag
- def self.object_class
- Operations::FeatureFlag
- end
-
- def self.object_sym
+ def object_sym
:feature_flag
end
diff --git a/lib/banzai/filter/references/issuable_reference_filter.rb b/lib/banzai/filter/references/issuable_reference_filter.rb
index b8ccb926ae9..6349f8542ca 100644
--- a/lib/banzai/filter/references/issuable_reference_filter.rb
+++ b/lib/banzai/filter/references/issuable_reference_filter.rb
@@ -9,11 +9,7 @@ module Banzai
end
def find_object(parent, iid)
- records_per_parent[parent][iid]
- end
-
- def parent_from_ref(ref)
- parent_per_reference[ref || current_parent_path]
+ reference_cache.records_per_parent[parent][iid]
end
end
end
diff --git a/lib/banzai/filter/references/issue_reference_filter.rb b/lib/banzai/filter/references/issue_reference_filter.rb
index eacf261b15f..1053501de7b 100644
--- a/lib/banzai/filter/references/issue_reference_filter.rb
+++ b/lib/banzai/filter/references/issue_reference_filter.rb
@@ -13,10 +13,7 @@ module Banzai
# to reference issues from other GitLab projects.
class IssueReferenceFilter < IssuableReferenceFilter
self.reference_type = :issue
-
- def self.object_class
- Issue
- end
+ self.object_class = Issue
def url_for_object(issue, project)
return issue_path(issue, project) if only_path?
diff --git a/lib/banzai/filter/references/iteration_reference_filter.rb b/lib/banzai/filter/references/iteration_reference_filter.rb
index cf3d446147f..591e07013c3 100644
--- a/lib/banzai/filter/references/iteration_reference_filter.rb
+++ b/lib/banzai/filter/references/iteration_reference_filter.rb
@@ -6,13 +6,10 @@ module Banzai
# The actual filter is implemented in the EE mixin
class IterationReferenceFilter < AbstractReferenceFilter
self.reference_type = :iteration
-
- def self.object_class
- Iteration
- end
+ self.object_class = Iteration
end
end
end
end
-Banzai::Filter::References::IterationReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::IterationReferenceFilter')
+Banzai::Filter::References::IterationReferenceFilter.prepend_mod_with('Banzai::Filter::References::IterationReferenceFilter')
diff --git a/lib/banzai/filter/references/label_reference_filter.rb b/lib/banzai/filter/references/label_reference_filter.rb
index a6a5eec5d9a..bf6b3e47d3b 100644
--- a/lib/banzai/filter/references/label_reference_filter.rb
+++ b/lib/banzai/filter/references/label_reference_filter.rb
@@ -6,10 +6,7 @@ module Banzai
# HTML filter that replaces label references with links.
class LabelReferenceFilter < AbstractReferenceFilter
self.reference_type = :label
-
- def self.object_class
- Label
- end
+ self.object_class = Label
def find_object(parent_object, id)
find_labels(parent_object).find(id)
@@ -20,7 +17,7 @@ module Banzai
unescaped_html = unescape_html_entities(text).gsub(pattern) do |match|
namespace = $~[:namespace]
project = $~[:project]
- project_path = full_project_path(namespace, project)
+ project_path = reference_cache.full_project_path(namespace, project)
label = find_label_cached(project_path, $~[:label_id], $~[:label_name])
if label
@@ -96,7 +93,7 @@ module Banzai
parent = project || group
if project || full_path_ref?(matches)
- project_path = full_project_path(matches[:namespace], matches[:project])
+ project_path = reference_cache.full_project_path(matches[:namespace], matches[:project])
parent_from_ref = from_ref_cached(project_path)
reference = parent_from_ref.to_human_reference(parent)
@@ -129,4 +126,4 @@ module Banzai
end
end
-Banzai::Filter::References::LabelReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::LabelReferenceFilter')
+Banzai::Filter::References::LabelReferenceFilter.prepend_mod_with('Banzai::Filter::References::LabelReferenceFilter')
diff --git a/lib/banzai/filter/references/merge_request_reference_filter.rb b/lib/banzai/filter/references/merge_request_reference_filter.rb
index 872c33f6873..6c5ad83d9ae 100644
--- a/lib/banzai/filter/references/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/references/merge_request_reference_filter.rb
@@ -9,10 +9,7 @@ module Banzai
# This filter supports cross-project references.
class MergeRequestReferenceFilter < IssuableReferenceFilter
self.reference_type = :merge_request
-
- def self.object_class
- MergeRequest
- end
+ self.object_class = MergeRequest
def url_for_object(mr, project)
h = Gitlab::Routing.url_helpers
diff --git a/lib/banzai/filter/references/milestone_reference_filter.rb b/lib/banzai/filter/references/milestone_reference_filter.rb
index 49110194ddc..31a961f3e73 100644
--- a/lib/banzai/filter/references/milestone_reference_filter.rb
+++ b/lib/banzai/filter/references/milestone_reference_filter.rb
@@ -8,10 +8,7 @@ module Banzai
include Gitlab::Utils::StrongMemoize
self.reference_type = :milestone
-
- def self.object_class
- Milestone
- end
+ self.object_class = Milestone
# Links to project milestones contain the IID, but when we're handling
# 'regular' references, we need to use the global ID to disambiguate
@@ -70,7 +67,7 @@ module Banzai
end
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
- project_path = full_project_path(namespace_ref, project_ref)
+ project_path = reference_cache.full_project_path(namespace_ref, project_ref)
# Returns group if project is not found by path
parent = parent_from_ref(project_path)
diff --git a/lib/banzai/filter/references/project_reference_filter.rb b/lib/banzai/filter/references/project_reference_filter.rb
index 522c6e0f5f3..678d2aa3468 100644
--- a/lib/banzai/filter/references/project_reference_filter.rb
+++ b/lib/banzai/filter/references/project_reference_filter.rb
@@ -6,10 +6,11 @@ module Banzai
# HTML filter that replaces project references with links.
class ProjectReferenceFilter < ReferenceFilter
self.reference_type = :project
+ self.object_class = Project
# Public: Find `namespace/project>` project references in text
#
- # ProjectReferenceFilter.references_in(text) do |match, project|
+ # references_in(text) do |match, project|
# "<a href=...>#{project}></a>"
# end
#
@@ -18,33 +19,16 @@ module Banzai
# Yields the String match, and the String project name.
#
# Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Project.markdown_reference_pattern) do |match|
+ def references_in(text, pattern = object_reference_pattern)
+ text.gsub(pattern) do |match|
yield match, "#{$~[:namespace]}/#{$~[:project]}"
end
end
- def call
- ref_pattern = Project.markdown_reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- project_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- project_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
+ private
- doc
+ def object_reference_pattern
+ @object_reference_pattern ||= Project.markdown_reference_pattern
end
# Replace `namespace/project>` project references in text with links to the referenced
@@ -55,8 +39,8 @@ module Banzai
#
# Returns a String with `namespace/project>` references replaced with links. All links
# have `gfm` and `gfm-project` class names attached for styling.
- def project_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, project_path|
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text) do |match, project_path|
cached_call(:banzai_url_for_object, match, path: [Project, project_path.downcase]) do
if project = projects_hash[project_path.downcase]
link_to_project(project, link_content: link_content) || match
@@ -92,8 +76,6 @@ module Banzai
refs.to_a
end
- private
-
def urls
Gitlab::Routing.url_helpers
end
diff --git a/lib/banzai/filter/references/reference_cache.rb b/lib/banzai/filter/references/reference_cache.rb
new file mode 100644
index 00000000000..ab0c74e00d9
--- /dev/null
+++ b/lib/banzai/filter/references/reference_cache.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ module References
+ class ReferenceCache
+ include Gitlab::Utils::StrongMemoize
+ include RequestStoreReferenceCache
+
+ def initialize(filter, context)
+ @filter = filter
+ @context = context
+ end
+
+ def load_reference_cache(nodes)
+ load_references_per_parent(nodes)
+ load_parent_per_reference
+ load_records_per_parent
+
+ @cache_loaded = true
+ end
+
+ # Loads all object references (e.g. issue IDs) per
+ # project/group they belong to.
+ def load_references_per_parent(nodes)
+ @references_per_parent ||= {}
+
+ @references_per_parent[parent_type] ||= begin
+ refs = Hash.new { |hash, key| hash[key] = Set.new }
+
+ nodes.each do |node|
+ node.to_html.scan(regex) do
+ path = if parent_type == :project
+ full_project_path($~[:namespace], $~[:project])
+ else
+ full_group_path($~[:group])
+ end
+
+ ident = filter.identifier($~)
+ refs[path] << ident if ident
+ end
+ end
+
+ refs
+ end
+ end
+
+ def references_per_parent
+ @references_per_parent[parent_type]
+ end
+
+ # Returns a Hash containing referenced projects grouped per their full
+ # path.
+ def load_parent_per_reference
+ @per_reference ||= {}
+
+ @per_reference[parent_type] ||= begin
+ refs = references_per_parent.keys.to_set
+
+ find_for_paths(refs.to_a).index_by(&:full_path)
+ end
+ end
+
+ def parent_per_reference
+ @per_reference[parent_type]
+ end
+
+ def load_records_per_parent
+ @_records_per_project ||= {}
+
+ @_records_per_project[filter.object_class.to_s.underscore] ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ parent_per_reference.each do |path, parent|
+ record_ids = references_per_parent[path]
+
+ filter.parent_records(parent, record_ids).each do |record|
+ hash[parent][filter.record_identifier(record)] = record
+ end
+ end
+
+ hash
+ end
+ end
+
+ def records_per_parent
+ @_records_per_project[filter.object_class.to_s.underscore]
+ end
+
+ def relation_for_paths(paths)
+ klass = parent_type.to_s.camelize.constantize
+ result = klass.where_full_path_in(paths)
+ return result if parent_type == :group
+
+ result.includes(namespace: :route) if parent_type == :project
+ end
+
+ # Returns projects for the given paths.
+ def find_for_paths(paths)
+ if Gitlab::SafeRequestStore.active?
+ cache = refs_cache
+ to_query = paths - cache.keys
+
+ unless to_query.empty?
+ records = relation_for_paths(to_query)
+
+ found = []
+ records.each do |record|
+ ref = record.full_path
+ get_or_set_cache(cache, ref) { record }
+ found << ref
+ end
+
+ not_found = to_query - found
+ not_found.each do |ref|
+ get_or_set_cache(cache, ref) { nil }
+ end
+ end
+
+ cache.slice(*paths).values.compact
+ else
+ relation_for_paths(paths)
+ end
+ end
+
+ def current_parent_path
+ strong_memoize(:current_parent_path) do
+ parent&.full_path
+ end
+ end
+
+ def current_project_namespace_path
+ strong_memoize(:current_project_namespace_path) do
+ project&.namespace&.full_path
+ end
+ end
+
+ def full_project_path(namespace, project_ref)
+ return current_parent_path unless project_ref
+
+ namespace_ref = namespace || current_project_namespace_path
+ "#{namespace_ref}/#{project_ref}"
+ end
+
+ def full_group_path(group_ref)
+ return current_parent_path unless group_ref
+
+ group_ref
+ end
+
+ def cache_loaded?
+ !!@cache_loaded
+ end
+
+ private
+
+ attr_accessor :filter, :context
+
+ delegate :project, :group, :parent, :parent_type, to: :filter
+
+ def regex
+ strong_memoize(:regex) do
+ [
+ filter.object_class.link_reference_pattern,
+ filter.object_class.reference_pattern
+ ].compact.reduce { |a, b| Regexp.union(a, b) }
+ end
+ end
+
+ def refs_cache
+ Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
+ end
+ end
+ end
+ end
+end
+
+Banzai::Filter::References::ReferenceCache.prepend_mod
diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb
index dd15c43f5d8..58436f4505e 100644
--- a/lib/banzai/filter/references/reference_filter.rb
+++ b/lib/banzai/filter/references/reference_filter.rb
@@ -16,8 +16,14 @@ module Banzai
include OutputSafety
class << self
+ # Implement in child class
+ # Example: self.reference_type = :merge_request
attr_accessor :reference_type
+ # Implement in child class
+ # Example: self.object_class = MergeRequest
+ attr_accessor :object_class
+
def call(doc, context = nil, result = nil)
new(doc, context, result).call_and_update_nodes
end
@@ -34,6 +40,77 @@ module Banzai
with_update_nodes { call }
end
+ def call
+ ref_pattern_start = /\A#{object_reference_pattern}\z/
+
+ nodes.each_with_index do |node, index|
+ if text_node?(node)
+ replace_text_when_pattern_matches(node, index, object_reference_pattern) do |content|
+ object_link_filter(content, object_reference_pattern)
+ end
+ elsif element_node?(node)
+ yield_valid_link(node) do |link, inner_html|
+ if link =~ ref_pattern_start
+ replace_link_node_with_href(node, index, link) do
+ object_link_filter(link, object_reference_pattern, link_content: inner_html)
+ end
+ end
+ end
+ end
+ end
+
+ doc
+ end
+
+ # Public: Find references in text (like `!123` for merge requests)
+ #
+ # references_in(text) do |match, id, project_ref, matches|
+ # object = find_object(project_ref, id)
+ # "<a href=...>#{object.to_reference}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer referenced object ID, an optional String
+ # of the external project reference, and all of the matchdata.
+ #
+ # Returns a String replaced with the return of the block.
+ def references_in(text, pattern = object_reference_pattern)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
+ end
+
+ # 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
+
+ # Returns an Array containing all HTML nodes.
+ def nodes
+ @nodes ||= each_node.to_a
+ end
+
+ def object_class
+ self.class.object_class
+ end
+
+ def project
+ context[:project]
+ end
+
+ def group
+ context[:group]
+ end
+
+ private
+
# Returns a data attribute String to attach to a reference link
#
# attributes - Hash, where the key becomes the data attribute name and the
@@ -69,12 +146,11 @@ module Banzai
end
end
- def project
- context[:project]
- end
-
- def group
- context[:group]
+ # Ensure that a :project key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project unless skip_project_check?
end
def user
@@ -93,31 +169,6 @@ module Banzai
"#{gfm_klass} has-tooltip"
end
- # Ensure that a :project key exists in context
- #
- # Note that while the key might exist, its value could be nil!
- def validate
- needs :project unless skip_project_check?
- end
-
- # 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
-
- # Returns an Array containing all HTML nodes.
- def nodes
- @nodes ||= each_node.to_a
- end
-
# Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = unescape_link(node.attr('href').to_s)
@@ -132,6 +183,14 @@ module Banzai
CGI.unescape(href)
end
+ def unescape_html_entities(text)
+ CGI.unescapeHTML(text.to_s)
+ end
+
+ def escape_html_entities(text)
+ CGI.escapeHTML(text.to_s)
+ end
+
def replace_text_when_pattern_matches(node, index, pattern)
return unless node.text =~ pattern
@@ -161,7 +220,21 @@ module Banzai
node.is_a?(Nokogiri::XML::Element)
end
- private
+ def object_reference_pattern
+ @object_reference_pattern ||= object_class.reference_pattern
+ end
+
+ def object_name
+ @object_name ||= object_class.name.underscore
+ end
+
+ def object_sym
+ @object_sym ||= object_name.to_sym
+ end
+
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ raise NotImplementedError, "#{self.class} must implement method: #{__callee__}"
+ end
def query
@query ||= %Q{descendant-or-self::text()[not(#{ignore_ancestor_query})]
diff --git a/lib/banzai/filter/references/snippet_reference_filter.rb b/lib/banzai/filter/references/snippet_reference_filter.rb
index bf7e0f78609..502bfca1ab7 100644
--- a/lib/banzai/filter/references/snippet_reference_filter.rb
+++ b/lib/banzai/filter/references/snippet_reference_filter.rb
@@ -9,15 +9,16 @@ module Banzai
# This filter supports cross-project references.
class SnippetReferenceFilter < AbstractReferenceFilter
self.reference_type = :snippet
+ self.object_class = Snippet
- def self.object_class
- Snippet
+ def parent_records(project, ids)
+ return unless project.is_a?(Project)
+
+ project.snippets.where(id: ids.to_a)
end
def find_object(project, id)
- return unless project.is_a?(Project)
-
- project.snippets.find_by(id: id)
+ reference_cache.records_per_parent[project][id]
end
def url_for_object(snippet, project)
diff --git a/lib/banzai/filter/references/user_reference_filter.rb b/lib/banzai/filter/references/user_reference_filter.rb
index 04665973f51..1709b607c2e 100644
--- a/lib/banzai/filter/references/user_reference_filter.rb
+++ b/lib/banzai/filter/references/user_reference_filter.rb
@@ -8,10 +8,11 @@ module Banzai
# A special `@all` reference is also supported.
class UserReferenceFilter < ReferenceFilter
self.reference_type = :user
+ self.object_class = User
# Public: Find `@user` user references in text
#
- # UserReferenceFilter.references_in(text) do |match, username|
+ # references_in(text) do |match, username|
# "<a href=...>@#{user}</a>"
# end
#
@@ -20,8 +21,8 @@ module Banzai
# 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|
+ def references_in(text, pattern = object_reference_pattern)
+ text.gsub(pattern) do |match|
yield match, $~[:user]
end
end
@@ -29,28 +30,11 @@ module Banzai
def call
return doc if project.nil? && group.nil? && !skip_project_check?
- ref_pattern = User.reference_pattern
- ref_pattern_start = /\A#{ref_pattern}\z/
-
- nodes.each_with_index do |node, index|
- if text_node?(node)
- replace_text_when_pattern_matches(node, index, ref_pattern) do |content|
- user_link_filter(content)
- end
- elsif element_node?(node)
- yield_valid_link(node) do |link, inner_html|
- if link =~ ref_pattern_start
- replace_link_node_with_href(node, index, link) do
- user_link_filter(link, link_content: inner_html)
- end
- end
- end
- end
- end
-
- doc
+ super
end
+ private
+
# Replace `@user` user references in text with links to the referenced
# user's profile page.
#
@@ -59,8 +43,8 @@ module Banzai
#
# Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text, link_content: nil)
- self.class.references_in(text) do |match, username|
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
+ references_in(text, pattern) do |match, username|
if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
else
@@ -100,8 +84,6 @@ module Banzai
refs.to_a
end
- private
-
def urls
Gitlab::Routing.url_helpers
end
diff --git a/lib/banzai/filter/references/vulnerability_reference_filter.rb b/lib/banzai/filter/references/vulnerability_reference_filter.rb
index e5f2408eda4..aaf45304021 100644
--- a/lib/banzai/filter/references/vulnerability_reference_filter.rb
+++ b/lib/banzai/filter/references/vulnerability_reference_filter.rb
@@ -6,19 +6,10 @@ module Banzai
# The actual filter is implemented in the EE mixin
class VulnerabilityReferenceFilter < IssuableReferenceFilter
self.reference_type = :vulnerability
-
- def self.object_class
- Vulnerability
- end
-
- private
-
- def project
- context[:project]
- end
+ self.object_class = Vulnerability
end
end
end
end
-Banzai::Filter::References::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::References::VulnerabilityReferenceFilter')
+Banzai::Filter::References::VulnerabilityReferenceFilter.prepend_mod_with('Banzai::Filter::References::VulnerabilityReferenceFilter')
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 06dddc74eba..1e84e7e8af3 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -65,4 +65,4 @@ module Banzai
end
end
-Banzai::Filter::SanitizationFilter.prepend_if_ee('EE::Banzai::Filter::SanitizationFilter')
+Banzai::Filter::SanitizationFilter.prepend_mod_with('Banzai::Filter::SanitizationFilter')
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 731a2bb4c77..b16ea689d2e 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -38,7 +38,7 @@ module Banzai
begin
code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, node.text), tag: language)
css_classes << " language-#{language}" if language
- rescue
+ rescue StandardError
# Gracefully handle syntax highlighter bugs/errors to ensure users can
# still access an issue/comment/etc. First, retry with the plain text
# filter. If that fails, then just skip this entirely, but that would
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index 762371e1418..ceb7547a85d 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -15,8 +15,16 @@ module Banzai
def call
return doc if context[:system_note]
- linkable_attributes.each do |attr|
- process_link_to_upload_attr(attr)
+ if Feature.enabled?(:optimize_linkable_attributes, project, default_enabled: :yaml)
+ # We exclude processed upload links from the linkable attributes to
+ # prevent further modifications by RepositoryLinkFilter
+ linkable_attributes.reject! do |attr|
+ process_link_to_upload_attr(attr)
+ end
+ else
+ linkable_attributes.each do |attr|
+ process_link_to_upload_attr(attr)
+ end
end
doc