Commit d80a4d3c authored by Douwe Maan's avatar Douwe Maan

Merge branch 'rs-redactor-filter' into 'master'

Separate rendering of Markdown reference links from redacting those the user doesn't have access to and extracting referenced objects

Related: !1014

See merge request !1090
parent f4d3639a
...@@ -19,7 +19,8 @@ module GitlabMarkdownHelper ...@@ -19,7 +19,8 @@ module GitlabMarkdownHelper
escape_once(body) escape_once(body)
end end
gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: current_user) user = current_user if defined?(current_user)
gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body) fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a' if fragment.children.size == 1 && fragment.children[0].name == 'a'
...@@ -45,29 +46,39 @@ module GitlabMarkdownHelper ...@@ -45,29 +46,39 @@ module GitlabMarkdownHelper
end end
def markdown(text, context = {}) def markdown(text, context = {})
return "" unless text.present?
context.reverse_merge!( context.reverse_merge!(
current_user: current_user,
path: @path, path: @path,
pipeline: :default,
project: @project, project: @project,
project_wiki: @project_wiki, project_wiki: @project_wiki,
ref: @ref ref: @ref
) )
Gitlab::Markdown.render(text, context) user = current_user if defined?(current_user)
html = Gitlab::Markdown.render(text, context)
Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user)
end end
# TODO (rspeicher): Remove all usages of this helper and just call `markdown` # TODO (rspeicher): Remove all usages of this helper and just call `markdown`
# with a custom pipeline depending on the content being rendered # with a custom pipeline depending on the content being rendered
def gfm(text, options = {}) def gfm(text, options = {})
return "" unless text.present?
options.reverse_merge!( options.reverse_merge!(
current_user: current_user,
path: @path, path: @path,
pipeline: :default,
project: @project, project: @project,
project_wiki: @project_wiki, project_wiki: @project_wiki,
ref: @ref ref: @ref
) )
Gitlab::Markdown.gfm(text, options) user = current_user if defined?(current_user)
html = Gitlab::Markdown.gfm(text, options)
Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user)
end end
def asciidoc(text) def asciidoc(text)
......
...@@ -2,13 +2,13 @@ class Commit ...@@ -2,13 +2,13 @@ class Commit
extend ActiveModel::Naming extend ActiveModel::Naming
include ActiveModel::Conversion include ActiveModel::Conversion
include Mentionable
include Participable include Participable
include Mentionable
include Referable include Referable
include StaticModel include StaticModel
attr_mentionable :safe_message attr_mentionable :safe_message
participant :author, :committer, :notes, :mentioned_users participant :author, :committer, :notes
attr_accessor :project attr_accessor :project
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Mentionable
include Participable include Participable
include Mentionable
included do included do
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
...@@ -47,8 +47,7 @@ module Issuable ...@@ -47,8 +47,7 @@ module Issuable
prefix: true prefix: true
attr_mentionable :title, :description attr_mentionable :title, :description
participant :author, :assignee, :notes_with_associations
participant :author, :assignee, :notes_with_associations, :mentioned_users
end end
module ClassMethods module ClassMethods
......
...@@ -20,6 +20,12 @@ module Mentionable ...@@ -20,6 +20,12 @@ module Mentionable
end end
end end
included do
if self < Participable
participant ->(current_user) { mentioned_users(current_user, load_lazy_references: false) }
end
end
# Returns the text used as the body of a Note when this object is referenced # Returns the text used as the body of a Note when this object is referenced
# #
# By default this will be the class name and the result of calling # By default this will be the class name and the result of calling
...@@ -41,22 +47,22 @@ module Mentionable ...@@ -41,22 +47,22 @@ module Mentionable
self self
end end
def all_references(current_user = self.author, text = self.mentionable_text) def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
ext = Gitlab::ReferenceExtractor.new(self.project, current_user) ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references)
ext.analyze(text) ext.analyze(text)
ext ext
end end
def mentioned_users(current_user = nil) def mentioned_users(current_user = nil, load_lazy_references: true)
all_references(current_user).users.uniq all_references(current_user, load_lazy_references: load_lazy_references).users
end end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference. # Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
def referenced_mentionables(current_user = self.author, text = self.mentionable_text) def referenced_mentionables(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
return [] if text.blank? return [] if text.blank?
refs = all_references(current_user, text) refs = all_references(current_user, text, load_lazy_references: load_lazy_references)
(refs.issues + refs.merge_requests + refs.commits).uniq - [local_reference] (refs.issues + refs.merge_requests + refs.commits) - [local_reference]
end end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# #
# # ... # # ...
# #
# participant :author, :assignee, :mentioned_users, :notes # participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) }
# end # end
# #
# issue = Issue.last # issue = Issue.last
...@@ -27,7 +27,7 @@ module Participable ...@@ -27,7 +27,7 @@ module Participable
module ClassMethods module ClassMethods
def participant(*attrs) def participant(*attrs)
participant_attrs.concat(attrs.map(&:to_s)) participant_attrs.concat(attrs)
end end
def participant_attrs def participant_attrs
...@@ -37,33 +37,39 @@ module Participable ...@@ -37,33 +37,39 @@ module Participable
# Be aware that this method makes a lot of sql queries. # Be aware that this method makes a lot of sql queries.
# Save result into variable if you are going to reuse it inside same request # Save result into variable if you are going to reuse it inside same request
def participants(current_user = self.author) def participants(current_user = self.author, load_lazy_references: true)
self.class.participant_attrs.flat_map do |attr| participants = self.class.participant_attrs.flat_map do |attr|
meth = method(attr)
value = value =
if meth.arity == 1 || meth.arity == -1 if attr.respond_to?(:call)
meth.call(current_user) instance_exec(current_user, &attr)
else else
meth.call send(attr)
end end
participants_for(value, current_user) participants_for(value, current_user)
end.compact.uniq.select do |user| end.compact.uniq
user.can?(:read_project, self.project)
if load_lazy_references
participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq
participants.select! do |user|
user.can?(:read_project, project)
end
end end
participants
end end
private private
def participants_for(value, current_user = nil) def participants_for(value, current_user = nil)
case value case value
when User when User, Gitlab::Markdown::ReferenceFilter::LazyReference
[value] [value]
when Enumerable, ActiveRecord::Relation when Enumerable, ActiveRecord::Relation
value.flat_map { |v| participants_for(v, current_user) } value.flat_map { |v| participants_for(v, current_user) }
when Participable when Participable
value.participants(current_user) value.participants(current_user, load_lazy_references: false)
end end
end end
end end
...@@ -22,14 +22,14 @@ require 'carrierwave/orm/activerecord' ...@@ -22,14 +22,14 @@ require 'carrierwave/orm/activerecord'
require 'file_size_validator' require 'file_size_validator'
class Note < ActiveRecord::Base class Note < ActiveRecord::Base
include Mentionable
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
include Participable include Participable
include Mentionable
default_value_for :system, false default_value_for :system, false
attr_mentionable :note attr_mentionable :note
participant :author, :mentioned_users participant :author
belongs_to :project belongs_to :project
belongs_to :noteable, polymorphic: true belongs_to :noteable, polymorphic: true
......
%div{xmlns: "http://www.w3.org/1999/xhtml"} %div{xmlns: "http://www.w3.org/1999/xhtml"}
- if issue.description.present? = markdown(issue.description, pipeline: :atom, project: issue.project)
= markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project)
%div{xmlns: "http://www.w3.org/1999/xhtml"} %div{xmlns: "http://www.w3.org/1999/xhtml"}
- if merge_request.description.present? = markdown(merge_request.description, pipeline: :atom, project: merge_request.project)
= markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project)
%div{xmlns: "http://www.w3.org/1999/xhtml"} %div{xmlns: "http://www.w3.org/1999/xhtml"}
= markdown(note.note, xhtml: true, reference_only_path: false, project: note.project) = markdown(note.note, pipeline: :atom, project: note.project)
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%i %i
at at
= commit[:timestamp].to_time.to_s(:short) = commit[:timestamp].to_time.to_s(:short)
%blockquote= markdown(escape_once(commit[:message]), xhtml: true, reference_only_path: false, project: event.project) %blockquote= markdown(escape_once(commit[:message]), pipeline: :atom, project: event.project)
- if event.commits_count > 15 - if event.commits_count > 15
%p %p
%i %i
......
%div %div
= markdown(@note.note, reference_only_path: false) = markdown(@note.note, pipeline: :email)
-if @issue.description -if @issue.description
= markdown(@issue.description, reference_only_path: false) = markdown(@issue.description, pipeline: :email)
- if @issue.assignee_id.present? - if @issue.assignee_id.present?
%p %p
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name} Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
-if @merge_request.description -if @merge_request.description
= markdown(@merge_request.description, reference_only_path: false) = markdown(@merge_request.description, pipeline: :email)
...@@ -7,6 +7,14 @@ module Gitlab ...@@ -7,6 +7,14 @@ module Gitlab
module Markdown module Markdown
# Convert a Markdown String into an HTML-safe String of HTML # Convert a Markdown String into an HTML-safe String of HTML
# #
# Note that while the returned HTML will have been sanitized of dangerous
# HTML, it may post a risk of information leakage if it's not also passed
# through `post_process`.
#
# Also note that the returned String is always HTML, not XHTML. Views
# requiring XHTML, such as Atom feeds, need to call `post_process` on the
# result, providing the appropriate `pipeline` option.
#
# markdown - Markdown String # markdown - Markdown String
# context - Hash of context options passed to our HTML Pipeline # context - Hash of context options passed to our HTML Pipeline
# #
...@@ -31,6 +39,33 @@ module Gitlab ...@@ -31,6 +39,33 @@ module Gitlab
renderer.render(markdown) renderer.render(markdown)
end end
# Perform post-processing on an HTML String
#
# This method is used to perform state-dependent changes to a String of
# HTML, such as removing references that the current user doesn't have
# permission to make (`RedactorFilter`).
#
# html - String to process
# options - Hash of options to customize output
# :pipeline - Symbol pipeline type
# :project - Project
# :user - User object
#
# Returns an HTML-safe String
def self.post_process(html, options)
context = {
project: options[:project],
current_user: options[:user]
}
doc = post_processor.to_document(html, context)
if options[:pipeline] == :atom
doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
else
doc.to_html
end.html_safe
end
# Provide autoload paths for filters to prevent a circular dependency error # Provide autoload paths for filters to prevent a circular dependency error
autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter'
autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter'
...@@ -41,6 +76,7 @@ module Gitlab ...@@ -41,6 +76,7 @@ module Gitlab
autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter'
autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter'
autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter'
autoload :RedactorFilter, 'gitlab/markdown/redactor_filter'
autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
...@@ -50,26 +86,20 @@ module Gitlab ...@@ -50,26 +86,20 @@ module Gitlab
autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter' autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter'
autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter' autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter'
# Public: Parse the provided text with GitLab-Flavored Markdown # Public: Parse the provided HTML with GitLab-Flavored Markdown
#
# html - HTML String
# options - A Hash of options used to customize output (default: {})
# :no_header_anchors - Disable header anchors in TableOfContentsFilter
# :path - Current path String
# :pipeline - Symbol pipeline type
# :project - Current Project object
# :project_wiki - Current ProjectWiki object
# :ref - Current ref String
# #
# text - the source text # Returns an HTML-safe String
# options - A Hash of options used to customize output (default: {}): def self.gfm(html, options = {})
# :xhtml - output XHTML instead of HTML return '' unless html.present?
# :reference_only_path - Use relative path for reference links
def self.gfm(text, options = {})
return text if text.nil?
# Duplicate the string so we don't alter the original, then call to_str
# to cast it back to a String instead of a SafeBuffer. This is required
# for gsub calls to work as we need them to.
text = text.dup.to_str
options.reverse_merge!(
xhtml: false,
reference_only_path: true,
project: options[:project],
current_user: options[:current_user]
)
@pipeline ||= HTML::Pipeline.new(filters) @pipeline ||= HTML::Pipeline.new(filters)
...@@ -78,41 +108,36 @@ module Gitlab ...@@ -78,41 +108,36 @@ module Gitlab
pipeline: options[:pipeline], pipeline: options[:pipeline],
# EmojiFilter # EmojiFilter
asset_root: Gitlab.config.gitlab.base_url,
asset_host: Gitlab::Application.config.asset_host, asset_host: Gitlab::Application.config.asset_host,
asset_root: Gitlab.config.gitlab.base_url,
# TableOfContentsFilter
no_header_anchors: options[:no_header_anchors],
# ReferenceFilter # ReferenceFilter
current_user: options[:current_user], only_path: only_path_pipeline?(options[:pipeline]),
only_path: options[:reference_only_path], project: options[:project],
project: options[:project],
# RelativeLinkFilter # RelativeLinkFilter
project_wiki: options[:project_wiki],
ref: options[:ref], ref: options[:ref],
requested_path: options[:path], requested_path: options[:path],
project_wiki: options[:project_wiki]
}
result = @pipeline.call(text, context)
save_options = 0 # TableOfContentsFilter
if options[:xhtml] no_header_anchors: options[:no_header_anchors]
save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML }
end
text = result[:output].to_html(save_with: save_options)
text.html_safe @pipeline.to_html(html, context).html_safe
end end
private private
def self.renderer # Check if a pipeline enables the `only_path` context option
@markdown ||= begin #
renderer = Redcarpet::Render::HTML.new # Returns Boolean
Redcarpet::Markdown.new(renderer, redcarpet_options) def self.only_path_pipeline?(pipeline)
case pipeline
when :atom, :email
false
else
true
end end
end end
...@@ -130,6 +155,17 @@ module Gitlab ...@@ -130,6 +155,17 @@ module Gitlab
}.freeze }.freeze
end end
def self.renderer
@markdown ||= begin
renderer = Redcarpet::Render::HTML.new
Redcarpet::Markdown.new(renderer, redcarpet_options)
end
end
def self.post_processor
@post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter])
end
# Filters used in our pipeline # Filters used in our pipeline
# #
# SanitizationFilter should come first so that all generated reference HTML # SanitizationFilter should come first so that all generated reference HTML
......
...@@ -26,6 +26,18 @@ module Gitlab ...@@ -26,6 +26,18 @@ module Gitlab
end end
end end
def self.referenced_by(node)
project = Project.find(node.attr("data-project")) rescue nil
return unless project
id = node.attr("data-commit-range")
range = CommitRange.new(id, project)
return unless range.valid_commits?
{ commit_range: range }
end
def initialize(*args) def initialize(*args)
super super
...@@ -53,13 +65,11 @@ module Gitlab ...@@ -53,13 +65,11 @@ module Gitlab
range = CommitRange.new(id, project) range = CommitRange.new(id, project)
if range.valid_commits? if range.valid_commits?
push_result(:commit_range, range)
url = url_for_commit_range(project, range) url = url_for_commit_range(project, range)
title = range.reference_title title = range.reference_title
klass = reference_class(:commit_range) klass = reference_class(:commit_range)
data = data_attribute(project.id) data = data_attribute(project: project.id, commit_range: id)
project_ref += '@' if project_ref project_ref += '@' if project_ref
......
...@@ -26,6 +26,18 @@ module Gitlab ...@@ -26,6 +26,18 @@ module Gitlab
end end
end end
def self.referenced_by(node)
project = Project.find(node.attr("data-project")) rescue nil
return unless project
id = node.attr("data-commit")
commit = commit_from_ref(project, id)
return unless commit
{ commit: commit }
end
def call def call
replace_text_nodes_matching(Commit.reference_pattern) do |content| replace_text_nodes_matching(Commit.reference_pattern) do |content|
commit_link_filter(content) commit_link_filter(content)
...@@ -39,17 +51,15 @@ module Gitlab ...@@ -39,17 +51,15 @@ module Gitlab
# Returns a String with commit references replaced with links. All links # Returns a String with commit references replaced with links. All links
# have `gfm` and `gfm-commit` class names attached for styling. # have `gfm` and `gfm-commit` class names attached for styling.
def commit_link_filter(text) def commit_link_filter(text)
self.class.references_in(text) do |match, commit_ref, project_ref| self.class.references_in(text) do |match, id, project_ref|
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if commit = commit_from_ref(project, commit_ref) if commit = self.class.commit_from_ref(project, id)
push_result(:commit, commit)
url = url_for_commit(project, commit) url = url_for_commit(project, commit)
title = escape_once(commit.link_title) title = escape_once(commit.link_title)
klass = reference_class(:commit) klass = reference_class(:commit)
data = data_attribute(project.id) data = data_attribute(project: project.id, commit: id)
project_ref += '@' if project_ref project_ref += '@' if project_ref
...@@ -62,9 +72,9 @@ module Gitlab ...@@ -62,9 +72,9 @@ module Gitlab
end end
end end
def commit_from_ref(project, commit_ref) def self.commit_from_ref(project, id)
if project && project.valid_repo? if project && project.valid_repo?
project.commit(commit_ref) project.commit(id)
end end
end end
......
...@@ -13,18 +13,11 @@ module Gitlab ...@@ -13,18 +13,11 @@ module Gitlab
# #
# ref - String reference. # ref - String reference.
# #
# Returns a Project, or nil if the reference can't be accessed # Returns a Project, or nil if the reference can't be found
def project_from_ref(ref) def project_from_ref(ref)
return context[:project] unless ref return context[:project] unless ref
other = Project.find_with_namespace(ref) 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
end end
......
...@@ -47,8 +47,9 @@ module Gitlab ...@@ -47,8 +47,9 @@ module Gitlab
title = escape_once("Issue in #{project.external_issue_tracker.title}") title = escape_once("Issue in #{project.external_issue_tracker.title}")
klass = reference_class(:issue) klass = reference_class(:issue)
data = data_attribute(project: project.id)
%(<a href="#{url}" %(<a href="#{url}" #{data}
title="#{title}" title="#{title}"
class="#{klass}">#{match}</a>) class="#{klass}">#{match}</a>)
end end
......
...@@ -27,6 +27,10 @@ module Gitlab ...@@ -27,6 +27,10 @@ module Gitlab
end end
end end
def self.referenced_by(node)
{ issue: LazyReference.new(Issue, node.attr("data-issue")) }
end
def call def call
replace_text_nodes_matching(Issue.reference_pattern) do |content| replace_text_nodes_matching(Issue.reference_pattern) do |content|
issue_link_filter(content) issue_link_filter(content)
...@@ -45,13 +49,11 @@ module Gitlab ...@@ -45,13 +49,11 @@ module Gitlab
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if project && issue = project.get_issue(id) if project && issue = project.get_issue(id)
push_result(:issue, issue)
url = url_for_issue(id, project, only_path: context[:only_path]) url = url_for_issue(id, project, only_path: context[:only_path])
title = escape_once("Issue: #{issue.title}") title = escape_once("Issue: #{issue.title}")
klass = reference_class(:issue) klass = reference_class(:issue)
data = data_attribute(project.id) data = data_attribute(project: project.id, issue: issue.id)
%(<a href="#{url}" #{data} %(<a href="#{url}" #{data}
title="#{title}" title="#{title}"
......
...@@ -22,6 +22,10 @@ module Gitlab ...@@ -22,6 +22,10 @@ module Gitlab
end end
end end
def self.referenced_by(node)
{ label: LazyReference.new(Label, node.attr("data-label")) }
end
def call def call
replace_text_nodes_matching(Label.reference_pattern) do |content| replace_text_nodes_matching(Label.reference_pattern) do |content|
label_link_filter(content) label_link_filter(content)
...@@ -41,11 +45,9 @@ module Gitlab ...@@ -41,11 +45,9 @@ module Gitlab
params = label_params(id, name) params = label_params(id, name)
if label = project.labels.find_by(params) if label = project.labels.find_by(params)
push_result(:label, label)
url = url_for_label(project, label) url = url_for_label(project, label)
klass = reference_class(:label) klass = reference_class(:label)
data = data_attribute(project.id) data = data_attribute(project: project.id, label: label.id)
%(<a href="#{url}" #{data} %(<a href="#{url}" #{data}
class="#{klass}">#{render_colored_label(label)}</a>) class="#{klass}">#{render_colored_label(label)}</a>)
......
...@@ -27,6 +27,10 @@ module Gitlab ...@@ -27,6 +27,10 @@ module Gitlab
end end
end end
def self.referenced_by(node)
{ merge_request: LazyReference.new(MergeRequest, node.attr("data-merge-request")) }
end
def call def call
replace_text_nodes_matching(MergeRequest.reference_pattern) do |content| replace_text_nodes_matching(MergeRequest.reference_pattern) do |content|
merge_request_link_filter(content) merge_request_link_filter(content)
...@@ -45,11 +49,9 @@ module Gitlab ...@@ -45,11 +49,9 @@ module Gitlab
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if project && merge_request = project.merge_requests.find_by(iid: id) if project && merge_request = project.merge_requests.find_by(iid: id)
push_result(:merge_request, merge_request)
title = escape_once("Merge Request: #{merge_request.title}") title = escape_once("Merge Request: #{merge_request.title}")
klass = reference_class(:merge_request) klass = reference_class(:merge_request)
data = data_attribute(project.id) data = data_attribute(project: project.id, merge_request: merge_request.id)
url = url_for_merge_request(merge_request, project) url = url_for_merge_request(merge_request, project)
......
require 'gitlab/markdown'
require 'html/pipeline/filter'
module Gitlab
module Markdown
# HTML filter that removes references to records that the current user does
# not have permission to view.
#
# Expected to be run in its own post-processing pipeline.
#
class RedactorFilter < HTML::Pipeline::Filter
def call
doc.css('a.gfm').each do |node|
unless user_can_reference?(node)
node.replace(node.text)
end
end
doc
end
private
def user_can_reference?(node)
if node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
reference_filter = reference_type.constantize
reference_filter.user_can_reference?(current_user, node, context)
else
true
end
end
def current_user
context[:current_user]
end
end
end
end
...@@ -11,30 +11,57 @@ module Gitlab ...@@ -11,30 +11,57 @@ module Gitlab
# Context options: # Context options:
# :project (required) - Current project, ignored if reference is cross-project. # :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links. # :only_path - Generate path-only links.
#
# Results:
# :references - A Hash of references that were found and replaced.
class ReferenceFilter < HTML::Pipeline::Filter class ReferenceFilter < HTML::Pipeline::Filter
def initialize(*args) LazyReference = Struct.new(:klass, :ids) do
super def self.load(refs)
lazy_references, values = refs.partition { |ref| ref.is_a?(self) }
lazy_values = lazy_references.group_by(&:klass).flat_map do |klass, refs|
ids = refs.flat_map(&:ids)
klass.where(id: ids)
end
values + lazy_values
end
def load
self.klass.where(id: self.ids)
end
end
def self.user_can_reference?(user, node, context)
if node.has_attribute?('data-project')
project_id = node.attr('data-project').to_i
return true if project_id == context[:project].try(:id)
project = Project.find(project_id) rescue nil
Ability.abilities.allowed?(user, :read_project, project)
else
true
end
end
result[:references] = Hash.new { |hash, type| hash[type] = [] } def self.referenced_by(node)
raise NotImplementedError, "#{self} does not implement #{__method__}"
end end
# Returns a data attribute String to attach to a reference link # Returns a data attribute String to attach to a reference link
# #
# id - Object ID # attributes - Hash, where the key becomes the data attribute name and the
# type - Object type (default: :project) # value is the data attribute value
# #
# Examples: # Examples:
# #
# data_attribute(1) # => "data-project-id=\"1\"" # data_attribute(project: 1, issue: 2)
# data_attribute(2, :user) # => "data-user-id=\"2\"" # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
# data_attribute(3, :group) # => "data-group-id=\"3\"" #
# data_attribute(project: 3, merge_request: 4)
# # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
# #
# Returns a String # Returns a String
def data_attribute(id, type = :project) def data_attribute(attributes = {})
%Q(data-#{type}-id="#{id}") attributes[:reference_filter] = self.class.name
attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
end end
def escape_once(html) def escape_once(html)
...@@ -59,16 +86,6 @@ module Gitlab ...@@ -59,16 +86,6 @@ module Gitlab
context[:project] context[:project]
end end
# Add a reference to the pipeline's result Hash
#
# type - Singular Symbol reference type (e.g., :issue, :user, etc.)
# values - One or more Objects to add
def push_result(type, *values)
return if values.empty?
result[:references][type].push(*values)
end
def reference_class(type) def reference_class(type)
"gfm gfm-#{type}" "gfm gfm-#{type}"
end end
...@@ -85,7 +102,7 @@ module Gitlab ...@@ -85,7 +102,7 @@ module Gitlab
# Yields the current node's String contents. The result of the block will # Yields the current node's String contents. The result of the block will
# replace the node's existing content and update the current document. # replace the node's existing content and update the current document.
# #
# Returns the updated Nokogiri::XML::Document object. # Returns the updated Nokogiri::HTML::DocumentFragment object.
def replace_text_nodes_matching(pattern) def replace_text_nodes_matching(pattern)
return doc if project.nil? return doc if project.nil?
......
require 'gitlab/markdown'
require 'html/pipeline/filter'
module Gitlab
module Markdown
# HTML filter that gathers all referenced records that the current user has
# permission to view.
#
# Expected to be run in its own post-processing pipeline.
#
class ReferenceGathererFilter < HTML::Pipeline::Filter
def initialize(*)
super
result[:references] ||= Hash.new { |hash, type| hash[type] = [] }
end
def call
doc.css('a.gfm').each do |node|
gather_references(node)
end
load_lazy_references unless context[:load_lazy_references] == false
doc
end
private
def gather_references(node)
return unless node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
reference_filter = reference_type.constantize
return if context[:reference_filter] && reference_filter != context[:reference_filter]
return unless reference_filter.user_can_reference?(current_user, node, context)
references = reference_filter.referenced_by(node)
return unless references
references.each do |type, values|
Array.wrap(values).each do |value|
result[:references][type] << value
end
end
end
# Will load all references of one type using one query.
def load_lazy_references
refs = result[:references]
refs.each do |type, values|
refs[type] = ReferenceFilter::LazyReference.load(values)
end
end
def current_user
context[:current_user]
end
end
end
end
...@@ -27,6 +27,10 @@ module Gitlab ...@@ -27,6 +27,10 @@ module Gitlab
end end
end end
def self.referenced_by(node)
{ snippet: LazyReference.new(Snippet, node.attr("data-snippet")) }
end
def call def call
replace_text_nodes_matching(Snippet.reference_pattern) do |content| replace_text_nodes_matching(Snippet.reference_pattern) do |content|
snippet_link_filter(content) snippet_link_filter(content)
...@@ -45,11 +49,9 @@ module Gitlab ...@@ -45,11 +49,9 @@ module Gitlab
project = self.project_from_ref(project_ref) project = self.project_from_ref(project_ref)
if project && snippet = project.snippets.find_by(id: id) if project && snippet = project.snippets.find_by(id: id)
push_result(:snippet, snippet)
title = escape_once("Snippet: #{snippet.title}") title = escape_once("Snippet: #{snippet.title}")
klass = reference_class(:snippet) klass = reference_class(:snippet)
data = data_attribute(project.id) data = data_attribute(project: project.id, snippet: snippet.id)
url = url_for_snippet(snippet, project) url = url_for_snippet(snippet, project)
......
...@@ -23,6 +23,31 @@ module Gitlab ...@@ -23,6 +23,31 @@ module Gitlab
end end
end end
def self.referenced_by(node)
if node.has_attribute?('data-group')
group = Group.find(node.attr('data-group')) rescue nil
return unless group
{ user: group.users }
elsif node.has_attribute?('data-user')
{ user: LazyReference.new(User, node.attr('data-user')) }
elsif node.has_attribute?('data-project')
project = Project.find(node.attr('data-project')) rescue nil
return unless project
{ user: project.team.members.flatten }
end
end
def self.user_can_reference?(user, node, context)
if node.has_attribute?('data-group')
group = Group.find(node.attr('data-group')) rescue nil
Ability.abilities.allowed?(user, :read_group, group)
else
super
end
end
def call def call
replace_text_nodes_matching(User.reference_pattern) do |content| replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content) user_link_filter(content)
...@@ -61,14 +86,12 @@ module Gitlab ...@@ -61,14 +86,12 @@ module Gitlab
def link_to_all def link_to_all
project = context[:project] project = context[:project]
# FIXME (rspeicher): Law of Demeter
push_result(:user, *project.team.members.flatten)
url = urls.namespace_project_url(project.namespace, project, url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path]) only_path: context[:only_path])
data = data_attribute(project: project.id)
text = User.reference_prefix + 'all' text = User.reference_prefix + 'all'
%(<a href="#{url}" class="#{link_class}">#{text}</a>) %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
end end
def link_to_namespace(namespace) def link_to_namespace(namespace)
...@@ -80,30 +103,20 @@ module Gitlab ...@@ -80,30 +103,20 @@ module Gitlab
end end
def link_to_group(group, namespace) def link_to_group(group, namespace)
return unless user_can_reference_group?(namespace)
push_result(:user, *namespace.users)
url = urls.group_url(group, only_path: context[:only_path]) url = urls.group_url(group, only_path: context[:only_path])
data = data_attribute(namespace.id, :group) data = data_attribute(group: namespace.id)
text = Group.reference_prefix + group text = Group.reference_prefix + group
%(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
end end
def link_to_user(user, namespace) def link_to_user(user, namespace)
push_result(:user, namespace.owner)
url = urls.user_url(user, only_path: context[:only_path]) url = urls.user_url(user, only_path: context[:only_path])
data = data_attribute(namespace.owner_id, :user) data = data_attribute(user: namespace.owner_id)
text = User.reference_prefix + user text = User.reference_prefix + user
%(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
end end
def user_can_reference_group?(group)
Ability.abilities.allowed?(context[:current_user], :read_group, group)
end
end end
end end
end end
...@@ -3,11 +3,12 @@ require 'gitlab/markdown' ...@@ -3,11 +3,12 @@ require 'gitlab/markdown'
module Gitlab module Gitlab
# Extract possible GFM references from an arbitrary String for further processing. # Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor class ReferenceExtractor
attr_accessor :project, :current_user attr_accessor :project, :current_user, :load_lazy_references
def initialize(project, current_user = nil) def initialize(project, current_user = nil, load_lazy_references: true)
@project = project @project = project
@current_user = current_user @current_user = current_user
@load_lazy_references = load_lazy_references
end end
def analyze(text) def analyze(text)
...@@ -28,7 +29,7 @@ module Gitlab ...@@ -28,7 +29,7 @@ module Gitlab
type = type.to_sym type = type.to_sym
return references[type] if references.has_key?(type) return references[type] if references.has_key?(type)
references[type] = pipeline_result(type).uniq references[type] = pipeline_result(type)
end end
end end
...@@ -41,21 +42,32 @@ module Gitlab ...@@ -41,21 +42,32 @@ module Gitlab
def pipeline_result(filter_type) def pipeline_result(filter_type)
return [] if @text.blank? return [] if @text.blank?
klass = filter_type.to_s.camelize + 'ReferenceFilter' klass = "#{filter_type.to_s.camelize}ReferenceFilter"
filter = Gitlab::Markdown.const_get(klass) filter = Gitlab::Markdown.const_get(klass)
context = { context = {
project: project, project: project,
current_user: current_user, current_user: current_user,
# We don't actually care about the links generated # We don't actually care about the links generated
only_path: true, only_path: true,
ignore_blockquotes: true ignore_blockquotes: true,
# ReferenceGathererFilter
load_lazy_references: false,
reference_filter: filter
} }
pipeline = HTML::Pipeline.new([filter], context) pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context)
result = pipeline.call(@text) result = pipeline.call(@text)
result[:references][filter_type] values = result[:references][filter_type].uniq
if @load_lazy_references
values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq
end
values
end end
end end
end end
...@@ -220,7 +220,7 @@ describe 'GitLab Markdown', feature: true do ...@@ -220,7 +220,7 @@ describe 'GitLab Markdown', feature: true do
end end
end end
# `markdown` calls these two methods # Fake a `current_user` helper
def current_user def current_user
@feat.user @feat.user
end end
......
...@@ -11,12 +11,15 @@ describe GitlabMarkdownHelper do ...@@ -11,12 +11,15 @@ describe GitlabMarkdownHelper do
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:snippet) { create(:project_snippet, project: project) } let(:snippet) { create(:project_snippet, project: project) }
# Helper expects a current_user method.
let(:current_user) { user }
before do before do
# Ensure the generated reference links aren't redacted
project.team << [user, :master]
# Helper expects a @project instance variable # Helper expects a @project instance variable
@project = project helper.instance_variable_set(:@project, project)
# Stub the `current_user` helper
allow(helper).to receive(:current_user).and_return(user)
end end
describe "#markdown" do describe "#markdown" do
...@@ -25,23 +28,23 @@ describe GitlabMarkdownHelper do ...@@ -25,23 +28,23 @@ describe GitlabMarkdownHelper do
it "should link to the merge request" do it "should link to the merge request" do
expected = namespace_project_merge_request_path(project.namespace, project, merge_request) expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(markdown(actual)).to match(expected) expect(helper.markdown(actual)).to match(expected)
end end
it "should link to the commit" do it "should link to the commit" do
expected = namespace_project_commit_path(project.namespace, project, commit) expected = namespace_project_commit_path(project.namespace, project, commit)
expect(markdown(actual)).to match(expected) expect(helper.markdown(actual)).to match(expected)
end end
it "should link to the issue" do it "should link to the issue" do
expected = namespace_project_issue_path(project.namespace, project, issue) expected = namespace_project_issue_path(project.namespace, project, issue)
expect(markdown(actual)).to match(expected) expect(helper.markdown(actual)).to match(expected)
end end
end end
describe "override default project" do describe "override default project" do
let(:actual) { issue.to_reference } let(:actual) { issue.to_reference }
let(:second_project) { create(:project) } let(:second_project) { create(:project, :public) }
let(:second_issue) { create(:issue, project: second_project) } let(:second_issue) { create(:issue, project: second_project) }
it 'should link to the issue' do it 'should link to the issue' do
...@@ -56,7 +59,7 @@ describe GitlabMarkdownHelper do ...@@ -56,7 +59,7 @@ describe GitlabMarkdownHelper do
let(:issues) { create_list(:issue, 2, project: project) } let(:issues) { create_list(:issue, 2, project: project) }
it 'should handle references nested in links with all the text' do it 'should handle references nested in links with all the text' do
actual = link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path) actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
doc = Nokogiri::HTML.parse(actual) doc = Nokogiri::HTML.parse(actual)
# Make sure we didn't create invalid markup # Make sure we didn't create invalid markup
...@@ -86,7 +89,7 @@ describe GitlabMarkdownHelper do ...@@ -86,7 +89,7 @@ describe GitlabMarkdownHelper do
end end
it 'should forward HTML options' do it 'should forward HTML options' do
actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo') actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
doc = Nokogiri::HTML.parse(actual) doc = Nokogiri::HTML.parse(actual)
expect(doc.css('a')).to satisfy do |v| expect(doc.css('a')).to satisfy do |v|
...@@ -97,13 +100,13 @@ describe GitlabMarkdownHelper do ...@@ -97,13 +100,13 @@ describe GitlabMarkdownHelper do
it "escapes HTML passed in as the body" do it "escapes HTML passed in as the body" do
actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}" actual = "This is a <h1>test</h1> - see #{issues[0].to_reference}"
expect(link_to_gfm(actual, commit_path)). expect(helper.link_to_gfm(actual, commit_path)).
to match('&lt;h1&gt;test&lt;/h1&gt;') to match('&lt;h1&gt;test&lt;/h1&gt;')
end end
it 'ignores reference links when they are the entire body' do it 'ignores reference links when they are the entire body' do
text = issues[0].to_reference text = issues[0].to_reference
act = link_to_gfm(text, '/foo') act = helper.link_to_gfm(text, '/foo')
expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>) expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
end end
......
...@@ -140,28 +140,28 @@ describe Gitlab::ClosingIssueExtractor do ...@@ -140,28 +140,28 @@ describe Gitlab::ClosingIssueExtractor do
message = "Closes #{reference} and fix #{reference2}" message = "Closes #{reference} and fix #{reference2}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue]) to match_array([issue, other_issue])
end end
it 'fetches comma-separated issues references in single line message' do it 'fetches comma-separated issues references in single line message' do
message = "Closes #{reference}, closes #{reference2}" message = "Closes #{reference}, closes #{reference2}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue]) to match_array([issue, other_issue])
end end
it 'fetches comma-separated issues numbers in single line message' do it 'fetches comma-separated issues numbers in single line message' do
message = "Closes #{reference}, #{reference2} and #{reference3}" message = "Closes #{reference}, #{reference2} and #{reference3}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue, third_issue]) to match_array([issue, other_issue, third_issue])
end end
it 'fetches issues in multi-line message' do it 'fetches issues in multi-line message' do
message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue]) to match_array([issue, other_issue])
end end
it 'fetches issues in hybrid message' do it 'fetches issues in hybrid message' do
...@@ -169,7 +169,7 @@ describe Gitlab::ClosingIssueExtractor do ...@@ -169,7 +169,7 @@ describe Gitlab::ClosingIssueExtractor do
"Also fixing issues #{reference2}, #{reference3} and #4" "Also fixing issues #{reference2}, #{reference3} and #4"
expect(subject.closed_by_message(message)). expect(subject.closed_by_message(message)).
to eq([issue, other_issue, third_issue]) to match_array([issue, other_issue, third_issue])
end end
end end
end end
......
...@@ -4,7 +4,7 @@ module Gitlab::Markdown ...@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe CommitRangeReferenceFilter do describe CommitRangeReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project) } let(:project) { create(:project, :public) }
let(:commit1) { project.commit } let(:commit1) { project.commit }
let(:commit2) { project.commit("HEAD~2") } let(:commit2) { project.commit("HEAD~2") }
...@@ -75,12 +75,20 @@ module Gitlab::Markdown ...@@ -75,12 +75,20 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
end end
it 'includes a data-project-id attribute' do it 'includes a data-project attribute' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
link = doc.css('a').first link = doc.css('a').first
expect(link).to have_attribute('data-project-id') expect(link).to have_attribute('data-project')
expect(link.attr('data-project-id')).to eq project.id.to_s expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-commit-range attribute' do
doc = filter("See #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-commit-range')
expect(link.attr('data-commit-range')).to eq range.to_reference
end end
it 'supports an :only_path option' do it 'supports an :only_path option' do
...@@ -92,59 +100,45 @@ module Gitlab::Markdown ...@@ -92,59 +100,45 @@ module Gitlab::Markdown
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("See #{reference}") result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty expect(result[:references][:commit_range]).not_to be_empty
end end
end end
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) } let(:project2) { create(:project, :public, namespace: namespace) }
let(:reference) { range.to_reference(project) } let(:reference) { range.to_reference(project) }
before do before do
range.project = project2 range.project = project2
end end
context 'when user can access reference' do it 'links to a valid reference' do
before { allow_cross_reference! } doc = filter("See #{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, range.to_param)
end
it 'links with adjacent text' do
doc = filter("Fixed (#{reference}.)")
exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}")
expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
end
it 'ignores invalid commit IDs on the referenced project' do expect(doc.css('a').first.attr('href')).
exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
expect(filter(act).to_html).to eq exp end
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" it 'links with adjacent text' do
expect(filter(act).to_html).to eq exp doc = filter("Fixed (#{reference}.)")
end
it 'adds to the results hash' do exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}")
result = pipeline_result("See #{reference}") expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
expect(result[:references][:commit_range]).not_to be_empty
end
end end
context 'when user cannot access reference' do it 'ignores invalid commit IDs on the referenced project' do
before { disallow_cross_reference! } exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
expect(filter(act).to_html).to eq exp
it 'ignores valid references' do exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
exp = act = "See #{reference}" expect(filter(act).to_html).to eq exp
end
expect(filter(act).to_html).to eq exp it 'adds to the results hash' do
end result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit_range]).not_to be_empty
end end
end end
end end
......
...@@ -4,7 +4,7 @@ module Gitlab::Markdown ...@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe CommitReferenceFilter do describe CommitReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project) } let(:project) { create(:project, :public) }
let(:commit) { project.commit } let(:commit) { project.commit }
it 'requires project context' do it 'requires project context' do
...@@ -71,12 +71,20 @@ module Gitlab::Markdown ...@@ -71,12 +71,20 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
end end
it 'includes a data-project-id attribute' do it 'includes a data-project attribute' do
doc = filter("See #{reference}") doc = filter("See #{reference}")
link = doc.css('a').first link = doc.css('a').first
expect(link).to have_attribute('data-project-id') expect(link).to have_attribute('data-project')
expect(link.attr('data-project-id')).to eq project.id.to_s expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-commit attribute' do
doc = filter("See #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-commit')
expect(link.attr('data-commit')).to eq commit.id
end end
it 'supports an :only_path context' do it 'supports an :only_path context' do
...@@ -88,53 +96,39 @@ module Gitlab::Markdown ...@@ -88,53 +96,39 @@ module Gitlab::Markdown
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("See #{reference}") result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty expect(result[:references][:commit]).not_to be_empty
end end
end end
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) } let(:project2) { create(:project, :public, namespace: namespace) }
let(:commit) { project2.commit } let(:commit) { project2.commit }
let(:reference) { commit.to_reference(project) } let(:reference) { commit.to_reference(project) }
context 'when user can access reference' do it 'links to a valid reference' do
before { allow_cross_reference! } doc = filter("See #{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}.)")
exp = Regexp.escape(project2.to_reference) expect(doc.css('a').first.attr('href')).
expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
end end
it 'ignores invalid commit IDs on the referenced project' do it 'links with adjacent text' do
exp = act = "Committed #{invalidate_reference(reference)}" doc = filter("Fixed (#{reference}.)")
expect(filter(act).to_html).to eq exp
end
it 'adds to the results hash' do exp = Regexp.escape(project2.to_reference)
result = pipeline_result("See #{reference}") expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
expect(result[:references][:commit]).not_to be_empty
end
end end
context 'when user cannot access reference' do it 'ignores invalid commit IDs on the referenced project' do
before { disallow_cross_reference! } exp = act = "Committed #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp
it 'ignores valid references' do end
exp = act = "See #{reference}"
expect(filter(act).to_html).to eq exp it 'adds to the results hash' do
end result = reference_pipeline_result("See #{reference}")
expect(result[:references][:commit]).not_to be_empty
end end
end end
end end
......
...@@ -2,20 +2,16 @@ require 'spec_helper' ...@@ -2,20 +2,16 @@ require 'spec_helper'
module Gitlab::Markdown module Gitlab::Markdown
describe CrossProjectReference do 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 include described_class
describe '#project_from_ref' do describe '#project_from_ref' do
context 'when no project was referenced' do context 'when no project was referenced' do
it 'returns the project from context' do it 'returns the project from context' do
expect(project_from_ref(nil)).to eq context[:project] project = double
allow(self).to receive(:context).and_return({ project: project })
expect(project_from_ref(nil)).to eq project
end end
end end
...@@ -26,29 +22,13 @@ module Gitlab::Markdown ...@@ -26,29 +22,13 @@ module Gitlab::Markdown
end end
context 'when referenced project exists' do context 'when referenced project exists' do
let(:project2) { double('referenced project') } it 'returns the referenced project' do
project2 = double('referenced project')
before do
expect(Project).to receive(:find_with_namespace). expect(Project).to receive(:find_with_namespace).
with('cross/reference').and_return(project2) 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 expect(project_from_ref('cross/reference')).to eq project2
end
end end
end end
end end
......
...@@ -8,7 +8,7 @@ module Gitlab::Markdown ...@@ -8,7 +8,7 @@ module Gitlab::Markdown
IssuesHelper IssuesHelper
end end
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
it 'requires project context' do it 'requires project context' do
...@@ -68,12 +68,20 @@ module Gitlab::Markdown ...@@ -68,12 +68,20 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
end end
it 'includes a data-project-id attribute' do it 'includes a data-project attribute' do
doc = filter("Issue #{reference}") doc = filter("Issue #{reference}")
link = doc.css('a').first link = doc.css('a').first
expect(link).to have_attribute('data-project-id') expect(link).to have_attribute('data-project')
expect(link.attr('data-project-id')).to eq project.id.to_s expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-issue attribute' do
doc = filter("See #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-issue')
expect(link.attr('data-issue')).to eq issue.id.to_s
end end
it 'supports an :only_path context' do it 'supports an :only_path context' do
...@@ -85,60 +93,46 @@ module Gitlab::Markdown ...@@ -85,60 +93,46 @@ module Gitlab::Markdown
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("Fixed #{reference}") result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue] expect(result[:references][:issue]).to eq [issue]
end end
end end
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, namespace: namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) } let(:issue) { create(:issue, project: project2) }
let(:reference) { issue.to_reference(project) } let(:reference) { issue.to_reference(project) }
context 'when user can access reference' do it 'ignores valid references when cross-reference project uses external tracker' do
before { allow_cross_reference! } expect_any_instance_of(Project).to receive(:get_issue).
with(issue.iid).and_return(nil)
it 'ignores valid references when cross-reference project uses external tracker' do
expect_any_instance_of(Project).to receive(:get_issue).
with(issue.iid).and_return(nil)
exp = act = "Issue #{reference}"
expect(filter(act).to_html).to eq exp
end
it 'links to a valid reference' do exp = act = "Issue #{reference}"
doc = filter("See #{reference}") expect(filter(act).to_html).to eq exp
end
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 it 'links to a valid reference' do
exp = act = "Fixed #{invalidate_reference(reference)}" doc = filter("See #{reference}")
expect(filter(act).to_html).to eq exp expect(doc.css('a').first.attr('href')).
end to eq helper.url_for_issue(issue.iid, project2)
end
it 'adds to the results hash' do it 'links with adjacent text' do
result = pipeline_result("Fixed #{reference}") doc = filter("Fixed (#{reference}.)")
expect(result[:references][:issue]).to eq [issue] expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
end end
context 'when user cannot access reference' do it 'ignores invalid issue IDs on the referenced project' do
before { disallow_cross_reference! } exp = act = "Fixed #{invalidate_reference(reference)}"
it 'ignores valid references' do expect(filter(act).to_html).to eq exp
exp = act = "See #{reference}" end
expect(filter(act).to_html).to eq exp it 'adds to the results hash' do
end result = reference_pipeline_result("Fixed #{reference}")
expect(result[:references][:issue]).to eq [issue]
end end
end end
end end
......
...@@ -5,7 +5,7 @@ module Gitlab::Markdown ...@@ -5,7 +5,7 @@ module Gitlab::Markdown
describe LabelReferenceFilter do describe LabelReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, :public) }
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
let(:reference) { label.to_reference } let(:reference) { label.to_reference }
...@@ -25,12 +25,20 @@ module Gitlab::Markdown ...@@ -25,12 +25,20 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
end end
it 'includes a data-project-id attribute' do it 'includes a data-project attribute' do
doc = filter("Label #{reference}") doc = filter("Label #{reference}")
link = doc.css('a').first link = doc.css('a').first
expect(link).to have_attribute('data-project-id') expect(link).to have_attribute('data-project')
expect(link.attr('data-project-id')).to eq project.id.to_s expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-label attribute' do
doc = filter("See #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-label')
expect(link.attr('data-label')).to eq label.id.to_s
end end
it 'supports an :only_path context' do it 'supports an :only_path context' do
...@@ -42,7 +50,7 @@ module Gitlab::Markdown ...@@ -42,7 +50,7 @@ module Gitlab::Markdown
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("Label #{reference}") result = reference_pipeline_result("Label #{reference}")
expect(result[:references][:label]).to eq [label] expect(result[:references][:label]).to eq [label]
end end
......
...@@ -4,7 +4,7 @@ module Gitlab::Markdown ...@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe MergeRequestReferenceFilter do describe MergeRequestReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:project) } let(:project) { create(:project, :public) }
let(:merge) { create(:merge_request, source_project: project) } let(:merge) { create(:merge_request, source_project: project) }
it 'requires project context' do it 'requires project context' do
...@@ -56,12 +56,20 @@ module Gitlab::Markdown ...@@ -56,12 +56,20 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
end end
it 'includes a data-project-id attribute' do it 'includes a data-project attribute' do
doc = filter("Merge #{reference}") doc = filter("Merge #{reference}")
link = doc.css('a').first link = doc.css('a').first
expect(link).to have_attribute('data-project-id') expect(link).to have_attribute('data-project')
expect(link.attr('data-project-id')).to eq project.id.to_s expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-merge-request attribute' do
doc = filter("See #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-merge-request')
expect(link.attr('data-merge-request')).to eq merge.id.to_s
end end
it 'supports an :only_path context' do it 'supports an :only_path context' do
...@@ -73,53 +81,39 @@ module Gitlab::Markdown ...@@ -73,53 +81,39 @@ module Gitlab::Markdown
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("Merge #{reference}") result = reference_pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge] expect(result[:references][:merge_request]).to eq [merge]
end end
end end
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:project, namespace: namespace) } let(:project2) { create(:project, :public, namespace: namespace) }
let(:merge) { create(:merge_request, source_project: project2) } let(:merge) { create(:merge_request, source_project: project2) }
let(:reference) { merge.to_reference(project) } let(:reference) { merge.to_reference(project) }
context 'when user can access reference' do it 'links to a valid reference' do
before { allow_cross_reference! } doc = filter("See #{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 #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(doc.css('a').first.attr('href')).
end to eq urls.namespace_project_merge_request_url(project2.namespace,
project, merge)
end
it 'adds to the results hash' do it 'links with adjacent text' do
result = pipeline_result("Merge #{reference}") doc = filter("Merge (#{reference}.)")
expect(result[:references][:merge_request]).to eq [merge] expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
end end
context 'when user cannot access reference' do it 'ignores invalid merge IDs on the referenced project' do
before { disallow_cross_reference! } exp = act = "Merge #{invalidate_reference(reference)}"
it 'ignores valid references' do expect(filter(act).to_html).to eq exp
exp = act = "See #{reference}" end
expect(filter(act).to_html).to eq exp it 'adds to the results hash' do
end result = reference_pipeline_result("Merge #{reference}")
expect(result[:references][:merge_request]).to eq [merge]
end end
end end
end end
......
require 'spec_helper'
module Gitlab::Markdown
describe RedactorFilter do
include ActionView::Helpers::UrlHelper
include FilterSpecHelper
it 'ignores non-GFM links' do
html = %(See <a href="https://google.com/">Google</a>)
doc = filter(html, current_user: double)
expect(doc.css('a').length).to eq 1
end
def reference_link(data)
link_to('text', '', class: 'gfm', data: data)
end
context 'with data-project' do
it 'removes unpermitted Project references' do
user = create(:user)
project = create(:empty_project)
link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
end
it 'allows permitted Project references' do
user = create(:user)
project = create(:empty_project)
project.team << [user, :master]
link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Project references' do
link = reference_link(project: 12345, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
expect { filter(link) }.not_to raise_error
end
end
context "for user references" do
context 'with data-group' do
it 'removes unpermitted Group references' do
user = create(:user)
group = create(:group)
link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 0
end
it 'allows permitted Group references' do
user = create(:user)
group = create(:group)
group.add_developer(user)
link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
doc = filter(link, current_user: user)
expect(doc.css('a').length).to eq 1
end
it 'handles invalid Group references' do
link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
expect { filter(link) }.not_to raise_error
end
end
context 'with data-user' do
it 'allows any User reference' do
user = create(:user)
link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
doc = filter(link)
expect(doc.css('a').length).to eq 1
end
end
end
end
end
require 'spec_helper'
module Gitlab::Markdown
describe ReferenceGathererFilter do
include ActionView::Helpers::UrlHelper
include FilterSpecHelper
def reference_link(data)
link_to('text', '', class: 'gfm', data: data)
end
context "for issue references" do
context 'with data-project' do
it 'removes unpermitted Project references' do
user = create(:user)
project = create(:empty_project)
issue = create(:issue, project: project)
link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
result = pipeline_result(link, current_user: user)
expect(result[:references][:issue]).to be_empty
end
it 'allows permitted Project references' do
user = create(:user)
project = create(:empty_project)
issue = create(:issue, project: project)
project.team << [user, :master]
link = reference_link(project: project.id, issue: issue.id, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
result = pipeline_result(link, current_user: user)
expect(result[:references][:issue]).to eq([issue])
end
it 'handles invalid Project references' do
link = reference_link(project: 12345, issue: 12345, reference_filter: Gitlab::Markdown::IssueReferenceFilter.name)
expect { pipeline_result(link) }.not_to raise_error
end
end
end
context "for user references" do
context 'with data-group' do
it 'removes unpermitted Group references' do
user = create(:user)
group = create(:group)
link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
result = pipeline_result(link, current_user: user)
expect(result[:references][:user]).to be_empty
end
it 'allows permitted Group references' do
user = create(:user)
group = create(:group)
group.add_developer(user)
link = reference_link(group: group.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
result = pipeline_result(link, current_user: user)
expect(result[:references][:user]).to eq([user])
end
it 'handles invalid Group references' do
link = reference_link(group: 12345, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
expect { pipeline_result(link) }.not_to raise_error
end
end
context 'with data-user' do
it 'allows any User reference' do
user = create(:user)
link = reference_link(user: user.id, reference_filter: Gitlab::Markdown::UserReferenceFilter.name)
result = pipeline_result(link)
expect(result[:references][:user]).to eq([user])
end
end
end
end
end
...@@ -4,7 +4,7 @@ module Gitlab::Markdown ...@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe SnippetReferenceFilter do describe SnippetReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:project_snippet, project: project) } let(:snippet) { create(:project_snippet, project: project) }
let(:reference) { snippet.to_reference } let(:reference) { snippet.to_reference }
...@@ -55,12 +55,20 @@ module Gitlab::Markdown ...@@ -55,12 +55,20 @@ module Gitlab::Markdown
expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
end end
it 'includes a data-project-id attribute' do it 'includes a data-project attribute' do
doc = filter("Snippet #{reference}") doc = filter("Snippet #{reference}")
link = doc.css('a').first link = doc.css('a').first
expect(link).to have_attribute('data-project-id') expect(link).to have_attribute('data-project')
expect(link.attr('data-project-id')).to eq project.id.to_s expect(link.attr('data-project')).to eq project.id.to_s
end
it 'includes a data-snippet attribute' do
doc = filter("See #{reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-snippet')
expect(link.attr('data-snippet')).to eq snippet.id.to_s
end end
it 'supports an :only_path context' do it 'supports an :only_path context' do
...@@ -72,52 +80,38 @@ module Gitlab::Markdown ...@@ -72,52 +80,38 @@ module Gitlab::Markdown
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("Snippet #{reference}") result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet] expect(result[:references][:snippet]).to eq [snippet]
end end
end end
context 'cross-project reference' do context 'cross-project reference' do
let(:namespace) { create(:namespace, name: 'cross-reference') } let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, namespace: namespace) } let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:snippet) { create(:project_snippet, project: project2) } let(:snippet) { create(:project_snippet, project: project2) }
let(:reference) { snippet.to_reference(project) } let(:reference) { snippet.to_reference(project) }
context 'when user can access reference' do it 'links to a valid reference' do
before { allow_cross_reference! } doc = filter("See #{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 #{invalidate_reference(reference)}"
expect(filter(act).to_html).to eq exp expect(doc.css('a').first.attr('href')).
end to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
end
it 'adds to the results hash' do it 'links with adjacent text' do
result = pipeline_result("Snippet #{reference}") doc = filter("See (#{reference}.)")
expect(result[:references][:snippet]).to eq [snippet] expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
end
end end
context 'when user cannot access reference' do it 'ignores invalid snippet IDs on the referenced project' do
before { disallow_cross_reference! } exp = act = "See #{invalidate_reference(reference)}"
it 'ignores valid references' do expect(filter(act).to_html).to eq exp
exp = act = "See #{reference}" end
expect(filter(act).to_html).to eq exp it 'adds to the results hash' do
end result = reference_pipeline_result("Snippet #{reference}")
expect(result[:references][:snippet]).to eq [snippet]
end end
end end
end end
......
...@@ -4,7 +4,7 @@ module Gitlab::Markdown ...@@ -4,7 +4,7 @@ module Gitlab::Markdown
describe UserReferenceFilter do describe UserReferenceFilter do
include FilterSpecHelper include FilterSpecHelper
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, :public) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:reference) { user.to_reference } let(:reference) { user.to_reference }
...@@ -39,7 +39,7 @@ module Gitlab::Markdown ...@@ -39,7 +39,7 @@ module Gitlab::Markdown
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("Hey #{reference}") result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [project.creator] expect(result[:references][:user]).to eq [project.creator]
end end
end end
...@@ -64,59 +64,40 @@ module Gitlab::Markdown ...@@ -64,59 +64,40 @@ module Gitlab::Markdown
expect(doc.css('a').length).to eq 1 expect(doc.css('a').length).to eq 1
end end
it 'includes a data-user-id attribute' do it 'includes a data-user attribute' do
doc = filter("Hey #{reference}") doc = filter("Hey #{reference}")
link = doc.css('a').first link = doc.css('a').first
expect(link).to have_attribute('data-user-id') expect(link).to have_attribute('data-user')
expect(link.attr('data-user-id')).to eq user.namespace.owner_id.to_s expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s
end end
it 'adds to the results hash' do it 'adds to the results hash' do
result = pipeline_result("Hey #{reference}") result = reference_pipeline_result("Hey #{reference}")
expect(result[:references][:user]).to eq [user] expect(result[:references][:user]).to eq [user]
end end
end end
context 'mentioning a group' do context 'mentioning a group' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:user) { create(:user) }
let(:reference) { group.to_reference } let(:reference) { group.to_reference }
context 'that the current user can read' do it 'links to the Group' do
before do doc = filter("Hey #{reference}")
group.add_developer(user) expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
end end
it 'links to the Group' do
doc = filter("Hey #{reference}", current_user: user)
expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
end
it 'includes a data-group-id attribute' do
doc = filter("Hey #{reference}", current_user: user)
link = doc.css('a').first
expect(link).to have_attribute('data-group-id') it 'includes a data-group attribute' do
expect(link.attr('data-group-id')).to eq group.id.to_s doc = filter("Hey #{reference}")
end link = doc.css('a').first
it 'adds to the results hash' do expect(link).to have_attribute('data-group')
result = pipeline_result("Hey #{reference}", current_user: user) expect(link.attr('data-group')).to eq group.id.to_s
expect(result[:references][:user]).to eq group.users
end
end end
context 'that the current user cannot read' do it 'adds to the results hash' do
it 'ignores references to the Group' do result = reference_pipeline_result("Hey #{reference}")
doc = filter("Hey #{reference}", current_user: user) expect(result[:references][:user]).to eq group.users
expect(doc.to_html).to eq "Hey #{reference}"
end
it 'does not add to the results hash' do
result = pipeline_result("Hey #{reference}", current_user: user)
expect(result[:references][:user]).to eq []
end
end end
end end
......
...@@ -13,7 +13,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -13,7 +13,7 @@ describe Gitlab::ReferenceExtractor do
project.team << [@u_bar, :guest] project.team << [@u_bar, :guest]
subject.analyze('@foo, @baduser, @bar, and @offteam') subject.analyze('@foo, @baduser, @bar, and @offteam')
expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam]) expect(subject.users).to match_array([@u_foo, @u_bar, @u_offteam])
end end
it 'ignores user mentions inside specific elements' do it 'ignores user mentions inside specific elements' do
...@@ -37,7 +37,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -37,7 +37,7 @@ describe Gitlab::ReferenceExtractor do
> @offteam > @offteam
}) })
expect(subject.users).to eq([]) expect(subject.users).to match_array([])
end end
it 'accesses valid issue objects' do it 'accesses valid issue objects' do
...@@ -45,7 +45,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -45,7 +45,7 @@ describe Gitlab::ReferenceExtractor do
@i1 = create(:issue, project: project) @i1 = create(:issue, project: project)
subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.") subject.analyze("#{@i0.to_reference}, #{@i1.to_reference}, and #{Issue.reference_prefix}999.")
expect(subject.issues).to eq([@i0, @i1]) expect(subject.issues).to match_array([@i0, @i1])
end end
it 'accesses valid merge requests' do it 'accesses valid merge requests' do
...@@ -53,7 +53,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -53,7 +53,7 @@ describe Gitlab::ReferenceExtractor do
@m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict') @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict')
subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.") subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.")
expect(subject.merge_requests).to eq([@m1, @m0]) expect(subject.merge_requests).to match_array([@m1, @m0])
end end
it 'accesses valid labels' do it 'accesses valid labels' do
...@@ -62,7 +62,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -62,7 +62,7 @@ describe Gitlab::ReferenceExtractor do
@l2 = create(:label) @l2 = create(:label)
subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}") subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}")
expect(subject.labels).to eq([@l0, @l1]) expect(subject.labels).to match_array([@l0, @l1])
end end
it 'accesses valid snippets' do it 'accesses valid snippets' do
...@@ -71,7 +71,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -71,7 +71,7 @@ describe Gitlab::ReferenceExtractor do
@s2 = create(:project_snippet) @s2 = create(:project_snippet)
subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}") subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}")
expect(subject.snippets).to eq([@s0, @s1]) expect(subject.snippets).to match_array([@s0, @s1])
end end
it 'accesses valid commits' do it 'accesses valid commits' do
...@@ -109,7 +109,7 @@ describe Gitlab::ReferenceExtractor do ...@@ -109,7 +109,7 @@ describe Gitlab::ReferenceExtractor do
subject.analyze("this refers issue #{issue.to_reference(project)}") subject.analyze("this refers issue #{issue.to_reference(project)}")
extracted = subject.issues extracted = subject.issues
expect(extracted.size).to eq(1) expect(extracted.size).to eq(1)
expect(extracted).to eq([issue]) expect(extracted).to match_array([issue])
end end
end end
end end
...@@ -29,12 +29,19 @@ module FilterSpecHelper ...@@ -29,12 +29,19 @@ module FilterSpecHelper
# #
# Returns the Hash # Returns the Hash
def pipeline_result(body, contexts = {}) def pipeline_result(body, contexts = {})
contexts.reverse_merge!(project: project) contexts.reverse_merge!(project: project) if defined?(project)
pipeline = HTML::Pipeline.new([described_class], contexts) pipeline = HTML::Pipeline.new([described_class], contexts)
pipeline.call(body) pipeline.call(body)
end end
def reference_pipeline_result(body, contexts = {})
contexts.reverse_merge!(project: project) if defined?(project)
pipeline = HTML::Pipeline.new([described_class, Gitlab::Markdown::ReferenceGathererFilter], contexts)
pipeline.call(body)
end
# Modify a String reference to make it invalid # Modify a String reference to make it invalid
# #
# Commit SHAs get reversed, IDs get incremented by 1, all other Strings get # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get
...@@ -55,20 +62,6 @@ module FilterSpecHelper ...@@ -55,20 +62,6 @@ module FilterSpecHelper
end end
end end
# Stub CrossProjectReference#user_can_reference_project? to return true for
# the current test
def allow_cross_reference!
allow_any_instance_of(described_class).
to receive(:user_can_reference_project?).and_return(true)
end
# Stub CrossProjectReference#user_can_reference_project? to return false for
# the current test
def disallow_cross_reference!
allow_any_instance_of(described_class).
to receive(:user_can_reference_project?).and_return(false)
end
# Shortcut to Rails' auto-generated routes helpers, to avoid including the # Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module # module
def urls def urls
......
...@@ -15,18 +15,17 @@ class MarkdownFeature ...@@ -15,18 +15,17 @@ class MarkdownFeature
end end
def group def group
unless @group @group ||= create(:group).tap do |group|
@group = create(:group) group.add_developer(user)
@group.add_developer(user)
end end
@group
end end
# Direct references ---------------------------------------------------------- # Direct references ----------------------------------------------------------
def project def project
@project ||= create(:project) @project ||= create(:project).tap do |project|
project.team << [user, :master]
end
end end
def issue def issue
...@@ -46,12 +45,10 @@ class MarkdownFeature ...@@ -46,12 +45,10 @@ class MarkdownFeature
end end
def commit_range def commit_range
unless @commit_range @commit_range ||= begin
commit2 = project.commit('HEAD~3') commit2 = project.commit('HEAD~3')
@commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) CommitRange.new("#{commit.id}...#{commit2.id}", project)
end end
@commit_range
end end
def simple_label def simple_label
...@@ -65,13 +62,12 @@ class MarkdownFeature ...@@ -65,13 +62,12 @@ class MarkdownFeature
# Cross-references ----------------------------------------------------------- # Cross-references -----------------------------------------------------------
def xproject def xproject
unless @xproject @xproject ||= begin
namespace = create(:namespace, name: 'cross-reference') namespace = create(:namespace, name: 'cross-reference')
@xproject = create(:project, namespace: namespace) create(:project, namespace: namespace) do |project|
@xproject.team << [user, :developer] project.team << [user, :developer]
end
end end
@xproject
end end
def xissue def xissue
...@@ -91,12 +87,10 @@ class MarkdownFeature ...@@ -91,12 +87,10 @@ class MarkdownFeature
end end
def xcommit_range def xcommit_range
unless @xcommit_range @xcommit_range ||= begin
xcommit2 = xproject.commit('HEAD~2') xcommit2 = xproject.commit('HEAD~2')
@xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject)
end end
@xcommit_range
end end
def raw_markdown def raw_markdown
......
...@@ -50,6 +50,8 @@ def common_mentionable_setup ...@@ -50,6 +50,8 @@ def common_mentionable_setup
} }
extra_commits.each { |c| commitmap[c.short_id] = c } extra_commits.each { |c| commitmap[c.short_id] = c }
allow(Project).to receive(:find).and_call_original
allow(Project).to receive(:find).with(project.id.to_s).and_return(project)
allow(project.repository).to receive(:commit) { |sha| commitmap[sha] } allow(project.repository).to receive(:commit) { |sha| commitmap[sha] }
set_mentionable_text.call(ref_string) set_mentionable_text.call(ref_string)
......
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