summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-04-23 12:08:03 +0300
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2015-04-23 12:08:03 +0300
commit71f6143552a47209d4d83c35260db608cac7de1a (patch)
treeb5c16ae980c71adc7af6ef803369d2f0f33d4bb3 /lib
parent63c5911961909b12b328b4182ba0f4b0e13c1bd6 (diff)
parentaac27550457eaf0503ce9bf7b04c18141ed317af (diff)
downloadgitlab-ce-71f6143552a47209d4d83c35260db608cac7de1a.tar.gz
Merge branch 'master' into new-sidebar
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> Conflicts: app/controllers/snippets_controller.rb
Diffstat (limited to 'lib')
-rw-r--r--lib/gitlab/google_code_import/importer.rb67
-rw-r--r--lib/gitlab/markdown.rb336
-rw-r--r--lib/gitlab/markdown/commit_range_reference_filter.rb105
-rw-r--r--lib/gitlab/markdown/commit_reference_filter.rb80
-rw-r--r--lib/gitlab/markdown/cross_project_reference.rb32
-rw-r--r--lib/gitlab/markdown/emoji_filter.rb79
-rw-r--r--lib/gitlab/markdown/external_issue_reference_filter.rb63
-rw-r--r--lib/gitlab/markdown/issue_reference_filter.rb74
-rw-r--r--lib/gitlab/markdown/label_reference_filter.rb94
-rw-r--r--lib/gitlab/markdown/merge_request_reference_filter.rb73
-rw-r--r--lib/gitlab/markdown/reference_filter.rb76
-rw-r--r--lib/gitlab/markdown/snippet_reference_filter.rb72
-rw-r--r--lib/gitlab/markdown/user_reference_filter.rb92
-rw-r--r--lib/gitlab/reference_extractor.rb70
14 files changed, 1001 insertions, 312 deletions
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index b5e82563ff1..70bfe059776 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -30,7 +30,10 @@ module Gitlab
def user_map
@user_map ||= begin
- user_map = Hash.new { |hash, user| Client.mask_email(user) }
+ user_map = Hash.new do |hash, user|
+ # Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked.
+ Client.mask_email(user).sub("...", "\\.\\.\\.")
+ end
import_data = project.import_data.try(:data)
stored_user_map = import_data["user_map"] if import_data
@@ -203,25 +206,25 @@ module Gitlab
end
def linkify_issues(s)
- s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ s = s.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
+ s
end
def escape_for_markdown(s)
- s = s.gsub("*", "\\*")
- s = s.gsub("#", "\\#")
+ # No headings and lists
+ s = s.gsub(/^#/, "\\#")
+ s = s.gsub(/^-/, "\\-")
+
+ # No inline code
s = s.gsub("`", "\\`")
- s = s.gsub(":", "\\:")
- s = s.gsub("-", "\\-")
- s = s.gsub("+", "\\+")
- s = s.gsub("_", "\\_")
- s = s.gsub("(", "\\(")
- s = s.gsub(")", "\\)")
- s = s.gsub("[", "\\[")
- s = s.gsub("]", "\\]")
- s = s.gsub("<", "\\<")
- s = s.gsub(">", "\\>")
+
+ # Carriage returns make me sad
s = s.gsub("\r", "")
+
+ # Markdown ignores single newlines, but we need them as <br />.
s = s.gsub("\n", " \n")
+
s
end
@@ -276,11 +279,18 @@ module Gitlab
if raw_updates.has_key?("blockedOn")
blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on|
name, id = raw_blocked_on.split(":", 2)
- if name == project.import_source
- "##{id}"
- else
- "#{project.namespace.path}/#{name}##{id}"
- end
+
+ deleted = name.start_with?("-")
+ name = name[1..-1] if deleted
+
+ text =
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ text = "~~#{text}~~" if deleted
+ text
end
updates << "*Blocked on: #{blocked_ons.join(", ")}*"
end
@@ -288,11 +298,18 @@ module Gitlab
if raw_updates.has_key?("blocking")
blockings = raw_updates["blocking"].map do |raw_blocked_on|
name, id = raw_blocked_on.split(":", 2)
- if name == project.import_source
- "##{id}"
- else
- "#{project.namespace.path}/#{name}##{id}"
- end
+
+ deleted = name.start_with?("-")
+ name = name[1..-1] if deleted
+
+ text =
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ text = "~~#{text}~~" if deleted
+ text
end
updates << "*Blocking: #{blockings.join(", ")}*"
end
@@ -340,7 +357,7 @@ module Gitlab
def format_issue_body(author, date, content, attachments)
body = []
- body << "*By #{author} on #{date}*"
+ body << "*By #{author} on #{date} (imported from Google Code)*"
body << "---"
if content.blank?
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index 47c456d8dc7..37b250d353e 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -1,5 +1,4 @@
require 'html/pipeline'
-require 'html/pipeline/gitlab'
module Gitlab
# Custom parser for GitLab-flavored Markdown
@@ -10,11 +9,11 @@ module Gitlab
# Supported reference formats are:
# * @foo for team members
# * #123 for issues
- # * #JIRA-123 for Jira issues
+ # * JIRA-123 for Jira issues
# * !123 for merge requests
# * $123 for snippets
- # * 123456 for commits
- # * 123456...7890123 for commit ranges (comparisons)
+ # * 1c002d for specific commit
+ # * 1c002d...35cfb2 for commit ranges (comparisons)
#
# It also parses Emoji codes to insert images. See
# http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
@@ -30,10 +29,6 @@ module Gitlab
# >> gfm(":trollface:")
# => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
module Markdown
- include IssuesHelper
-
- attr_reader :options, :html_options
-
# Public: Parse the provided text with GitLab-Flavored Markdown
#
# text - the source text
@@ -65,47 +60,24 @@ module Gitlab
reference_only_path: true
)
- @options = options
- @html_options = html_options
-
- # TODO: add popups with additional information
+ pipeline = HTML::Pipeline.new(filters)
- # Used markdown pipelines in GitLab:
- # GitlabEmojiFilter - performs emoji replacement.
- # SanitizationFilter - remove unsafe HTML tags and attributes
- #
- # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
- filters = [
- HTML::Pipeline::Gitlab::GitlabEmojiFilter,
- HTML::Pipeline::SanitizationFilter
- ]
+ context = {
+ # SanitizationFilter
+ whitelist: sanitization_whitelist,
- whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST
- whitelist[:attributes][:all].push('class', 'id')
- whitelist[:elements].push('span')
-
- # Remove the rel attribute that the sanitize gem adds, and remove the
- # href attribute if it contains inline javascript
- fix_anchors = lambda do |env|
- name, node = env[:node_name], env[:node]
- if name == 'a'
- node.remove_attribute('rel')
- if node['href'] && node['href'].match('javascript:')
- node.remove_attribute('href')
- end
- end
- end
- whitelist[:transformers].push(fix_anchors)
+ # EmojiFilter
+ asset_root: Gitlab.config.gitlab.url,
+ asset_host: Gitlab::Application.config.asset_host,
- markdown_context = {
- asset_root: Gitlab.config.gitlab.url,
- asset_host: Gitlab::Application.config.asset_host,
- whitelist: whitelist
+ # ReferenceFilter
+ current_user: current_user,
+ only_path: options[:reference_only_path],
+ project: project,
+ reference_class: html_options[:class]
}
- markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
-
- result = markdown_pipeline.call(text, markdown_context)
+ result = pipeline.call(text, context)
save_options = 0
if options[:xhtml]
@@ -114,21 +86,6 @@ module Gitlab
text = result[:output].to_html(save_with: save_options)
- # Extract pre blocks so they are not altered
- # from http://github.github.com/github-flavored-markdown/
- text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| extract_piece(match) }
- # Extract links with probably parsable hrefs
- text.gsub!(%r{<a.*?>.*?</a>}m) { |match| extract_piece(match) }
- # Extract images with probably parsable src
- text.gsub!(%r{<img.*?>}m) { |match| extract_piece(match) }
-
- text = parse(text, project)
-
- # Insert pre block extractions
- text.gsub!(/\{gfm-extraction-(\h{32})\}/) do
- insert_piece($1)
- end
-
if options[:parse_tasks]
text = parse_tasks(text)
end
@@ -138,242 +95,53 @@ module Gitlab
private
- def extract_piece(text)
- @extractions ||= {}
-
- md5 = Digest::MD5.hexdigest(text)
- @extractions[md5] = text
- "{gfm-extraction-#{md5}}"
- end
-
- def insert_piece(id)
- @extractions[id]
- end
-
- # Private: Parses text for references
+ # Filters used in our pipeline
#
- # text - Text to parse
+ # SanitizationFilter should come first so that all generated reference HTML
+ # goes through untouched.
#
- # Returns parsed text
- def parse(text, project = @project)
- parse_references(text, project) if project
-
- text
- end
-
- NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR
- PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
-
- REFERENCE_PATTERN = %r{
- (?<prefix>\W)? # Prefix
- ( # Reference
- @(?<user>#{NAME_STR}) # User name
- |~(?<label>\d+) # Label ID
- |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID
- |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
- |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
- |\$(?<snippet>\d+) # Snippet ID
- |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
- |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
- |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit
- )
- (?<suffix>\W)? # Suffix
- }x.freeze
-
- TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze
-
- def parse_references(text, project = @project)
- # parse reference links
- text.gsub!(REFERENCE_PATTERN) do |match|
- type = TYPES.select{|t| !$~[t].nil?}.first
-
- actual_project = project
- project_prefix = nil
- project_path = $LAST_MATCH_INFO[:project]
- if project_path
- actual_project = ::Project.find_with_namespace(project_path)
- actual_project = nil unless can?(current_user, :read_project, actual_project)
- project_prefix = project_path
- end
-
- parse_result($LAST_MATCH_INFO, type,
- actual_project, project_prefix) || match
- end
- end
-
- # Called from #parse_references. Attempts to build a gitlab reference
- # link. Returns nil if +type+ is nil, if the match string is an HTML
- # entity, if the reference is invalid, or if the matched text includes an
- # invalid project path.
- def parse_result(match_info, type, project, project_prefix)
- prefix = match_info[:prefix]
- suffix = match_info[:suffix]
-
- return nil if html_entity?(prefix, suffix) || type.nil?
- return nil if project.nil? && !project_prefix.nil?
-
- identifier = match_info[type]
- ref_link = reference_link(type, identifier, project, project_prefix)
-
- if ref_link
- "#{prefix}#{ref_link}#{suffix}"
- else
- nil
- end
- end
-
- # Return true if the +prefix+ and +suffix+ indicate that the matched string
- # is an HTML entity like &amp;
- def html_entity?(prefix, suffix)
- prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
+ # See https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
+ def filters
+ [
+ HTML::Pipeline::SanitizationFilter,
+
+ Gitlab::Markdown::EmojiFilter,
+
+ Gitlab::Markdown::UserReferenceFilter,
+ Gitlab::Markdown::IssueReferenceFilter,
+ Gitlab::Markdown::ExternalIssueReferenceFilter,
+ Gitlab::Markdown::MergeRequestReferenceFilter,
+ Gitlab::Markdown::SnippetReferenceFilter,
+ Gitlab::Markdown::CommitRangeReferenceFilter,
+ Gitlab::Markdown::CommitReferenceFilter,
+ Gitlab::Markdown::LabelReferenceFilter,
+ ]
end
- # Private: Dispatches to a dedicated processing method based on reference
- #
- # reference - Object reference ("@1234", "!567", etc.)
- # identifier - Object identifier (Issue ID, SHA hash, etc.)
+ # Customize the SanitizationFilter whitelist
#
- # Returns string rendered by the processing method
- def reference_link(type, identifier, project = @project, prefix_text = nil)
- send("reference_#{type}", identifier, project, prefix_text)
- end
-
- def reference_user(identifier, project = @project, _ = nil)
- link_options = html_options.merge(
- class: "gfm gfm-project_member #{html_options[:class]}"
- )
+ # - Allow `class` and `id` attributes on all elements
+ # - Allow `span` elements
+ # - Remove `rel` attributes from `a` elements
+ # - Remove `a` nodes with `javascript:` in the `href` attribute
+ def sanitization_whitelist
+ whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST
+ whitelist[:attributes][:all].push('class', 'id')
+ whitelist[:elements].push('span')
- if identifier == "all"
- link_to(
- "@all",
- namespace_project_url(project.namespace, project, only_path: options[:reference_only_path]),
- link_options
- )
- elsif namespace = Namespace.find_by(path: identifier)
- url =
- if namespace.is_a?(Group)
- return nil unless can?(current_user, :read_group, namespace)
- group_url(identifier, only_path: options[:reference_only_path])
- else
- user_url(identifier, only_path: options[:reference_only_path])
+ fix_anchors = lambda do |env|
+ name, node = env[:node_name], env[:node]
+ if name == 'a'
+ node.remove_attribute('rel')
+ if node['href'] && node['href'].match('javascript:')
+ node.remove_attribute('href')
end
-
- link_to("@#{identifier}", url, link_options)
- end
- end
-
- def reference_label(identifier, project = @project, _ = nil)
- if label = project.labels.find_by(id: identifier)
- link_options = html_options.merge(
- class: "gfm gfm-label #{html_options[:class]}"
- )
- link_to(
- render_colored_label(label),
- namespace_project_issues_path(project.namespace, project, label_name: label.name),
- link_options
- )
- end
- end
-
- def reference_issue(identifier, project = @project, prefix_text = nil)
- if project.default_issues_tracker?
- if project.issue_exists? identifier
- url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
- title = title_for_issue(identifier, project)
- link_options = html_options.merge(
- title: "Issue: #{title}",
- class: "gfm gfm-issue #{html_options[:class]}"
- )
-
- link_to("#{prefix_text}##{identifier}", url, link_options)
- end
- else
- if project.external_issue_tracker.present?
- reference_external_issue(identifier, project,
- prefix_text)
end
end
- end
-
- def reference_merge_request(identifier, project = @project, prefix_text = nil)
- if merge_request = project.merge_requests.find_by(iid: identifier)
- link_options = html_options.merge(
- title: "Merge Request: #{merge_request.title}",
- class: "gfm gfm-merge_request #{html_options[:class]}"
- )
- url = namespace_project_merge_request_url(project.namespace, project,
- merge_request,
- only_path: options[:reference_only_path])
- link_to("#{prefix_text}!#{identifier}", url, link_options)
- end
- end
-
- def reference_snippet(identifier, project = @project, _ = nil)
- if snippet = project.snippets.find_by(id: identifier)
- link_options = html_options.merge(
- title: "Snippet: #{snippet.title}",
- class: "gfm gfm-snippet #{html_options[:class]}"
- )
- link_to(
- "$#{identifier}",
- namespace_project_snippet_url(project.namespace, project, snippet,
- only_path: options[:reference_only_path]),
- link_options
- )
- end
- end
-
- def reference_commit(identifier, project = @project, prefix_text = nil)
- if project.valid_repo? && commit = project.repository.commit(identifier)
- link_options = html_options.merge(
- title: commit.link_title,
- class: "gfm gfm-commit #{html_options[:class]}"
- )
- prefix_text = "#{prefix_text}@" if prefix_text
- link_to(
- "#{prefix_text}#{identifier}",
- namespace_project_commit_url( project.namespace, project, commit,
- only_path: options[:reference_only_path]),
- link_options
- )
- end
- end
-
- def reference_commit_range(identifier, project = @project, prefix_text = nil)
- from_id, to_id = identifier.split(/\.{2,3}/, 2)
-
- inclusive = identifier !~ /\.{3}/
- from_id << "^" if inclusive
-
- if project.valid_repo? &&
- from = project.repository.commit(from_id) &&
- to = project.repository.commit(to_id)
-
- link_options = html_options.merge(
- title: "Commits #{from_id} through #{to_id}",
- class: "gfm gfm-commit_range #{html_options[:class]}"
- )
- prefix_text = "#{prefix_text}@" if prefix_text
- link_to(
- "#{prefix_text}#{identifier}",
- namespace_project_compare_url(project.namespace, project,
- from: from_id, to: to_id,
- only_path: options[:reference_only_path]),
- link_options
- )
- end
- end
-
- def reference_external_issue(identifier, project = @project, prefix_text = nil)
- url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
- title = project.external_issue_tracker.title
+ whitelist[:transformers].push(fix_anchors)
- link_options = html_options.merge(
- title: "Issue in #{title}",
- class: "gfm gfm-issue #{html_options[:class]}"
- )
- link_to("#{prefix_text}##{identifier}", url, link_options)
+ whitelist
end
# Turn list items that start with "[ ]" into HTML checkbox inputs.
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
new file mode 100644
index 00000000000..1128c1bed7a
--- /dev/null
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -0,0 +1,105 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces commit range references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitRangeReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find commit range references in text
+ #
+ # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref|
+ # "<a href=...>#{commit_range}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the String commit range, and an optional String
+ # of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(COMMIT_RANGE_PATTERN) do |match|
+ yield match, $~[:commit_range], $~[:project]
+ end
+ end
+
+ def initialize(*args)
+ super
+
+ @commit_map = {}
+ end
+
+ # Pattern used to extract commit range references from text
+ #
+ # The beginning and ending SHA1 sums can be between 6 and 40 hex
+ # characters, and the range selection can be double- or triple-dot.
+ #
+ # This pattern supports cross-project references.
+ COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>\h{6,40}\.{2,3}\h{6,40})/
+
+ def call
+ replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content|
+ commit_range_link_filter(content)
+ end
+ end
+
+ # Replace commit range references in text with links to compare the commit
+ # ranges.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with commit range references replaced with links. All
+ # links have `gfm` and `gfm-commit_range` class names attached for
+ # styling.
+ def commit_range_link_filter(text)
+ self.class.references_in(text) do |match, commit_range, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ from_id, to_id = split_commit_range(commit_range)
+
+ if valid_range?(project, from_id, to_id)
+ url = url_for_commit_range(project, from_id, to_id)
+
+ title = "Commits #{from_id} through #{to_id}"
+ klass = reference_class(:commit_range)
+
+ project_ref += '@' if project_ref
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}#{commit_range}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def split_commit_range(range)
+ from_id, to_id = range.split(/\.{2,3}/, 2)
+ from_id << "^" if range !~ /\.{3}/
+
+ [from_id, to_id]
+ end
+
+ def commit(id)
+ unless @commit_map[id]
+ @commit_map[id] = project.repository.commit(id)
+ end
+
+ @commit_map[id]
+ end
+
+ def valid_range?(project, from_id, to_id)
+ project && project.valid_repo? && commit(from_id) && commit(to_id)
+ end
+
+ def url_for_commit_range(project, from_id, to_id)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_compare_url(project.namespace, project,
+ from: from_id, to: to_id,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
new file mode 100644
index 00000000000..745de6402cf
--- /dev/null
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -0,0 +1,80 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces commit references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find commit references in text
+ #
+ # CommitReferenceFilter.references_in(text) do |match, commit, project_ref|
+ # "<a href=...>#{commit}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the String commit identifier, and an optional
+ # String of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(COMMIT_PATTERN) do |match|
+ yield match, $~[:commit], $~[:project]
+ end
+ end
+
+ # Pattern used to extract commit references from text
+ #
+ # The SHA1 sum can be between 6 and 40 hex characters.
+ #
+ # This pattern supports cross-project references.
+ COMMIT_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit>\h{6,40})/
+
+ def call
+ replace_text_nodes_matching(COMMIT_PATTERN) do |content|
+ commit_link_filter(content)
+ end
+ end
+
+ # Replace commit references in text with links to the commit specified.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with commit references replaced with links. All links
+ # have `gfm` and `gfm-commit` class names attached for styling.
+ def commit_link_filter(text)
+ self.class.references_in(text) do |match, commit_ref, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if commit = commit_from_ref(project, commit_ref)
+ url = url_for_commit(project, commit)
+
+ title = escape_once(commit.link_title)
+ klass = reference_class(:commit)
+
+ project_ref += '@' if project_ref
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}#{commit_ref}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def commit_from_ref(project, commit_ref)
+ if project && project.valid_repo?
+ project.repository.commit(commit_ref)
+ end
+ end
+
+ def url_for_commit(project, commit)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_commit_url(project.namespace, project, commit,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb
new file mode 100644
index 00000000000..887c205cdc9
--- /dev/null
+++ b/lib/gitlab/markdown/cross_project_reference.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module Markdown
+ # Common methods for ReferenceFilters that support an optional cross-project
+ # reference.
+ module CrossProjectReference
+ NAMING_PATTERN = Gitlab::Regex::NAMESPACE_REGEX_STR
+ PROJECT_PATTERN = "(?<project>#{NAMING_PATTERN}/#{NAMING_PATTERN})"
+
+ # Given a cross-project reference string, get the Project record
+ #
+ # Defaults to value of `context[:project]` if:
+ # * No reference is given OR
+ # * Reference given doesn't exist
+ #
+ # ref - String reference.
+ #
+ # Returns a Project, or nil if the reference can't be accessed
+ def project_from_ref(ref)
+ return context[:project] unless ref
+
+ other = Project.find_with_namespace(ref)
+ return nil unless other && user_can_reference_project?(other)
+
+ other
+ end
+
+ def user_can_reference_project?(project, user = context[:current_user])
+ Ability.abilities.allowed?(user, :read_project, project)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/emoji_filter.rb
new file mode 100644
index 00000000000..e239f766844
--- /dev/null
+++ b/lib/gitlab/markdown/emoji_filter.rb
@@ -0,0 +1,79 @@
+require 'gitlab_emoji'
+require 'html/pipeline/filter'
+require 'action_controller'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces :emoji: with images.
+ #
+ # Based on HTML::Pipeline::EmojiFilter
+ #
+ # Context options:
+ # :asset_root
+ # :asset_host
+ class EmojiFilter < HTML::Pipeline::Filter
+ IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
+ def call
+ doc.search('text()').each do |node|
+ content = node.to_html
+ next unless content.include?(':')
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+
+ html = emoji_image_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Replace :emoji: with corresponding images.
+ #
+ # text - String text to replace :emoji: in.
+ #
+ # Returns a String with :emoji: replaced with images.
+ def emoji_image_filter(text)
+ text.gsub(emoji_pattern) do |match|
+ name = $1
+ "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{emoji_url(name)}' height='20' width='20' align='absmiddle' />"
+ end
+ end
+
+ private
+
+ def emoji_url(name)
+ emoji_path = "emoji/#{emoji_filename(name)}"
+ if context[:asset_host]
+ # Asset host is specified.
+ url_to_image(emoji_path)
+ elsif context[:asset_root]
+ # Gitlab url is specified
+ File.join(context[:asset_root], url_to_image(emoji_path))
+ else
+ # All other cases
+ url_to_image(emoji_path)
+ end
+ end
+
+ def url_to_image(image)
+ ActionController::Base.helpers.url_to_image(image)
+ end
+
+ # Build a regexp that matches all valid :emoji: names.
+ def self.emoji_pattern
+ @emoji_pattern ||= /:(#{Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
+ end
+
+ def emoji_pattern
+ self.class.emoji_pattern
+ end
+
+ def emoji_filename(name)
+ "#{Emoji.emoji_filename(name)}.png"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb
new file mode 100644
index 00000000000..0fc3f4cca06
--- /dev/null
+++ b/lib/gitlab/markdown/external_issue_reference_filter.rb
@@ -0,0 +1,63 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces external issue tracker references with links.
+ # References are ignored if the project doesn't use an external issue
+ # tracker.
+ class ExternalIssueReferenceFilter < ReferenceFilter
+ # Public: Find `JIRA-123` issue references in text
+ #
+ # ExternalIssueReferenceFilter.references_in(text) do |match, issue|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match and the String issue reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(ISSUE_PATTERN) do |match|
+ yield match, $~[:issue]
+ end
+ end
+
+ # Pattern used to extract `JIRA-123` issue references from text
+ ISSUE_PATTERN = /(?<issue>([A-Z\-]+-)\d+)/
+
+ def call
+ # Early return if the project isn't using an external tracker
+ return doc if project.nil? || project.default_issues_tracker?
+
+ replace_text_nodes_matching(ISSUE_PATTERN) do |content|
+ issue_link_filter(content)
+ end
+ end
+
+ # Replace `JIRA-123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # 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)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, issue|
+ url = url_for_issue(issue, project, only_path: context[:only_path])
+
+ title = escape_once("Issue in #{project.external_issue_tracker.title}")
+ klass = reference_class(:issue)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{issue}</a>)
+ end
+ end
+
+ def url_for_issue(*args)
+ IssuesHelper.url_for_issue(*args)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb
new file mode 100644
index 00000000000..c9267cc3e9d
--- /dev/null
+++ b/lib/gitlab/markdown/issue_reference_filter.rb
@@ -0,0 +1,74 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces issue references with links. References to
+ # issues that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class IssueReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find `#123` issue references in text
+ #
+ # IssueReferenceFilter.references_in(text) do |match, issue, project_ref|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer issue ID, and an optional String of
+ # the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(ISSUE_PATTERN) do |match|
+ yield match, $~[:issue].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `#123` issue references from text
+ #
+ # This pattern supports cross-project references.
+ ISSUE_PATTERN = /#{PROJECT_PATTERN}?\#(?<issue>([a-zA-Z\-]+-)?\d+)/
+
+ def call
+ replace_text_nodes_matching(ISSUE_PATTERN) do |content|
+ issue_link_filter(content)
+ end
+ end
+
+ # Replace `#123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `#123` references replaced with links. All links
+ # have `gfm` and `gfm-issue` class names attached for styling.
+ def issue_link_filter(text)
+ self.class.references_in(text) do |match, issue, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project && project.issue_exists?(issue)
+ url = url_for_issue(issue, project, only_path: context[:only_path])
+
+ title = escape_once("Issue: #{title_for_issue(issue, project)}")
+ klass = reference_class(:issue)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}##{issue}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_issue(*args)
+ IssuesHelper.url_for_issue(*args)
+ end
+
+ def title_for_issue(*args)
+ IssuesHelper.title_for_issue(*args)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb
new file mode 100644
index 00000000000..4c21192c0d3
--- /dev/null
+++ b/lib/gitlab/markdown/label_reference_filter.rb
@@ -0,0 +1,94 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces label references with links.
+ class LabelReferenceFilter < ReferenceFilter
+ # Public: Find label references in text
+ #
+ # LabelReferenceFilter.references_in(text) do |match, id, name|
+ # "<a href=...>#{Label.find(id)}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, an optional Integer label ID, and an optional
+ # String label name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(LABEL_PATTERN) do |match|
+ yield match, $~[:label_id].to_i, $~[:label_name]
+ end
+ end
+
+ # Pattern used to extract label references from text
+ #
+ # TODO (rspeicher): Limit to double quotes (meh) or disallow single quotes in label names (bad).
+ LABEL_PATTERN = %r{
+ ~(
+ (?<label_id>\d+) | # Integer-based label ID, or
+ (?<label_name>
+ [A-Za-z0-9_-]+ | # String-based single-word label title
+ ['"][^&\?,]+['"] # String-based multi-word label surrounded in quotes
+ )
+ )
+ }x
+
+ def call
+ replace_text_nodes_matching(LABEL_PATTERN) do |content|
+ label_link_filter(content)
+ end
+ end
+
+ # Replace label references in text with links to the label specified.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with label references replaced with links. All links
+ # have `gfm` and `gfm-label` class names attached for styling.
+ def label_link_filter(text)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, id, name|
+ params = label_params(id, name)
+
+ if label = project.labels.find_by(params)
+ url = url_for_label(project, label)
+
+ klass = reference_class(:label)
+
+ %(<a href="#{url}" class="#{klass}">#{render_colored_label(label)}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_label(project, label)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_issues_path(project.namespace, project,
+ label_name: label.name,
+ only_path: context[:only_path])
+ end
+
+ def render_colored_label(label)
+ LabelsHelper.render_colored_label(label)
+ end
+
+ # Parameters to pass to `Label.find_by` based on the given arguments
+ #
+ # id - Integer ID to pass. If present, returns {id: id}
+ # name - String name to pass. If `id` is absent, finds by name without
+ # surrounding quotes.
+ #
+ # Returns a Hash.
+ def label_params(id, name)
+ if id > 0
+ { id: id }
+ else
+ # TODO (rspeicher): Don't strip single quotes if we decide to only use double quotes for surrounding.
+ { name: name.tr('\'"', '') }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
new file mode 100644
index 00000000000..40239523cda
--- /dev/null
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -0,0 +1,73 @@
+module Gitlab
+ module Markdown
+ # 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 < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find `!123` merge request references in text
+ #
+ # MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref|
+ # "<a href=...>##{merge_request}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer merge request ID, and an optional
+ # String of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(MERGE_REQUEST_PATTERN) do |match|
+ yield match, $~[:merge_request].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `!123` merge request references from text
+ #
+ # This pattern supports cross-project references.
+ MERGE_REQUEST_PATTERN = /#{PROJECT_PATTERN}?!(?<merge_request>\d+)/
+
+ def call
+ replace_text_nodes_matching(MERGE_REQUEST_PATTERN) do |content|
+ merge_request_link_filter(content)
+ end
+ end
+
+ # Replace `!123` merge request references in text with links to the
+ # referenced merge request's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `!123` references replaced with links. All links
+ # have `gfm` and `gfm-merge_request` class names attached for styling.
+ def merge_request_link_filter(text)
+ self.class.references_in(text) do |match, id, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project && merge_request = project.merge_requests.find_by(iid: id)
+ title = escape_once("Merge Request: #{merge_request.title}")
+ klass = reference_class(:merge_request)
+
+ url = url_for_merge_request(merge_request, project)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}!#{id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ # TODO (rspeicher): Cleanup
+ def url_for_merge_request(mr, project)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_merge_request_url(project.namespace, project, mr,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb
new file mode 100644
index 00000000000..26663c8d990
--- /dev/null
+++ b/lib/gitlab/markdown/reference_filter.rb
@@ -0,0 +1,76 @@
+require 'active_support/core_ext/string/output_safety'
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # 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.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ class ReferenceFilter < HTML::Pipeline::Filter
+ def escape_once(html)
+ ERB::Util.html_escape_once(html)
+ end
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def ignored_ancestry?(node)
+ has_ancestor?(node, IGNORE_PARENTS)
+ end
+
+ def project
+ context[:project]
+ end
+
+ def reference_class(type)
+ "gfm gfm-#{type} #{context[:reference_class]}".strip
+ end
+
+ # Iterate through the document's text nodes, yielding the current node's
+ # content if:
+ #
+ # * The `project` context value is present AND
+ # * The node's content matches `pattern` AND
+ # * The node is not an ancestor of an ignored node type
+ #
+ # pattern - Regex pattern against which to match the node's content
+ #
+ # Yields the current node's String contents. The result of the block will
+ # replace the node's existing content and update the current document.
+ #
+ # Returns the updated Nokogiri::XML::Document object.
+ def replace_text_nodes_matching(pattern)
+ return doc if project.nil?
+
+ doc.search('text()').each do |node|
+ content = node.to_html
+
+ next unless content.match(pattern)
+ next if ignored_ancestry?(node)
+
+ html = yield content
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ 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
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
new file mode 100644
index 00000000000..ada67de992b
--- /dev/null
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -0,0 +1,72 @@
+module Gitlab
+ module Markdown
+ # 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 < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find `$123` snippet references in text
+ #
+ # SnippetReferenceFilter.references_in(text) do |match, snippet|
+ # "<a href=...>$#{snippet}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer snippet ID, and an optional String
+ # of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(SNIPPET_PATTERN) do |match|
+ yield match, $~[:snippet].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `$123` snippet references from text
+ #
+ # This pattern supports cross-project references.
+ SNIPPET_PATTERN = /#{PROJECT_PATTERN}?\$(?<snippet>\d+)/
+
+ def call
+ replace_text_nodes_matching(SNIPPET_PATTERN) do |content|
+ snippet_link_filter(content)
+ end
+ end
+
+ # Replace `$123` snippet references in text with links to the referenced
+ # snippets's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `$123` references replaced with links. All links
+ # have `gfm` and `gfm-snippet` class names attached for styling.
+ def snippet_link_filter(text)
+ self.class.references_in(text) do |match, id, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project && snippet = project.snippets.find_by(id: id)
+ title = escape_once("Snippet: #{snippet.title}")
+ klass = reference_class(:snippet)
+
+ url = url_for_snippet(snippet, project)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}$#{id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_snippet(snippet, project)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_snippet_url(project.namespace, project, snippet,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
new file mode 100644
index 00000000000..5fc8ed55fe2
--- /dev/null
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -0,0 +1,92 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces user or group references with links.
+ #
+ # A special `@all` reference is also supported.
+ class UserReferenceFilter < ReferenceFilter
+ # 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_PATTERN) do |match|
+ yield match, $~[:user]
+ end
+ end
+
+ # Pattern used to extract `@user` user references from text
+ USER_PATTERN = /@(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})/
+
+ def call
+ replace_text_nodes_matching(USER_PATTERN) do |content|
+ user_link_filter(content)
+ end
+ end
+
+ # Replace `@user` user references in text with links to the referenced
+ # user's profile page.
+ #
+ # text - String text to replace references in.
+ #
+ # 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)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, user|
+ klass = reference_class(:project_member)
+
+ if user == 'all'
+ url = link_to_all(project)
+
+ %(<a href="#{url}" class="#{klass}">@#{user}</a>)
+ elsif namespace = Namespace.find_by(path: user)
+ if namespace.is_a?(Group)
+ if user_can_reference_group?(namespace)
+ url = group_url(user, only_path: context[:only_path])
+ %(<a href="#{url}" class="#{klass}">@#{user}</a>)
+ else
+ match
+ end
+ else
+ url = user_url(user, only_path: context[:only_path])
+ %(<a href="#{url}" class="#{klass}">@#{user}</a>)
+ end
+ else
+ match
+ end
+ end
+ end
+
+ private
+
+ def urls
+ Rails.application.routes.url_helpers
+ end
+
+ def group_url(*args)
+ urls.group_url(*args)
+ end
+
+ def user_url(*args)
+ urls.user_url(*args)
+ end
+
+ def link_to_all(project)
+ urls.namespace_project_url(project.namespace, project,
+ only_path: context[:only_path])
+ end
+
+ def user_can_reference_group?(group)
+ Ability.abilities.allowed?(context[:current_user], :read_group, group)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index a502a8fe9cd..34aae384355 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -3,8 +3,6 @@ module Gitlab
class ReferenceExtractor
attr_accessor :project, :current_user, :references
- include ::Gitlab::Markdown
-
def initialize(project, current_user = nil)
@project = project
@current_user = current_user
@@ -34,7 +32,7 @@ module Gitlab
project.team.members.flatten
elsif namespace = Namespace.find_by(path: identifier)
if namespace.is_a?(Group)
- namespace.users
+ namespace.users if can?(current_user, :read_group, namespace)
else
namespace.owner
end
@@ -87,6 +85,72 @@ module Gitlab
private
+ NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR
+ PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
+
+ REFERENCE_PATTERN = %r{
+ (?<prefix>\W)? # Prefix
+ ( # Reference
+ @(?<user>#{NAME_STR}) # User name
+ |~(?<label>\d+) # Label ID
+ |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID
+ |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
+ |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
+ |\$(?<snippet>\d+) # Snippet ID
+ |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
+ |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
+ )
+ (?<suffix>\W)? # Suffix
+ }x.freeze
+
+ TYPES = %i(user issue label merge_request snippet commit commit_range).freeze
+
+ def parse_references(text, project = @project)
+ # parse reference links
+ text.gsub!(REFERENCE_PATTERN) do |match|
+ type = TYPES.detect { |t| $~[t].present? }
+
+ actual_project = project
+ project_prefix = nil
+ project_path = $LAST_MATCH_INFO[:project]
+ if project_path
+ actual_project = ::Project.find_with_namespace(project_path)
+ actual_project = nil unless can?(current_user, :read_project, actual_project)
+ project_prefix = project_path
+ end
+
+ parse_result($LAST_MATCH_INFO, type,
+ actual_project, project_prefix) || match
+ end
+ end
+
+ # Called from #parse_references. Attempts to build a gitlab reference
+ # link. Returns nil if +type+ is nil, if the match string is an HTML
+ # entity, if the reference is invalid, or if the matched text includes an
+ # invalid project path.
+ def parse_result(match_info, type, project, project_prefix)
+ prefix = match_info[:prefix]
+ suffix = match_info[:suffix]
+
+ return nil if html_entity?(prefix, suffix) || type.nil?
+ return nil if project.nil? && !project_prefix.nil?
+
+ identifier = match_info[type]
+ ref_link = reference_link(type, identifier, project, project_prefix)
+
+ if ref_link
+ "#{prefix}#{ref_link}#{suffix}"
+ else
+ nil
+ end
+ end
+
+ # Return true if the +prefix+ and +suffix+ indicate that the matched string
+ # is an HTML entity like &amp;
+ def html_entity?(prefix, suffix)
+ prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
+ end
+
def reference_link(type, identifier, project, _)
references[type] << [project, identifier]
end