Commit 2cd501f7 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'rs-reference-filters' into 'master'

Convert GFM reference handling to html-pipeline filters

- `Gitlab::Markdown` is now much cleaner
- Better separation of concerns
- Cleaner, less brittle, more maintainable specs for each reference type.
- Label references actually work!

See merge request !1753
parents 6750e2d5 b3894246
......@@ -74,6 +74,7 @@ module GitlabMarkdownHelper
end
end
# TODO (rspeicher): This should be its own filter
def create_relative_links(text)
paths = extract_paths(text)
......
......@@ -108,4 +108,7 @@ module IssuesHelper
xml.summary issue.title
end
end
# Required for Gitlab::Markdown::IssueReferenceFilter
module_function :url_for_issue, :title_for_issue
end
module LabelsHelper
include ActionView::Helpers::TagHelper
def project_label_names
@project.labels.pluck(:title)
end
......@@ -7,9 +9,13 @@ module LabelsHelper
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
content_tag :span, class: 'label color-label', style: "background-color:#{label_color};color:#{text_color}" do
label.name
end
# Intentionally not using content_tag here so that this method can be called
# by LabelReferenceFilter
span = %(<span class="label color-label") +
%( style="background-color: #{label_color}; color: #{text_color}">) +
escape_once(label.name) + '</span>'
span.html_safe
end
def suggested_colors
......@@ -42,13 +48,16 @@ module LabelsHelper
r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
if (r + g + b) > 500
"#333"
'#333333'
else
"#FFF"
'#FFFFFF'
end
end
def project_labels_options(project)
options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
end
# Required for Gitlab::Markdown::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once
end
......@@ -27,7 +27,7 @@ class Label < ActiveRecord::Base
# Don't allow '?', '&', and ',' for label titles
validates :title,
presence: true,
format: { with: /\A[^&\?,&]+\z/ },
format: { with: /\A[^&\?,]+\z/ },
uniqueness: { scope: :project_id }
default_scope { order(title: :asc) }
......
......@@ -163,7 +163,7 @@ Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported
## Special GitLab References
GFM recognized special references.
GFM recognizes special references.
You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
......@@ -171,19 +171,30 @@ GFM will turn that reference into a link so you can navigate between them easily
GFM will recognize the following:
- @foo : for specific team members or groups
- @all : for the whole team
- #123 : for issues
- !123 : for merge requests
- $123 : for snippets
- 1234567 : for commits
- \[file\](path/to/file) : for file references
GFM also recognizes references to commits, issues, and merge requests in other projects:
- namespace/project#123 : for issues
- namespace/project!123 : for merge requests
- namespace/project@1234567 : for commits
| input | references |
|-----------------------:|:---------------------------|
| `@user_name` | specific user |
| `@group_name` | specific group |
| `@all` | entire team |
| `#123` | issue |
| `!123` | merge request |
| `$123` | snippet |
| `~123` | label by ID |
| `~bug` | one-word label by name |
| `~"feature request"` | multi-word label by name |
| `9ba12248` | specific commit |
| `9ba12248...b19a04f5` | commit range comparison |
| `[README](doc/README)` | repository file references |
GFM also recognizes certain cross-project references:
| input | references |
|----------------------------------------:|:------------------------|
| `namespace/project#123` | issue |
| `namespace/project!123` | merge request |
| `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
## Task Lists
......
This diff is collapsed.
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
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
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
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
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
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
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
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
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
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
......@@ -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
......
This diff is collapsed.
require 'spec_helper'
describe LabelsHelper do
it { expect(text_color_for_bg('#EEEEEE')).to eq('#333') }
it { expect(text_color_for_bg('#222E2E')).to eq('#FFF') }
it { expect(text_color_for_bg('#EEEEEE')).to eq('#333333') }
it { expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') }
end
require 'spec_helper'
module Gitlab::Markdown
describe CommitRangeReferenceFilter do
include ReferenceFilterSpecHelper
let(:project) { create(:project) }
let(:commit1) { project.repository.commit }
let(:commit2) { project.repository.commit("HEAD~2") }
it 'requires project context' do
expect { described_class.call('Commit Range 1c002d..d200c1', {}) }.
to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
context 'internal reference' do
let(:reference) { "#{commit1.id}...#{commit2.id}" }
let(:reference2) { "#{commit1.id}..#{commit2.id}" }
it 'links to a valid two-dot reference' do
doc = filter("See #{reference2}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id)
end
it 'links to a valid three-dot reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id)
end
it 'links to a valid short ID' do
reference = "#{commit1.short_id}...#{commit2.id}"
reference2 = "#{commit1.id}...#{commit2.short_id}"
expect(filter("See #{reference}").css('a').first.text).to eq reference
expect(filter("See #{reference2}").css('a').first.text).to eq reference2
end
it 'links with adjacent text' do
doc = filter("See (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid commit IDs' do
exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
expect(project).to receive(:valid_repo?).and_return(true)
expect(project.repository).to receive(:commit).with(commit1.id.reverse)
expect(filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}"
end
it 'includes default classes' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
end
it 'includes an optional custom class' do
doc = filter("See #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path option' do
doc = filter("See #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) }
let(:commit1) { project.repository.commit }
let(:commit2) { project.repository.commit("HEAD~2") }
let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" }
context 'when user can access reference' do
before { allow_cross_reference! }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id)
end
it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}"
expect(filter(act).to_html).to eq exp
exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}"
expect(filter(act).to_html).to eq exp
end
end
context 'when user cannot access reference' do
before { disallow_cross_reference! }
it 'ignores valid references' do
exp = act = "See #{reference}"
expect(filter(act).to_html).to eq exp
end
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe CommitReferenceFilter do
include ReferenceFilterSpecHelper
let(:project) { create(:project) }
let(:commit) { project.repository.commit }
it 'requires project context' do
expect { described_class.call('Commit 1c002d', {}) }.
to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
context 'internal reference' do
let(:reference) { commit.id }
# Let's test a variety of commit SHA sizes just to be paranoid
[6, 8, 12, 18, 20, 32, 40].each do |size|
it "links to a valid reference of #{size} characters" do
doc = filter("See #{reference[0...size]}")
expect(doc.css('a').first.text).to eq reference[0...size]
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_commit_url(project.namespace, project, reference)
end
end
it 'links with adjacent text' do
doc = filter("See (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid commit IDs' do
exp = act = "See #{reference.reverse}"
expect(project).to receive(:valid_repo?).and_return(true)
expect(project.repository).to receive(:commit).with(reference.reverse)
expect(filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('title')).to eq commit.link_title
end
it 'escapes the title attribute' do
allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
doc = filter("See #{reference}")
expect(doc.text).to eq "See #{commit.id}"
end
it 'includes default classes' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
end
it 'includes an optional custom class' do
doc = filter("See #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("See #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) }
let(:commit) { project.repository.commit }
let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" }
context 'when user can access reference' do
before { allow_cross_reference! }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
end
it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}"
expect(filter(act).to_html).to eq exp
end
end
context 'when user cannot access reference' do
before { disallow_cross_reference! }
it 'ignores valid references' do
exp = act = "See #{reference}"
expect(filter(act).to_html).to eq exp
end
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe CrossProjectReference do
# context in the html-pipeline sense, not in the rspec sense
let(:context) do
{
current_user: double('user'),
project: double('project')
}
end
include described_class
describe '#project_from_ref' do
context 'when no project was referenced' do
it 'returns the project from context' do
expect(project_from_ref(nil)).to eq context[:project]
end
end
context 'when referenced project does not exist' do
it 'returns nil' do
expect(project_from_ref('invalid/reference')).to be_nil
end
end
context 'when referenced project exists' do
let(:project2) { double('referenced project') }
before do
expect(Project).to receive(:find_with_namespace).
with('cross/reference').and_return(project2)
end
context 'and the user has permission to read it' do
it 'returns the referenced project' do
expect(self).to receive(:user_can_reference_project?).
with(project2).and_return(true)
expect(project_from_ref('cross/reference')).to eq project2
end
end
context 'and the user does not have permission to read it' do
it 'returns nil' do
expect(self).to receive(:user_can_reference_project?).
with(project2).and_return(false)
expect(project_from_ref('cross/reference')).to be_nil
end
end
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe ExternalIssueReferenceFilter do
include ReferenceFilterSpecHelper
def helper
IssuesHelper
end
let(:project) { create(:empty_project) }
let(:issue) { double('issue', iid: 123) }
context 'JIRA issue references' do
let(:reference) { "JIRA-#{issue.iid}" }
before do
jira = project.create_jira_service
props = {
'title' => 'JIRA tracker',
'project_url' => 'http://jira.example/issues/?jql=project=A',
'issues_url' => 'http://jira.example/browse/:id',
'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
}
jira.update_attributes(properties: props, active: true)
end
after do
project.jira_service.destroy
end
it 'requires project context' do
expect { described_class.call('Issue JIRA-123', {}) }.
to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
it 'ignores valid references when using default tracker' do
expect(project).to receive(:default_issues_tracker?).and_return(true)
exp = act = "Issue #{reference}"
expect(filter(act).to_html).to eq exp
end
%w(pre code a style).each do |elem|
it "ignores references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
it 'links to a valid reference' do
doc = filter("Issue #{reference}")
expect(doc.css('a').first.attr('href'))
.to eq helper.url_for_issue(reference, project)
end
it 'links to the external tracker' do
doc = filter("Issue #{reference}")
link = doc.css('a').first.attr('href')
expect(link).to eq "http://jira.example/browse/#{reference}"
end
it 'links with adjacent text' do
doc = filter("Issue (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
end
it 'includes a title attribute' do
doc = filter("Issue #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
end
it 'escapes the title attribute' do
allow(project.external_issue_tracker).to receive(:title).
and_return(%{"></a>whatever<a title="})
doc = filter("Issue #{reference}")
expect(doc.text).to eq "Issue #{reference}"
end
it 'includes default classes' do
doc = filter("Issue #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
end
it 'includes an optional custom class' do
doc = filter("Issue #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe IssueReferenceFilter do
include ReferenceFilterSpecHelper
def helper
IssuesHelper
end
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
it 'requires project context' do
expect { described_class.call('Issue #123', {}) }.
to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
context 'internal reference' do
let(:reference) { "##{issue.iid}" }
it 'ignores valid references when using non-default tracker' do
expect(project).to receive(:issue_exists?).with(issue.iid).and_return(false)
exp = act = "Issue ##{issue.iid}"
expect(filter(act).to_html).to eq exp
end
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project)
end
it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid issue IDs' do
exp = act = "Fixed ##{issue.iid + 1}"
expect(project).to receive(:issue_exists?).with(issue.iid + 1)
expect(filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
doc = filter("Issue #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
end
it 'escapes the title attribute' do
issue.update_attribute(:title, %{"></a>whatever<a title="})
doc = filter("Issue #{reference}")
expect(doc.text).to eq "Issue #{reference}"
end
it 'includes default classes' do
doc = filter("Issue #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
end
it 'includes an optional custom class' do
doc = filter("Issue #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" }
context 'when user can access reference' do
before { allow_cross_reference! }
it 'ignores valid references when cross-reference project uses external tracker' do
expect_any_instance_of(Project).to receive(:issue_exists?).
with(issue.iid).and_return(false)
exp = act = "Issue ##{issue.iid}"
expect(filter(act).to_html).to eq exp
end
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project2)
end
it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}"
expect(filter(act).to_html).to eq exp
end
end
context 'when user cannot access reference' do
before { disallow_cross_reference! }
it 'ignores valid references' do
exp = act = "See #{reference}"
expect(filter(act).to_html).to eq exp
end
end
end
end
end
require 'spec_helper'
require 'html/pipeline'
module Gitlab::Markdown
describe LabelReferenceFilter do
include ReferenceFilterSpecHelper
let(:project) { create(:empty_project) }
let(:label) { create(:label, project: project) }
let(:reference) { "~#{label.id}" }
it 'requires project context' do
expect { described_class.call('Label ~123', {}) }.
to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Label #{reference}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
it 'includes default classes' do
doc = filter("Label #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
end
it 'includes an optional custom class' do
doc = filter("Label #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("Label #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true)
end
describe 'label span element' do
it 'includes default classes' do
doc = filter("Label #{reference}")
expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
end
it 'includes a style attribute' do
doc = filter("Label #{reference}")
expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
end
end
context 'Integer-based references' do
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
end
it 'links with adjacent text' do
doc = filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label IDs' do
exp = act = "Label ~#{label.id + 1}"
expect(filter(act).to_html).to eq exp
end
end
context 'String-based single-word references' do
let(:label) { create(:label, name: 'gfm', project: project) }
let(:reference) { "~#{label.name}" }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See gfm'
end
it 'links with adjacent text' do
doc = filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = "Label ~#{label.name.reverse}"
expect(filter(act).to_html).to eq exp
end
end
context 'String-based multi-word references in quotes' do
let(:label) { create(:label, name: 'gfm references', project: project) }
context 'in single quotes' do
let(:reference) { "~'#{label.name}'" }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = "Label ~'#{label.name.reverse}'"
expect(filter(act).to_html).to eq exp
end
end
context 'in double quotes' do
let(:reference) { %(~"#{label.name}") }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
expect(doc.text).to eq 'See gfm references'
end
it 'links with adjacent text' do
doc = filter("Label (#{reference}.)")
expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
end
it 'ignores invalid label names' do
exp = act = %(Label ~"#{label.name.reverse}")
expect(filter(act).to_html).to eq exp
end
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe MergeRequestReferenceFilter do
include ReferenceFilterSpecHelper
let(:project) { create(:project) }
let(:merge) { create(:merge_request, source_project: project) }
it 'requires project context' do
expect { described_class.call('MergeRequest !123', {}) }.
to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
context 'internal reference' do
let(:reference) { "!#{merge.iid}" }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_merge_request_url(project.namespace, project, merge)
end
it 'links with adjacent text' do
doc = filter("Merge (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid merge IDs' do
exp = act = "Merge !#{merge.iid + 1}"
expect(filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
doc = filter("Merge #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
end
it 'escapes the title attribute' do
merge.update_attribute(:title, %{"></a>whatever<a title="})
doc = filter("Merge #{reference}")
expect(doc.text).to eq "Merge #{reference}"
end
it 'includes default classes' do
doc = filter("Merge #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
end
it 'includes an optional custom class' do
doc = filter("Merge #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("Merge #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) }
let(:merge) { create(:merge_request, source_project: project2) }
let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" }
context 'when user can access reference' do
before { allow_cross_reference! }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_merge_request_url(project2.namespace,
project, merge)
end
it 'links with adjacent text' do
doc = filter("Merge (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid merge IDs on the referenced project' do
exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}"
expect(filter(act).to_html).to eq exp
end
end
context 'when user cannot access reference' do
before { disallow_cross_reference! }
it 'ignores valid references' do
exp = act = "See #{reference}"
expect(filter(act).to_html).to eq exp
end
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe SnippetReferenceFilter do
include ReferenceFilterSpecHelper
let(:project) { create(:empty_project) }
let(:snippet) { create(:project_snippet, project: project) }
let(:reference) { "$#{snippet.id}" }
it 'requires project context' do
expect { described_class.call('Snippet $123', {}) }.
to raise_error(ArgumentError, /:project/)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Snippet #{reference}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
context 'internal reference' do
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_snippet_url(project.namespace, project, snippet)
end
it 'links with adjacent text' do
doc = filter("Snippet (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid snippet IDs' do
exp = act = "Snippet $#{snippet.id + 1}"
expect(filter(act).to_html).to eq exp
end
it 'includes a title attribute' do
doc = filter("Snippet #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
end
it 'escapes the title attribute' do
snippet.update_attribute(:title, %{"></a>whatever<a title="})
doc = filter("Snippet #{reference}")
expect(doc.text).to eq "Snippet #{reference}"
end
it 'includes default classes' do
doc = filter("Snippet #{reference}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
end
it 'includes an optional custom class' do
doc = filter("Snippet #{reference}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("Snippet #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
end
end
context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, namespace: namespace) }
let(:snippet) { create(:project_snippet, project: project2) }
let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" }
context 'when user can access reference' do
before { allow_cross_reference! }
it 'links to a valid reference' do
doc = filter("See #{reference}")
expect(doc.css('a').first.attr('href')).
to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
end
it 'links with adjacent text' do
doc = filter("See (#{reference}.)")
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
it 'ignores invalid snippet IDs on the referenced project' do
exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}"
expect(filter(act).to_html).to eq exp
end
end
context 'when user cannot access reference' do
before { disallow_cross_reference! }
it 'ignores valid references' do
exp = act = "See #{reference}"
expect(filter(act).to_html).to eq exp
end
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe UserReferenceFilter do
include ReferenceFilterSpecHelper
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
it 'requires project context' do
expect { described_class.call('Example @mention', {}) }.
to raise_error(ArgumentError, /:project/)
end
it 'ignores invalid users' do
exp = act = 'Hey @somebody'
expect(filter(act).to_html).to eq(exp)
end
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>"
expect(filter(act).to_html).to eq exp
end
end
context 'mentioning a user' do
it 'links to a User' do
doc = filter("Hey @#{user.username}")
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
end
# TODO (rspeicher): This test might be overkill
it 'links to a User with a period' do
user = create(:user, name: 'alphA.Beta')
doc = filter("Hey @#{user.username}")
expect(doc.css('a').length).to eq 1
end
# TODO (rspeicher): This test might be overkill
it 'links to a User with an underscore' do
user = create(:user, name: 'ping_pong_king')
doc = filter("Hey @#{user.username}")
expect(doc.css('a').length).to eq 1
end
end
context 'mentioning a group' do
let(:group) { create(:group) }
let(:user) { create(:user) }
it 'links to a Group that the current user can read' do
group.add_user(user, Gitlab::Access::DEVELOPER)
doc = filter("Hey @#{group.name}", current_user: user)
expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
end
it 'ignores references to a Group that the current user cannot read' do
doc = filter("Hey @#{group.name}", current_user: user)
expect(doc.to_html).to eq "Hey @#{group.name}"
end
end
it 'links with adjacent text' do
skip 'TODO (rspeicher): Re-enable when usernames can\'t end in periods.'
doc = filter("Mention me (@#{user.username}.)")
expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/)
end
it 'supports a special @all mention' do
doc = filter("Hey @all")
expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project)
end
it 'includes default classes' do
doc = filter("Hey @#{user.username}")
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
end
it 'includes an optional custom class' do
doc = filter("Hey @#{user.username}", reference_class: 'custom')
expect(doc.css('a').first.attr('class')).to include 'custom'
end
it 'supports an :only_path context' do
doc = filter("Hey @#{user.username}", only_path: true)
link = doc.css('a').first.attr('href')
expect(link).not_to match %r(https?://)
expect(link).to eq urls.user_path(user)
end
end
end
......@@ -74,7 +74,7 @@ describe Gitlab::ReferenceExtractor do
end
it 'handles all possible kinds of references' do
accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym }
accessors = described_class::TYPES.map { |t| "#{t}s".to_sym }
expect(subject).to respond_to(*accessors)
end
......@@ -106,6 +106,15 @@ describe Gitlab::ReferenceExtractor do
expect(subject.merge_requests).to eq([@m1, @m0])
end
it 'accesses valid labels' do
@l0 = create(:label, title: 'one', project: project)
@l1 = create(:label, title: 'two', project: project)
@l2 = create(:label)
subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}")
expect(subject.labels).to eq([@l0, @l1])
end
it 'accesses valid snippets' do
@s0 = create(:project_snippet, project: project)
@s1 = create(:project_snippet, project: project)
......
# Common methods and setup for Gitlab::Markdown reference filter specs
#
# Must be included into specs manually
module ReferenceFilterSpecHelper
extend ActiveSupport::Concern
included do
before { set_default_url_options }
end
# Allow *_url helpers to work
def set_default_url_options
Rails.application.routes.default_url_options = {
host: 'example.foo'
}
end
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module
def urls
Rails.application.routes.url_helpers
end
# Perform `call` on the described class
#
# Automatically passes the current `project` value to the context if none is
# provided.
#
# html - String text to pass to the filter's `call` method.
# contexts - Hash context for the filter. (default: {project: project})
#
# Returns the String text returned by the filter's `call` method.
def filter(html, contexts = {})
contexts.reverse_merge!(project: project)
described_class.call(html, contexts)
end
def allow_cross_reference!
allow_any_instance_of(described_class).
to receive(:user_can_reference_project?).and_return(true)
end
def disallow_cross_reference!
allow_any_instance_of(described_class).
to receive(:user_can_reference_project?).and_return(false)
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