diff options
author | Douwe Maan <douwe@gitlab.com> | 2015-10-17 11:55:55 +0000 |
---|---|---|
committer | Douwe Maan <douwe@gitlab.com> | 2015-10-17 11:55:55 +0000 |
commit | 5ad3a274b3404286bb32b695c8f2b7bdd21e4953 (patch) | |
tree | 2b3fd69c3a8cfd064d11cb90ed8e5c7a1ab6f803 | |
parent | 5d010ecd8a3ed6e0a83b3c050c7bdfff833f77e1 (diff) | |
parent | 34148d15764898579cc44ea02f439e8359e01233 (diff) | |
download | gitlab-ce-5ad3a274b3404286bb32b695c8f2b7bdd21e4953.tar.gz |
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
44 files changed, 810 insertions, 476 deletions
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 12b87dca798..65813482120 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -19,7 +19,8 @@ module GitlabMarkdownHelper escape_once(body) 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) if fragment.children.size == 1 && fragment.children[0].name == 'a' @@ -45,29 +46,39 @@ module GitlabMarkdownHelper end def markdown(text, context = {}) + return "" unless text.present? + context.reverse_merge!( - current_user: current_user, path: @path, + pipeline: :default, project: @project, project_wiki: @project_wiki, 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 # TODO (rspeicher): Remove all usages of this helper and just call `markdown` # with a custom pipeline depending on the content being rendered def gfm(text, options = {}) + return "" unless text.present? + options.reverse_merge!( - current_user: current_user, path: @path, + pipeline: :default, project: @project, project_wiki: @project_wiki, 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 def asciidoc(text) diff --git a/app/models/commit.rb b/app/models/commit.rb index d5c50013525..23b5e38336c 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -2,13 +2,13 @@ class Commit extend ActiveModel::Naming include ActiveModel::Conversion - include Mentionable include Participable + include Mentionable include Referable include StaticModel attr_mentionable :safe_message - participant :author, :committer, :notes, :mentioned_users + participant :author, :committer, :notes attr_accessor :project diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 0e8bcc1a4ec..1d85a353a6b 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -6,8 +6,8 @@ # module Issuable extend ActiveSupport::Concern - include Mentionable include Participable + include Mentionable included do belongs_to :author, class_name: "User" @@ -47,8 +47,7 @@ module Issuable prefix: true attr_mentionable :title, :description - - participant :author, :assignee, :notes_with_associations, :mentioned_users + participant :author, :assignee, :notes_with_associations end module ClassMethods diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index b34def66d2e..193c91f1742 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -20,6 +20,12 @@ module Mentionable 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 # # By default this will be the class name and the result of calling @@ -41,22 +47,22 @@ module Mentionable self end - def all_references(current_user = self.author, text = self.mentionable_text) - ext = Gitlab::ReferenceExtractor.new(self.project, current_user) + def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true) + ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references) ext.analyze(text) ext end - def mentioned_users(current_user = nil) - all_references(current_user).users.uniq + def mentioned_users(current_user = nil, load_lazy_references: true) + all_references(current_user, load_lazy_references: load_lazy_references).users end # 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? - refs = all_references(current_user, text) - (refs.issues + refs.merge_requests + refs.commits).uniq - [local_reference] + refs = all_references(current_user, text, load_lazy_references: load_lazy_references) + (refs.issues + refs.merge_requests + refs.commits) - [local_reference] end # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+. diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index ffc874357fd..85367f89f4f 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -12,7 +12,7 @@ # # # ... # -# participant :author, :assignee, :mentioned_users, :notes +# participant :author, :assignee, :notes, ->(current_user) { mentioned_users(current_user) } # end # # issue = Issue.last @@ -27,7 +27,7 @@ module Participable module ClassMethods def participant(*attrs) - participant_attrs.concat(attrs.map(&:to_s)) + participant_attrs.concat(attrs) end def participant_attrs @@ -37,33 +37,39 @@ module Participable # 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 - def participants(current_user = self.author) - self.class.participant_attrs.flat_map do |attr| - meth = method(attr) - + def participants(current_user = self.author, load_lazy_references: true) + participants = self.class.participant_attrs.flat_map do |attr| value = - if meth.arity == 1 || meth.arity == -1 - meth.call(current_user) + if attr.respond_to?(:call) + instance_exec(current_user, &attr) else - meth.call + send(attr) end participants_for(value, current_user) - end.compact.uniq.select do |user| - user.can?(:read_project, self.project) + end.compact.uniq + + if load_lazy_references + participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq + + participants.select! do |user| + user.can?(:read_project, project) + end end + + participants end private def participants_for(value, current_user = nil) case value - when User + when User, Gitlab::Markdown::ReferenceFilter::LazyReference [value] when Enumerable, ActiveRecord::Relation value.flat_map { |v| participants_for(v, current_user) } when Participable - value.participants(current_user) + value.participants(current_user, load_lazy_references: false) end end end diff --git a/app/models/note.rb b/app/models/note.rb index ebbdd2f33a1..0b3aa30abd7 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -22,14 +22,14 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Note < ActiveRecord::Base - include Mentionable include Gitlab::CurrentSettings include Participable + include Mentionable default_value_for :system, false attr_mentionable :note - participant :author, :mentioned_users + participant :author belongs_to :project belongs_to :noteable, polymorphic: true diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml index 4259f64c191..fad65310021 100644 --- a/app/views/events/_event_issue.atom.haml +++ b/app/views/events/_event_issue.atom.haml @@ -1,3 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - - if issue.description.present? - = markdown(issue.description, xhtml: true, reference_only_path: false, project: issue.project) + = markdown(issue.description, pipeline: :atom, project: issue.project) diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml index e8ed13df783..19bdc7b9ca5 100644 --- a/app/views/events/_event_merge_request.atom.haml +++ b/app/views/events/_event_merge_request.atom.haml @@ -1,3 +1,2 @@ %div{xmlns: "http://www.w3.org/1999/xhtml"} - - if merge_request.description.present? - = markdown(merge_request.description, xhtml: true, reference_only_path: false, project: merge_request.project) + = markdown(merge_request.description, pipeline: :atom, project: merge_request.project) diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml index cfbfba50202..b730ebbd5f9 100644 --- a/app/views/events/_event_note.atom.haml +++ b/app/views/events/_event_note.atom.haml @@ -1,2 +1,2 @@ %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) diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml index 3625cb49d8b..b271b9daff1 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -6,7 +6,7 @@ %i at = 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 %p %i diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml index 3fd4b04ac84..00cb4aa24cc 100644 --- a/app/views/notify/_note_message.html.haml +++ b/app/views/notify/_note_message.html.haml @@ -1,2 +1,2 @@ %div - = markdown(@note.note, reference_only_path: false) + = markdown(@note.note, pipeline: :email) diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index 53a068be52e..d3b799fca23 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,5 +1,5 @@ -if @issue.description - = markdown(@issue.description, reference_only_path: false) + = markdown(@issue.description, pipeline: :email) - if @issue.assignee_id.present? %p diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 5b7dd117c16..90ebdfc3fe2 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -6,4 +6,4 @@ Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name} -if @merge_request.description - = markdown(@merge_request.description, reference_only_path: false) + = markdown(@merge_request.description, pipeline: :email) diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 32a368c2e2b..b082bfc434b 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -7,6 +7,14 @@ module Gitlab module Markdown # 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 # context - Hash of context options passed to our HTML Pipeline # @@ -31,6 +39,33 @@ module Gitlab renderer.render(markdown) 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 autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter' autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter' @@ -41,6 +76,7 @@ module Gitlab autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter' autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter' autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter' + autoload :RedactorFilter, 'gitlab/markdown/redactor_filter' autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter' autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter' autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter' @@ -50,26 +86,20 @@ module Gitlab autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_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 - # options - A Hash of options used to customize output (default: {}): - # :xhtml - output XHTML instead of HTML - # :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] - ) + # Returns an HTML-safe String + def self.gfm(html, options = {}) + return '' unless html.present? @pipeline ||= HTML::Pipeline.new(filters) @@ -78,41 +108,36 @@ module Gitlab pipeline: options[:pipeline], # EmojiFilter - asset_root: Gitlab.config.gitlab.base_url, asset_host: Gitlab::Application.config.asset_host, - - # TableOfContentsFilter - no_header_anchors: options[:no_header_anchors], + asset_root: Gitlab.config.gitlab.base_url, # ReferenceFilter - current_user: options[:current_user], - only_path: options[:reference_only_path], - project: options[:project], + only_path: only_path_pipeline?(options[:pipeline]), + project: options[:project], # RelativeLinkFilter + project_wiki: options[:project_wiki], ref: options[:ref], requested_path: options[:path], - project_wiki: options[:project_wiki] - } - - result = @pipeline.call(text, context) - save_options = 0 - if options[:xhtml] - save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML - end - - text = result[:output].to_html(save_with: save_options) + # TableOfContentsFilter + no_header_anchors: options[:no_header_anchors] + } - text.html_safe + @pipeline.to_html(html, context).html_safe end private - def self.renderer - @markdown ||= begin - renderer = Redcarpet::Render::HTML.new - Redcarpet::Markdown.new(renderer, redcarpet_options) + # Check if a pipeline enables the `only_path` context option + # + # Returns Boolean + def self.only_path_pipeline?(pipeline) + case pipeline + when :atom, :email + false + else + true end end @@ -130,6 +155,17 @@ module Gitlab }.freeze 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 # # SanitizationFilter should come first so that all generated reference HTML diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb index bb496135d92..e070edae0a4 100644 --- a/lib/gitlab/markdown/commit_range_reference_filter.rb +++ b/lib/gitlab/markdown/commit_range_reference_filter.rb @@ -26,6 +26,18 @@ module Gitlab 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) super @@ -53,13 +65,11 @@ module Gitlab range = CommitRange.new(id, project) if range.valid_commits? - push_result(:commit_range, range) - url = url_for_commit_range(project, range) title = range.reference_title klass = reference_class(:commit_range) - data = data_attribute(project.id) + data = data_attribute(project: project.id, commit_range: id) project_ref += '@' if project_ref diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb index fcbb2e936a5..8cdbeb1f9cf 100644 --- a/lib/gitlab/markdown/commit_reference_filter.rb +++ b/lib/gitlab/markdown/commit_reference_filter.rb @@ -26,6 +26,18 @@ module Gitlab 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 replace_text_nodes_matching(Commit.reference_pattern) do |content| commit_link_filter(content) @@ -39,17 +51,15 @@ module Gitlab # 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| + self.class.references_in(text) do |match, id, project_ref| project = self.project_from_ref(project_ref) - if commit = commit_from_ref(project, commit_ref) - push_result(:commit, commit) - + if commit = self.class.commit_from_ref(project, id) url = url_for_commit(project, commit) title = escape_once(commit.link_title) klass = reference_class(:commit) - data = data_attribute(project.id) + data = data_attribute(project: project.id, commit: id) project_ref += '@' if project_ref @@ -62,9 +72,9 @@ module Gitlab end end - def commit_from_ref(project, commit_ref) + def self.commit_from_ref(project, id) if project && project.valid_repo? - project.commit(commit_ref) + project.commit(id) end end diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb index 855748fdccc..6ab04a584b0 100644 --- a/lib/gitlab/markdown/cross_project_reference.rb +++ b/lib/gitlab/markdown/cross_project_reference.rb @@ -13,18 +13,11 @@ module Gitlab # # 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) 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) + Project.find_with_namespace(ref) end end end diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb index f7c43e1ca89..8f86f13976a 100644 --- a/lib/gitlab/markdown/external_issue_reference_filter.rb +++ b/lib/gitlab/markdown/external_issue_reference_filter.rb @@ -47,8 +47,9 @@ module Gitlab title = escape_once("Issue in #{project.external_issue_tracker.title}") klass = reference_class(:issue) + data = data_attribute(project: project.id) - %(<a href="#{url}" + %(<a href="#{url}" #{data} title="#{title}" class="#{klass}">#{match}</a>) end diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb index 01320f80796..481d282f7b1 100644 --- a/lib/gitlab/markdown/issue_reference_filter.rb +++ b/lib/gitlab/markdown/issue_reference_filter.rb @@ -27,6 +27,10 @@ module Gitlab end end + def self.referenced_by(node) + { issue: LazyReference.new(Issue, node.attr("data-issue")) } + end + def call replace_text_nodes_matching(Issue.reference_pattern) do |content| issue_link_filter(content) @@ -45,13 +49,11 @@ module Gitlab project = self.project_from_ref(project_ref) if project && issue = project.get_issue(id) - push_result(:issue, issue) - url = url_for_issue(id, project, only_path: context[:only_path]) title = escape_once("Issue: #{issue.title}") klass = reference_class(:issue) - data = data_attribute(project.id) + data = data_attribute(project: project.id, issue: issue.id) %(<a href="#{url}" #{data} title="#{title}" diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb index 1e5cb12071e..618acb7a578 100644 --- a/lib/gitlab/markdown/label_reference_filter.rb +++ b/lib/gitlab/markdown/label_reference_filter.rb @@ -22,6 +22,10 @@ module Gitlab end end + def self.referenced_by(node) + { label: LazyReference.new(Label, node.attr("data-label")) } + end + def call replace_text_nodes_matching(Label.reference_pattern) do |content| label_link_filter(content) @@ -41,11 +45,9 @@ module Gitlab params = label_params(id, name) if label = project.labels.find_by(params) - push_result(:label, label) - url = url_for_label(project, label) klass = reference_class(:label) - data = data_attribute(project.id) + data = data_attribute(project: project.id, label: label.id) %(<a href="#{url}" #{data} class="#{klass}">#{render_colored_label(label)}</a>) diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb index ecbd263d0e0..5bc63269808 100644 --- a/lib/gitlab/markdown/merge_request_reference_filter.rb +++ b/lib/gitlab/markdown/merge_request_reference_filter.rb @@ -27,6 +27,10 @@ module Gitlab end end + def self.referenced_by(node) + { merge_request: LazyReference.new(MergeRequest, node.attr("data-merge-request")) } + end + def call replace_text_nodes_matching(MergeRequest.reference_pattern) do |content| merge_request_link_filter(content) @@ -45,11 +49,9 @@ module Gitlab project = self.project_from_ref(project_ref) 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}") 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) diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/gitlab/markdown/redactor_filter.rb new file mode 100644 index 00000000000..a1f3a8a8ebf --- /dev/null +++ b/lib/gitlab/markdown/redactor_filter.rb @@ -0,0 +1,40 @@ +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 diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb index 9b293c957d6..adaca78ba27 100644 --- a/lib/gitlab/markdown/reference_filter.rb +++ b/lib/gitlab/markdown/reference_filter.rb @@ -11,30 +11,57 @@ module Gitlab # Context options: # :project (required) - Current project, ignored if reference is cross-project. # :only_path - Generate path-only links. - # - # Results: - # :references - A Hash of references that were found and replaced. class ReferenceFilter < HTML::Pipeline::Filter - def initialize(*args) - super + LazyReference = Struct.new(:klass, :ids) do + 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 # Returns a data attribute String to attach to a reference link # - # id - Object ID - # type - Object type (default: :project) + # attributes - Hash, where the key becomes the data attribute name and the + # value is the data attribute value # # Examples: # - # data_attribute(1) # => "data-project-id=\"1\"" - # data_attribute(2, :user) # => "data-user-id=\"2\"" - # data_attribute(3, :group) # => "data-group-id=\"3\"" + # data_attribute(project: 1, issue: 2) + # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" + # + # data_attribute(project: 3, merge_request: 4) + # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" # # Returns a String - def data_attribute(id, type = :project) - %Q(data-#{type}-id="#{id}") + def data_attribute(attributes = {}) + attributes[:reference_filter] = self.class.name + attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ") end def escape_once(html) @@ -59,16 +86,6 @@ module Gitlab context[:project] 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) "gfm gfm-#{type}" end @@ -85,7 +102,7 @@ module Gitlab # 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. + # Returns the updated Nokogiri::HTML::DocumentFragment object. def replace_text_nodes_matching(pattern) return doc if project.nil? diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/gitlab/markdown/reference_gatherer_filter.rb new file mode 100644 index 00000000000..00f983675e6 --- /dev/null +++ b/lib/gitlab/markdown/reference_gatherer_filter.rb @@ -0,0 +1,63 @@ +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 diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb index e2cf89cb1d8..f783f951711 100644 --- a/lib/gitlab/markdown/snippet_reference_filter.rb +++ b/lib/gitlab/markdown/snippet_reference_filter.rb @@ -27,6 +27,10 @@ module Gitlab end end + def self.referenced_by(node) + { snippet: LazyReference.new(Snippet, node.attr("data-snippet")) } + end + def call replace_text_nodes_matching(Snippet.reference_pattern) do |content| snippet_link_filter(content) @@ -45,11 +49,9 @@ module Gitlab project = self.project_from_ref(project_ref) if project && snippet = project.snippets.find_by(id: id) - push_result(:snippet, snippet) - title = escape_once("Snippet: #{snippet.title}") klass = reference_class(:snippet) - data = data_attribute(project.id) + data = data_attribute(project: project.id, snippet: snippet.id) url = url_for_snippet(snippet, project) diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb index 6f436ea7167..2a594e1662e 100644 --- a/lib/gitlab/markdown/user_reference_filter.rb +++ b/lib/gitlab/markdown/user_reference_filter.rb @@ -23,6 +23,31 @@ module Gitlab 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 replace_text_nodes_matching(User.reference_pattern) do |content| user_link_filter(content) @@ -61,14 +86,12 @@ module Gitlab def link_to_all project = context[:project] - # FIXME (rspeicher): Law of Demeter - push_result(:user, *project.team.members.flatten) - url = urls.namespace_project_url(project.namespace, project, only_path: context[:only_path]) + data = data_attribute(project: project.id) text = User.reference_prefix + 'all' - %(<a href="#{url}" class="#{link_class}">#{text}</a>) + %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) end def link_to_namespace(namespace) @@ -80,30 +103,20 @@ module Gitlab end 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]) - data = data_attribute(namespace.id, :group) + data = data_attribute(group: namespace.id) text = Group.reference_prefix + group %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) end def link_to_user(user, namespace) - push_result(:user, namespace.owner) - 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 %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>) end - - def user_can_reference_group?(group) - Ability.abilities.allowed?(context[:current_user], :read_group, group) - end end end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index 30497e274c2..333bd059055 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -3,11 +3,12 @@ require 'gitlab/markdown' module Gitlab # Extract possible GFM references from an arbitrary String for further processing. 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 @current_user = current_user + @load_lazy_references = load_lazy_references end def analyze(text) @@ -28,7 +29,7 @@ module Gitlab type = type.to_sym return references[type] if references.has_key?(type) - references[type] = pipeline_result(type).uniq + references[type] = pipeline_result(type) end end @@ -41,21 +42,32 @@ module Gitlab def pipeline_result(filter_type) return [] if @text.blank? - klass = filter_type.to_s.camelize + 'ReferenceFilter' + klass = "#{filter_type.to_s.camelize}ReferenceFilter" filter = Gitlab::Markdown.const_get(klass) context = { project: project, current_user: current_user, + # We don't actually care about the links generated 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[: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 diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index c557a1061af..fdd8cf07b12 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -220,7 +220,7 @@ describe 'GitLab Markdown', feature: true do end end - # `markdown` calls these two methods + # Fake a `current_user` helper def current_user @feat.user end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 20ae29e2bd3..762ec25c4f5 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -11,12 +11,15 @@ describe GitlabMarkdownHelper do let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:snippet) { create(:project_snippet, project: project) } - # Helper expects a current_user method. - let(:current_user) { user } - before do + # Ensure the generated reference links aren't redacted + project.team << [user, :master] + # 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 describe "#markdown" do @@ -25,23 +28,23 @@ describe GitlabMarkdownHelper do it "should link to the merge request" do 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 it "should link to the commit" do expected = namespace_project_commit_path(project.namespace, project, commit) - expect(markdown(actual)).to match(expected) + expect(helper.markdown(actual)).to match(expected) end it "should link to the issue" do expected = namespace_project_issue_path(project.namespace, project, issue) - expect(markdown(actual)).to match(expected) + expect(helper.markdown(actual)).to match(expected) end end describe "override default project" do let(:actual) { issue.to_reference } - let(:second_project) { create(:project) } + let(:second_project) { create(:project, :public) } let(:second_issue) { create(:issue, project: second_project) } it 'should link to the issue' do @@ -56,7 +59,7 @@ describe GitlabMarkdownHelper do let(:issues) { create_list(:issue, 2, project: project) } 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) # Make sure we didn't create invalid markup @@ -86,7 +89,7 @@ describe GitlabMarkdownHelper do end 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) expect(doc.css('a')).to satisfy do |v| @@ -97,13 +100,13 @@ describe GitlabMarkdownHelper do it "escapes HTML passed in as the body" do 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('<h1>test</h1>') end it 'ignores reference links when they are the entire body' do 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>) end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 5d7ff4f6122..21254f778d3 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -140,28 +140,28 @@ describe Gitlab::ClosingIssueExtractor do message = "Closes #{reference} and fix #{reference2}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue]) + to match_array([issue, other_issue]) end it 'fetches comma-separated issues references in single line message' do message = "Closes #{reference}, closes #{reference2}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue]) + to match_array([issue, other_issue]) end it 'fetches comma-separated issues numbers in single line message' do message = "Closes #{reference}, #{reference2} and #{reference3}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue, third_issue]) + to match_array([issue, other_issue, third_issue]) end it 'fetches issues in multi-line message' do message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" expect(subject.closed_by_message(message)). - to eq([issue, other_issue]) + to match_array([issue, other_issue]) end it 'fetches issues in hybrid message' do @@ -169,7 +169,7 @@ describe Gitlab::ClosingIssueExtractor do "Also fixing issues #{reference2}, #{reference3} and #4" expect(subject.closed_by_message(message)). - to eq([issue, other_issue, third_issue]) + to match_array([issue, other_issue, third_issue]) end end end diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb index 3c6c84a0416..e5b8d723fe5 100644 --- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe CommitRangeReferenceFilter do include FilterSpecHelper - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:commit1) { project.commit } let(:commit2) { project.commit("HEAD~2") } @@ -75,12 +75,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("See #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + 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 it 'supports an :only_path option' do @@ -92,59 +100,45 @@ module Gitlab::Markdown end 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 end end context 'cross-project reference' do 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) } before do range.project = project2 end - 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, 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 'links to a valid reference' do + doc = filter("See #{reference}") - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" - expect(filter(act).to_html).to eq exp + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param) + end - exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" - expect(filter(act).to_html).to eq exp - end + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") - it 'adds to the results hash' do - result = pipeline_result("See #{reference}") - expect(result[:references][:commit_range]).not_to be_empty - end + exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}") + expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'ignores invalid commit IDs on the referenced project' do + 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 = "See #{reference}" + exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" + expect(filter(act).to_html).to eq exp + end - expect(filter(act).to_html).to eq exp - end + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit_range]).not_to be_empty end end end diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb index 9ed438252b3..d080efbf3d4 100644 --- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe CommitReferenceFilter do include FilterSpecHelper - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:commit) { project.commit } it 'requires project context' do @@ -71,12 +71,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("See #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + 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 it 'supports an :only_path context' do @@ -88,53 +96,39 @@ module Gitlab::Markdown end 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 end end context 'cross-project reference' do 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(:reference) { commit.to_reference(project) } - 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}.)") + it 'links to a valid reference' do + doc = filter("See #{reference}") - exp = Regexp.escape(project2.to_reference) - expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) - end + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id) + end - it 'ignores invalid commit IDs on the referenced project' do - exp = act = "Committed #{invalidate_reference(reference)}" - expect(filter(act).to_html).to eq exp - end + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") - it 'adds to the results hash' do - result = pipeline_result("See #{reference}") - expect(result[:references][:commit]).not_to be_empty - end + exp = Regexp.escape(project2.to_reference) + expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } - - it 'ignores valid references' do - exp = act = "See #{reference}" + it 'ignores invalid commit IDs on the referenced project' do + exp = act = "Committed #{invalidate_reference(reference)}" + expect(filter(act).to_html).to eq exp + end - expect(filter(act).to_html).to eq exp - end + it 'adds to the results hash' do + result = reference_pipeline_result("See #{reference}") + expect(result[:references][:commit]).not_to be_empty end end end diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb index 4698d6138c2..8d4f9e403a6 100644 --- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb +++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb @@ -2,20 +2,16 @@ 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] + project = double + + allow(self).to receive(:context).and_return({ project: project }) + + expect(project_from_ref(nil)).to eq project end end @@ -26,29 +22,13 @@ module Gitlab::Markdown end 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). 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 + expect(project_from_ref('cross/reference')).to eq project2 end end end diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb index 1dd54f58588..94c80ae6611 100644 --- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb @@ -8,7 +8,7 @@ module Gitlab::Markdown IssuesHelper end - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:issue) { create(:issue, project: project) } it 'requires project context' do @@ -68,12 +68,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Issue #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + 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 it 'supports an :only_path context' do @@ -85,60 +93,46 @@ module Gitlab::Markdown end 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] end end context 'cross-project reference' do 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(:reference) { issue.to_reference(project) } - 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(:get_issue). - with(issue.iid).and_return(nil) - - exp = act = "Issue #{reference}" - expect(filter(act).to_html).to eq exp - end + 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) - 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 + exp = act = "Issue #{reference}" + expect(filter(act).to_html).to eq exp + end - it 'ignores invalid issue IDs on the referenced project' do - exp = act = "Fixed #{invalidate_reference(reference)}" + it 'links to a valid reference' do + 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 'adds to the results hash' do - result = pipeline_result("Fixed #{reference}") - expect(result[:references][:issue]).to eq [issue] - end + it 'links with adjacent text' do + doc = filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'ignores invalid issue IDs on the referenced project' do + exp = act = "Fixed #{invalidate_reference(reference)}" - it 'ignores valid references' do - exp = act = "See #{reference}" + expect(filter(act).to_html).to eq exp + end - expect(filter(act).to_html).to eq exp - end + it 'adds to the results hash' do + result = reference_pipeline_result("Fixed #{reference}") + expect(result[:references][:issue]).to eq [issue] end end end diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb index e32089de376..fc21b65a843 100644 --- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb @@ -5,7 +5,7 @@ module Gitlab::Markdown describe LabelReferenceFilter do include FilterSpecHelper - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:label) { create(:label, project: project) } let(:reference) { label.to_reference } @@ -25,12 +25,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Label #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + 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 it 'supports an :only_path context' do @@ -42,7 +50,7 @@ module Gitlab::Markdown end 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] end diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb index 66616b93368..3ef6cdfff33 100644 --- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe MergeRequestReferenceFilter do include FilterSpecHelper - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:merge) { create(:merge_request, source_project: project) } it 'requires project context' do @@ -56,12 +56,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Merge #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + 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 it 'supports an :only_path context' do @@ -73,53 +81,39 @@ module Gitlab::Markdown end 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] end end context 'cross-project reference' do 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(:reference) { merge.to_reference(project) } - 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 #{invalidate_reference(reference)}" + it 'links to a valid reference' do + doc = filter("See #{reference}") - expect(filter(act).to_html).to eq exp - end + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_merge_request_url(project2.namespace, + project, merge) + end - it 'adds to the results hash' do - result = pipeline_result("Merge #{reference}") - expect(result[:references][:merge_request]).to eq [merge] - end + it 'links with adjacent text' do + doc = filter("Merge (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'ignores invalid merge IDs on the referenced project' do + exp = act = "Merge #{invalidate_reference(reference)}" - it 'ignores valid references' do - exp = act = "See #{reference}" + expect(filter(act).to_html).to eq exp + end - expect(filter(act).to_html).to eq exp - end + it 'adds to the results hash' do + result = reference_pipeline_result("Merge #{reference}") + expect(result[:references][:merge_request]).to eq [merge] end end end diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb new file mode 100644 index 00000000000..eea3f1cf370 --- /dev/null +++ b/spec/lib/gitlab/markdown/redactor_filter_spec.rb @@ -0,0 +1,91 @@ +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 diff --git a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb new file mode 100644 index 00000000000..4fa473ad191 --- /dev/null +++ b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb @@ -0,0 +1,89 @@ +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 diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb index fd3f0d20fad..9d9652dba46 100644 --- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe SnippetReferenceFilter do include FilterSpecHelper - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:snippet) { create(:project_snippet, project: project) } let(:reference) { snippet.to_reference } @@ -55,12 +55,20 @@ module Gitlab::Markdown expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet' end - it 'includes a data-project-id attribute' do + it 'includes a data-project attribute' do doc = filter("Snippet #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-project-id') - expect(link.attr('data-project-id')).to eq project.id.to_s + expect(link).to have_attribute('data-project') + 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 it 'supports an :only_path context' do @@ -72,52 +80,38 @@ module Gitlab::Markdown end 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] end end context 'cross-project reference' do 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(:reference) { snippet.to_reference(project) } - 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 #{invalidate_reference(reference)}" + it 'links to a valid reference' do + doc = filter("See #{reference}") - expect(filter(act).to_html).to eq exp - end + expect(doc.css('a').first.attr('href')). + to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet) + end - it 'adds to the results hash' do - result = pipeline_result("Snippet #{reference}") - expect(result[:references][:snippet]).to eq [snippet] - end + it 'links with adjacent text' do + doc = filter("See (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/) end - context 'when user cannot access reference' do - before { disallow_cross_reference! } + it 'ignores invalid snippet IDs on the referenced project' do + exp = act = "See #{invalidate_reference(reference)}" - it 'ignores valid references' do - exp = act = "See #{reference}" + expect(filter(act).to_html).to eq exp + end - expect(filter(act).to_html).to eq exp - end + it 'adds to the results hash' do + result = reference_pipeline_result("Snippet #{reference}") + expect(result[:references][:snippet]).to eq [snippet] end end end diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb index b2155fab59b..d9e0d7c42db 100644 --- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb +++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb @@ -4,7 +4,7 @@ module Gitlab::Markdown describe UserReferenceFilter do include FilterSpecHelper - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:user) { create(:user) } let(:reference) { user.to_reference } @@ -39,7 +39,7 @@ module Gitlab::Markdown end 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] end end @@ -64,59 +64,40 @@ module Gitlab::Markdown expect(doc.css('a').length).to eq 1 end - it 'includes a data-user-id attribute' do + it 'includes a data-user attribute' do doc = filter("Hey #{reference}") link = doc.css('a').first - expect(link).to have_attribute('data-user-id') - expect(link.attr('data-user-id')).to eq user.namespace.owner_id.to_s + expect(link).to have_attribute('data-user') + expect(link.attr('data-user')).to eq user.namespace.owner_id.to_s end 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] end end context 'mentioning a group' do let(:group) { create(:group) } - let(:user) { create(:user) } let(:reference) { group.to_reference } - context 'that the current user can read' do - before do - group.add_developer(user) - 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 + it 'links to the Group' do + doc = filter("Hey #{reference}") + expect(doc.css('a').first.attr('href')).to eq urls.group_url(group) + end - expect(link).to have_attribute('data-group-id') - expect(link.attr('data-group-id')).to eq group.id.to_s - end + it 'includes a data-group attribute' do + doc = filter("Hey #{reference}") + link = doc.css('a').first - it 'adds to the results hash' do - result = pipeline_result("Hey #{reference}", current_user: user) - expect(result[:references][:user]).to eq group.users - end + expect(link).to have_attribute('data-group') + expect(link.attr('data-group')).to eq group.id.to_s end - context 'that the current user cannot read' do - it 'ignores references to the Group' do - doc = filter("Hey #{reference}", current_user: user) - 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 + it 'adds to the results hash' do + result = reference_pipeline_result("Hey #{reference}") + expect(result[:references][:user]).to eq group.users end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 088e34f050c..ad84d2274e8 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::ReferenceExtractor do project.team << [@u_bar, :guest] 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 it 'ignores user mentions inside specific elements' do @@ -37,7 +37,7 @@ describe Gitlab::ReferenceExtractor do > @offteam }) - expect(subject.users).to eq([]) + expect(subject.users).to match_array([]) end it 'accesses valid issue objects' do @@ -45,7 +45,7 @@ describe Gitlab::ReferenceExtractor do @i1 = create(:issue, project: project) 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 it 'accesses valid merge requests' do @@ -53,7 +53,7 @@ describe Gitlab::ReferenceExtractor do @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'feature_conflict') 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 it 'accesses valid labels' do @@ -62,7 +62,7 @@ describe Gitlab::ReferenceExtractor do @l2 = create(:label) 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 it 'accesses valid snippets' do @@ -71,7 +71,7 @@ describe Gitlab::ReferenceExtractor do @s2 = create(:project_snippet) 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 it 'accesses valid commits' do @@ -109,7 +109,7 @@ describe Gitlab::ReferenceExtractor do subject.analyze("this refers issue #{issue.to_reference(project)}") extracted = subject.issues expect(extracted.size).to eq(1) - expect(extracted).to eq([issue]) + expect(extracted).to match_array([issue]) end end end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index 203117aee70..97e5c270a59 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -29,12 +29,19 @@ module FilterSpecHelper # # Returns the Hash 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.call(body) 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 # # Commit SHAs get reversed, IDs get incremented by 1, all other Strings get @@ -55,20 +62,6 @@ module FilterSpecHelper 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 # module def urls diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index 39a64391460..bedc1a7f1db 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -15,18 +15,17 @@ class MarkdownFeature end def group - unless @group - @group = create(:group) - @group.add_developer(user) + @group ||= create(:group).tap do |group| + group.add_developer(user) end - - @group end # Direct references ---------------------------------------------------------- def project - @project ||= create(:project) + @project ||= create(:project).tap do |project| + project.team << [user, :master] + end end def issue @@ -46,12 +45,10 @@ class MarkdownFeature end def commit_range - unless @commit_range + @commit_range ||= begin commit2 = project.commit('HEAD~3') - @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) + CommitRange.new("#{commit.id}...#{commit2.id}", project) end - - @commit_range end def simple_label @@ -65,13 +62,12 @@ class MarkdownFeature # Cross-references ----------------------------------------------------------- def xproject - unless @xproject + @xproject ||= begin namespace = create(:namespace, name: 'cross-reference') - @xproject = create(:project, namespace: namespace) - @xproject.team << [user, :developer] + create(:project, namespace: namespace) do |project| + project.team << [user, :developer] + end end - - @xproject end def xissue @@ -91,12 +87,10 @@ class MarkdownFeature end def xcommit_range - unless @xcommit_range + @xcommit_range ||= begin xcommit2 = xproject.commit('HEAD~2') - @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) + CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) end - - @xcommit_range end def raw_markdown diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index f584904845e..3bb568f4d49 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -50,6 +50,8 @@ def common_mentionable_setup } 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] } set_mentionable_text.call(ref_string) |